diff --git a/packages/core/module.json b/packages/core/module.json new file mode 100644 index 00000000..f3dcc57b --- /dev/null +++ b/packages/core/module.json @@ -0,0 +1,23 @@ +{ + "id": "core", + "name": "@esengine/ecs-framework", + "displayName": "Core ECS", + "outputPath": "dist/index.mjs", + "description": "Core Entity-Component-System framework | 核心 ECS 框架", + "version": "1.0.0", + "category": "Core", + "icon": "Box", + "tags": ["ecs", "entity", "component", "system"], + "isCore": true, + "defaultEnabled": true, + "isEngineModule": true, + "canContainContent": false, + "platforms": ["web", "desktop", "mobile"], + "dependencies": [], + "exports": { + "components": ["Component", "Transform"], + "systems": ["System"], + "other": ["World", "Entity", "EntityManager", "SystemManager"] + }, + "requiresWasm": false +} diff --git a/packages/core/package.json b/packages/core/package.json index 6f3f97fc..bf520065 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,10 +30,10 @@ "clean": "rimraf bin dist tsconfig.tsbuildinfo", "build:ts": "tsc", "prebuild": "npm run clean", - "build": "npm run build:ts", + "build": "npm run build:ts && node build-rollup.cjs", "build:watch": "tsc --watch", "rebuild": "npm run clean && npm run build", - "build:npm": "npm run build && node build-rollup.cjs", + "build:npm": "npm run build", "test": "jest --config jest.config.cjs", "test:watch": "jest --watch --config jest.config.cjs", "test:performance": "jest --config jest.performance.config.cjs", diff --git a/packages/core/rollup.config.cjs b/packages/core/rollup.config.cjs index 627bd105..4727f8e6 100644 --- a/packages/core/rollup.config.cjs +++ b/packages/core/rollup.config.cjs @@ -70,7 +70,7 @@ module.exports = [ warn(warning); }, treeshake: { - moduleSideEffects: false, + moduleSideEffects: (id) => id.includes('reflect-metadata'), propertyReadSideEffects: false, unknownGlobalSideEffects: false } @@ -102,7 +102,7 @@ module.exports = [ warn(warning); }, treeshake: { - moduleSideEffects: false + moduleSideEffects: (id) => id.includes('reflect-metadata') } }, @@ -133,7 +133,7 @@ module.exports = [ warn(warning); }, treeshake: { - moduleSideEffects: false + moduleSideEffects: (id) => id.includes('reflect-metadata') } }, @@ -193,10 +193,10 @@ module.exports = [ warn(warning); }, treeshake: { - moduleSideEffects: false + moduleSideEffects: (id) => id.includes('reflect-metadata') } }, - + // 类型定义构建 { input: 'bin/index.d.ts', diff --git a/packages/ecs-engine-bindgen/module.json b/packages/ecs-engine-bindgen/module.json new file mode 100644 index 00000000..4573af5e --- /dev/null +++ b/packages/ecs-engine-bindgen/module.json @@ -0,0 +1,21 @@ +{ + "id": "ecs-engine-bindgen", + "name": "@esengine/ecs-engine-bindgen", + "displayName": "Engine Bindgen", + "description": "Bridge between ECS and Rust Engine | ECS 与 Rust 引擎之间的桥接层", + "version": "0.1.0", + "category": "Core", + "icon": "Link", + "tags": ["engine", "bindgen", "wasm", "rendering"], + "isCore": true, + "defaultEnabled": true, + "isEngineModule": true, + "canContainContent": false, + "platforms": ["web", "desktop"], + "dependencies": ["core", "math"], + "exports": { + "other": ["EngineBridge", "EngineRenderSystem", "CameraSystem"] + }, + "requiresWasm": true, + "outputPath": "dist/index.js" +} diff --git a/packages/ecs-engine-bindgen/package.json b/packages/ecs-engine-bindgen/package.json index 5f1699d3..032108e3 100644 --- a/packages/ecs-engine-bindgen/package.json +++ b/packages/ecs-engine-bindgen/package.json @@ -43,6 +43,7 @@ "@esengine/sprite": "workspace:*", "@esengine/camera": "workspace:*", "@esengine/asset-system": "workspace:*", + "@esengine/material-system": "workspace:*", "tsup": "^8.5.1", "typescript": "^5.8.0", "rimraf": "^5.0.0" diff --git a/packages/ecs-engine-bindgen/src/core/EngineBridge.ts b/packages/ecs-engine-bindgen/src/core/EngineBridge.ts index d6085b20..afd552e9 100644 --- a/packages/ecs-engine-bindgen/src/core/EngineBridge.ts +++ b/packages/ecs-engine-bindgen/src/core/EngineBridge.ts @@ -58,6 +58,7 @@ export class EngineBridge implements IEngineBridge { private textureIdBuffer: Uint32Array; private uvBuffer: Float32Array; private colorBuffer: Uint32Array; + private materialIdBuffer: Uint32Array; // Statistics | 统计信息 private stats: EngineStats = { @@ -92,6 +93,7 @@ export class EngineBridge implements IEngineBridge { this.textureIdBuffer = new Uint32Array(maxSprites); this.uvBuffer = new Float32Array(maxSprites * 4); // u0, v0, u1, v1 this.colorBuffer = new Uint32Array(maxSprites); + this.materialIdBuffer = new Uint32Array(maxSprites); } /** @@ -235,6 +237,9 @@ export class EngineBridge implements IEngineBridge { // Color | 颜色 this.colorBuffer[i] = sprite.color; + + // Material ID (0 = default) | 材质ID(0 = 默认) + this.materialIdBuffer[i] = sprite.materialId ?? 0; } // Submit to engine (single WASM call) | 提交到引擎(单次WASM调用) @@ -242,7 +247,8 @@ export class EngineBridge implements IEngineBridge { this.transformBuffer.subarray(0, count * 7), this.textureIdBuffer.subarray(0, count), this.uvBuffer.subarray(0, count * 4), - this.colorBuffer.subarray(0, count) + this.colorBuffer.subarray(0, count), + this.materialIdBuffer.subarray(0, count) ); this.stats.spriteCount = count; diff --git a/packages/ecs-engine-bindgen/src/core/RenderBatcher.ts b/packages/ecs-engine-bindgen/src/core/RenderBatcher.ts index 68d9bff0..7064f1d5 100644 --- a/packages/ecs-engine-bindgen/src/core/RenderBatcher.ts +++ b/packages/ecs-engine-bindgen/src/core/RenderBatcher.ts @@ -74,10 +74,14 @@ export class RenderBatcher { * @returns Sorted array of sprites | 排序后的精灵数组 */ getSprites(): SpriteRenderData[] { - // Sort by texture ID for better batching (fewer texture switches) - // 按纹理ID排序以获得更好的批处理效果(减少纹理切换) + // Sort by material ID first, then texture ID for better batching + // 先按材质ID排序,再按纹理ID排序以获得更好的批处理效果 if (!this.sortByZ) { - this.sprites.sort((a, b) => a.textureId - b.textureId); + this.sprites.sort((a, b) => { + const materialDiff = (a.materialId || 0) - (b.materialId || 0); + if (materialDiff !== 0) return materialDiff; + return a.textureId - b.textureId; + }); } return this.sprites; } diff --git a/packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts b/packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts index d00b6ac9..47e76061 100644 --- a/packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts +++ b/packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts @@ -7,6 +7,7 @@ import { Entity, Component } from '@esengine/ecs-framework'; import type { EngineBridge } from './EngineBridge'; import { RenderBatcher } from './RenderBatcher'; import { SpriteComponent } from '@esengine/sprite'; +import { getMaterialManager } from '@esengine/material-system'; import type { SpriteRenderData } from '../types'; /** @@ -108,6 +109,14 @@ export class SpriteRenderHelper { // Convert hex color string to packed RGBA const color = this.hexToPackedColor(sprite.color, sprite.alpha); + // Get material ID from path (0 = default if not found or no path specified) + const materialId = sprite.material + ? getMaterialManager().getMaterialIdByPath(sprite.material) + : 0; + + // Collect material overrides if any + const hasOverrides = sprite.hasOverrides(); + const renderData: SpriteRenderData = { x: transform.position.x, y: transform.position.y, @@ -118,7 +127,10 @@ export class SpriteRenderHelper { originY: sprite.originY, textureId: sprite.textureId, uv, - color + color, + materialId, + // Only include overrides if there are any + ...(hasOverrides ? { materialOverrides: sprite.materialOverrides } : {}) }; this.batcher.addSprite(renderData); diff --git a/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts b/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts index fef29376..441eb024 100644 --- a/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts +++ b/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts @@ -5,8 +5,10 @@ import { EntitySystem, Matcher, Entity, ComponentType, ECSSystem, Component, Core } from '@esengine/ecs-framework'; import { TransformComponent } from '@esengine/engine-core'; +import { Color } from '@esengine/ecs-framework-math'; import { SpriteComponent } from '@esengine/sprite'; import { CameraComponent } from '@esengine/camera'; +import { getMaterialManager } from '@esengine/material-system'; import type { EngineBridge } from '../core/EngineBridge'; import { RenderBatcher } from '../core/RenderBatcher'; import type { SpriteRenderData } from '../types'; @@ -279,7 +281,7 @@ export class EngineRenderSystem extends EntitySystem { : (typeof transform.rotation === 'number' ? transform.rotation : transform.rotation.z); // Convert hex color string to packed RGBA | 将十六进制颜色字符串转换为打包的RGBA - const color = this.hexToPackedColor(sprite.color, sprite.alpha); + const color = Color.packHexAlpha(sprite.color, sprite.alpha); // Get texture ID from sprite component // 从精灵组件获取纹理ID @@ -290,6 +292,16 @@ export class EngineRenderSystem extends EntitySystem { textureId = this.bridge.getOrLoadTextureByPath(sprite.texture); } + // Get material ID from path (0 = default if not found or no path specified) + // 从路径获取材质 ID(0 = 默认,如果未找到或未指定路径) + const materialId = sprite.material + ? getMaterialManager().getMaterialIdByPath(sprite.material) + : 0; + + // Collect material overrides if any + // 收集材质覆盖(如果有) + const hasOverrides = sprite.hasOverrides(); + // Pass actual display dimensions (sprite size * world transform scale) // 传递实际显示尺寸(sprite尺寸 * 世界变换缩放) const renderData: SpriteRenderData = { @@ -302,7 +314,11 @@ export class EngineRenderSystem extends EntitySystem { originY: sprite.anchorY, textureId, uv, - color + color, + materialId, + // Only include overrides if there are any + // 仅在有覆盖时包含 + ...(hasOverrides ? { materialOverrides: sprite.materialOverrides } : {}) }; renderItems.push({ sortingOrder: sprite.sortingOrder, sprites: [renderData] }); @@ -1054,32 +1070,6 @@ export class EngineRenderSystem extends EntitySystem { return this.transformMode; } - /** - * Convert hex color string to packed RGBA. - * 将十六进制颜色字符串转换为打包的RGBA。 - */ - private hexToPackedColor(hex: string, alpha: number): number { - let r = 255, g = 255, b = 255; - - if (typeof hex === 'string' && hex.startsWith('#')) { - const hexValue = hex.slice(1); - if (hexValue.length === 3) { - r = parseInt(hexValue[0] + hexValue[0], 16); - g = parseInt(hexValue[1] + hexValue[1], 16); - b = parseInt(hexValue[2] + hexValue[2], 16); - } else if (hexValue.length === 6) { - r = parseInt(hexValue.slice(0, 2), 16); - g = parseInt(hexValue.slice(2, 4), 16); - b = parseInt(hexValue.slice(4, 6), 16); - } - } - - const a = Math.round(alpha * 255); - // Pack as 0xAABBGGRR for WebGL - return ((a & 0xFF) << 24) | ((b & 0xFF) << 16) | ((g & 0xFF) << 8) | (r & 0xFF); - } - - /** * Register a render data provider. * 注册渲染数据提供者。 diff --git a/packages/ecs-engine-bindgen/src/types/index.ts b/packages/ecs-engine-bindgen/src/types/index.ts index 38669cba..53a01c28 100644 --- a/packages/ecs-engine-bindgen/src/types/index.ts +++ b/packages/ecs-engine-bindgen/src/types/index.ts @@ -3,6 +3,23 @@ * 引擎桥接层的类型定义。 */ +/** + * Material property override for rendering. + * 用于渲染的材质属性覆盖。 + */ +export interface MaterialPropertyOverride { + /** Uniform type. | Uniform 类型。 */ + type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int'; + /** Uniform value. | Uniform 值。 */ + value: number | number[]; +} + +/** + * Material overrides map. + * 材质覆盖映射。 + */ +export type MaterialOverrides = Record; + /** * Sprite render data for batch submission. * 用于批量提交的精灵渲染数据。 @@ -28,6 +45,13 @@ export interface SpriteRenderData { uv: [number, number, number, number]; /** Packed RGBA color. | 打包的RGBA颜色。 */ color: number; + /** Material ID (0 = default material). | 材质ID(0 = 默认材质)。 */ + materialId?: number; + /** + * Material property overrides (instance level). + * 材质属性覆盖(实例级别)。 + */ + materialOverrides?: MaterialOverrides; } /** diff --git a/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts b/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts index f16ea536..3dcb7d9e 100644 --- a/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts +++ b/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts @@ -23,6 +23,11 @@ export class GameEngine { * Array of [x, y, zoom, rotation] | 数组 [x, y, zoom, rotation] */ getCamera(): Float32Array; + /** + * Check if a shader exists. + * 检查着色器是否存在。 + */ + hasShader(shader_id: number): boolean; /** * Set camera position, zoom, and rotation. * 设置相机位置、缩放和旋转。 @@ -42,6 +47,11 @@ export class GameEngine { * * `key_code` - The key code to check | 要检查的键码 */ isKeyDown(key_code: string): boolean; + /** + * Check if a material exists. + * 检查材质是否存在。 + */ + hasMaterial(material_id: number): boolean; /** * Load a texture from URL. * 从URL加载纹理。 @@ -64,6 +74,11 @@ export class GameEngine { * 适用于微信小游戏等环境。 */ static fromExternal(gl_context: any, width: number, height: number): GameEngine; + /** + * Remove a shader. + * 移除着色器。 + */ + removeShader(shader_id: number): boolean; /** * Set grid visibility. * 设置网格可见性。 @@ -90,6 +105,18 @@ export class GameEngine { * * `show_handles` - Whether to show transform handles | 是否显示变换手柄 */ addGizmoRect(x: number, y: number, width: number, height: number, rotation: number, origin_x: number, origin_y: number, r: number, g: number, b: number, a: number, show_handles: boolean): void; + /** + * Compile and register a custom shader. + * 编译并注册自定义着色器。 + * + * # Arguments | 参数 + * * `vertex_source` - Vertex shader GLSL source | 顶点着色器GLSL源代码 + * * `fragment_source` - Fragment shader GLSL source | 片段着色器GLSL源代码 + * + * # Returns | 返回 + * The shader ID for referencing this shader | 用于引用此着色器的ID + */ + compileShader(vertex_source: string, fragment_source: string): number; /** * Render sprites as overlay (without clearing screen). * 渲染精灵作为叠加层(不清除屏幕)。 @@ -98,6 +125,24 @@ export class GameEngine { * 用于在世界内容上渲染 UI。 */ renderOverlay(): void; + /** + * Create and register a new material. + * 创建并注册新材质。 + * + * # Arguments | 参数 + * * `name` - Material name for debugging | 材质名称(用于调试) + * * `shader_id` - Shader ID to use | 使用的着色器ID + * * `blend_mode` - Blend mode: 0=None, 1=Alpha, 2=Additive, 3=Multiply, 4=Screen, 5=PremultipliedAlpha + * + * # Returns | 返回 + * The material ID for referencing this material | 用于引用此材质的ID + */ + createMaterial(name: string, shader_id: number, blend_mode: number): number; + /** + * Remove a material. + * 移除材质。 + */ + removeMaterial(material_id: number): boolean; /** * Resize a specific viewport. * 调整特定视口大小。 @@ -140,11 +185,36 @@ export class GameEngine { * * `canvas_id` - HTML canvas element ID | HTML canvas元素ID */ registerViewport(id: string, canvas_id: string): void; + /** + * Set a material's vec2 uniform. + * 设置材质的vec2 uniform。 + */ + setMaterialVec2(material_id: number, name: string, x: number, y: number): boolean; + /** + * Set a material's vec3 uniform. + * 设置材质的vec3 uniform。 + */ + setMaterialVec3(material_id: number, name: string, x: number, y: number, z: number): boolean; + /** + * Set a material's vec4 uniform (also used for colors). + * 设置材质的vec4 uniform(也用于颜色)。 + */ + setMaterialVec4(material_id: number, name: string, x: number, y: number, z: number, w: number): boolean; /** * Render to a specific viewport. * 渲染到特定视口。 */ renderToViewport(viewport_id: string): void; + /** + * Set a material's color uniform (RGBA, 0.0-1.0). + * 设置材质的颜色uniform(RGBA,0.0-1.0)。 + */ + setMaterialColor(material_id: number, name: string, r: number, g: number, b: number, a: number): boolean; + /** + * Set a material's float uniform. + * 设置材质的浮点uniform。 + */ + setMaterialFloat(material_id: number, name: string, value: number): boolean; /** * Set transform tool mode. * 设置变换工具模式。 @@ -191,8 +261,9 @@ export class GameEngine { * * `texture_ids` - Uint32Array of texture IDs | 纹理ID数组 * * `uvs` - Float32Array [u0, v0, u1, v1] per sprite | 每个精灵的UV坐标 * * `colors` - Uint32Array of packed RGBA colors | 打包的RGBA颜色数组 + * * `material_ids` - Uint32Array of material IDs (0 = default) | 材质ID数组(0 = 默认) */ - submitSpriteBatch(transforms: Float32Array, texture_ids: Uint32Array, uvs: Float32Array, colors: Uint32Array): void; + submitSpriteBatch(transforms: Float32Array, texture_ids: Uint32Array, uvs: Float32Array, colors: Uint32Array, material_ids: Uint32Array): void; /** * Unregister a viewport. * 注销视口。 @@ -206,6 +277,11 @@ export class GameEngine { * * `path` - Image path/URL to load | 要加载的图片路径/URL */ loadTextureByPath(path: string): number; + /** + * Compile a shader with a specific ID. + * 使用特定ID编译着色器。 + */ + compileShaderWithId(shader_id: number, vertex_source: string, fragment_source: string): void; /** * Get texture ID by path. * 按路径获取纹理ID。 @@ -214,6 +290,19 @@ export class GameEngine { * * `path` - Image path to lookup | 要查找的图片路径 */ getTextureIdByPath(path: string): number | undefined; + /** + * Create a material with a specific ID. + * 使用特定ID创建材质。 + */ + createMaterialWithId(material_id: number, name: string, shader_id: number, blend_mode: number): void; + /** + * Set a material's blend mode. + * 设置材质的混合模式。 + * + * # Arguments | 参数 + * * `blend_mode` - 0=None, 1=Alpha, 2=Additive, 3=Multiply, 4=Screen, 5=PremultipliedAlpha + */ + setMaterialBlendMode(material_id: number, blend_mode: number): boolean; /** * Create a new game engine instance. * 创建新的游戏引擎实例。 @@ -272,18 +361,26 @@ export interface InitOutput { readonly gameengine_addGizmoLine: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void; readonly gameengine_addGizmoRect: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number) => void; readonly gameengine_clear: (a: number, b: number, c: number, d: number, e: number) => void; + readonly gameengine_compileShader: (a: number, b: number, c: number, d: number, e: number) => [number, number, number]; + readonly gameengine_compileShaderWithId: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number]; + readonly gameengine_createMaterial: (a: number, b: number, c: number, d: number, e: number) => number; + readonly gameengine_createMaterialWithId: (a: number, b: number, c: number, d: number, e: number, f: number) => void; readonly gameengine_fromExternal: (a: any, b: number, c: number) => [number, number, number]; readonly gameengine_getCamera: (a: number) => [number, number]; readonly gameengine_getOrLoadTextureByPath: (a: number, b: number, c: number) => [number, number, number]; readonly gameengine_getTextureIdByPath: (a: number, b: number, c: number) => number; readonly gameengine_getViewportCamera: (a: number, b: number, c: number) => [number, number]; readonly gameengine_getViewportIds: (a: number) => [number, number]; + readonly gameengine_hasMaterial: (a: number, b: number) => number; + readonly gameengine_hasShader: (a: number, b: number) => number; readonly gameengine_height: (a: number) => number; readonly gameengine_isKeyDown: (a: number, b: number, c: number) => number; readonly gameengine_loadTexture: (a: number, b: number, c: number, d: number) => [number, number]; readonly gameengine_loadTextureByPath: (a: number, b: number, c: number) => [number, number, number]; readonly gameengine_new: (a: number, b: number) => [number, number, number]; readonly gameengine_registerViewport: (a: number, b: number, c: number, d: number, e: number) => [number, number]; + readonly gameengine_removeMaterial: (a: number, b: number) => number; + readonly gameengine_removeShader: (a: number, b: number) => number; readonly gameengine_render: (a: number) => [number, number]; readonly gameengine_renderOverlay: (a: number) => [number, number]; readonly gameengine_renderToViewport: (a: number, b: number, c: number) => [number, number]; @@ -292,12 +389,18 @@ export interface InitOutput { readonly gameengine_setActiveViewport: (a: number, b: number, c: number) => number; readonly gameengine_setCamera: (a: number, b: number, c: number, d: number, e: number) => void; readonly gameengine_setClearColor: (a: number, b: number, c: number, d: number, e: number) => void; + readonly gameengine_setMaterialBlendMode: (a: number, b: number, c: number) => number; + readonly gameengine_setMaterialColor: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number; + readonly gameengine_setMaterialFloat: (a: number, b: number, c: number, d: number, e: number) => number; + readonly gameengine_setMaterialVec2: (a: number, b: number, c: number, d: number, e: number, f: number) => number; + readonly gameengine_setMaterialVec3: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => number; + readonly gameengine_setMaterialVec4: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number; readonly gameengine_setShowGizmos: (a: number, b: number) => void; readonly gameengine_setShowGrid: (a: number, b: number) => void; readonly gameengine_setTransformMode: (a: number, b: number) => void; readonly gameengine_setViewportCamera: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; readonly gameengine_setViewportConfig: (a: number, b: number, c: number, d: number, e: number) => void; - readonly gameengine_submitSpriteBatch: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number]; + readonly gameengine_submitSpriteBatch: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number]; readonly gameengine_unregisterViewport: (a: number, b: number, c: number) => void; readonly gameengine_updateInput: (a: number) => void; readonly gameengine_width: (a: number) => number; diff --git a/packages/editor-runtime/src/index.ts b/packages/editor-runtime/src/index.ts index 5b0a49ee..1593b1f5 100644 --- a/packages/editor-runtime/src/index.ts +++ b/packages/editor-runtime/src/index.ts @@ -146,11 +146,7 @@ export type { FileActionHandler, RegisteredPlugin, PluginConfig, - PluginCategory, LoadingPhase, - ModuleType, - ModuleDescriptor, - PluginDependency, PluginState, // Service interfaces @@ -203,7 +199,12 @@ export type { IPluginLoader, IRuntimeModuleLoader, IEditorModuleLoader, - PluginDescriptor, + ModuleManifest, + ModuleCategory, + ModulePlatform, + ModuleExports, + IPlugin, + IRuntimeModule, SystemContext, ComponentInspectorProviderDef, } from '@esengine/editor-core'; diff --git a/packages/engine-core/module.json b/packages/engine-core/module.json new file mode 100644 index 00000000..bcfba762 --- /dev/null +++ b/packages/engine-core/module.json @@ -0,0 +1,38 @@ +{ + "id": "engine-core", + "name": "@esengine/engine-core", + "displayName": "Engine Core", + "description": "Engine lifecycle, scene management, game loop | 引擎生命周期、场景管理、游戏循环", + "version": "1.0.0", + "category": "Core", + "icon": "Cpu", + "tags": [ + "engine", + "scene", + "gameloop" + ], + "isCore": true, + "defaultEnabled": true, + "isEngineModule": true, + "canContainContent": false, + "platforms": [ + "web", + "desktop", + "mobile" + ], + "dependencies": [ + "core", + "math" + ], + "exports": { + "other": [ + "Engine", + "Scene", + "SceneManager", + "GameLoop", + "Time" + ] + }, + "requiresWasm": false, + "outputPath": "dist/index.js" +} diff --git a/packages/engine-core/src/EnginePlugin.ts b/packages/engine-core/src/EnginePlugin.ts index 72aee990..36a8f61e 100644 --- a/packages/engine-core/src/EnginePlugin.ts +++ b/packages/engine-core/src/EnginePlugin.ts @@ -8,27 +8,7 @@ import type { ComponentRegistry as ComponentRegistryType, IScene, ServiceContainer } from '@esengine/ecs-framework'; import { TransformComponent } from './TransformComponent'; - -// ============================================================================ -// 基础类型 | Basic Types -// ============================================================================ - -/** - * 插件类别 - * Plugin category - */ -export type PluginCategory = - | 'core' // 核心功能 | Core functionality - | 'rendering' // 渲染相关 | Rendering - | 'ui' // UI 系统 | UI System - | 'ai' // AI/行为树 | AI/Behavior - | 'physics' // 物理引擎 | Physics - | 'audio' // 音频系统 | Audio - | 'networking' // 网络功能 | Networking - | 'tools' // 工具/编辑器扩展 | Tools/Editor extensions - | 'scripting' // 脚本/蓝图 | Scripting/Blueprint - | 'tilemap' // 瓦片地图 | Tilemap - | 'content'; // 内容/资源 | Content/Assets +import type { ModuleManifest } from './ModuleManifest'; /** * 加载阶段 - 控制插件模块的加载顺序 @@ -41,80 +21,6 @@ export type LoadingPhase = | 'postDefault' // 默认之后 | After default | 'postEngine'; // 引擎初始化后 | After engine init -/** - * 模块类型 - * Module type - */ -export type ModuleType = 'runtime' | 'editor'; - -/** - * 模块描述符 - * Module descriptor - */ -export interface ModuleDescriptor { - /** 模块名称 | Module name */ - name: string; - /** 模块类型 | Module type */ - type: ModuleType; - /** 加载阶段 | Loading phase */ - loadingPhase?: LoadingPhase; -} - -/** - * 插件依赖 - * Plugin dependency - */ -export interface PluginDependency { - /** 依赖的插件ID | Dependent plugin ID */ - id: string; - /** 版本要求 | Version requirement */ - version?: string; - /** 是否可选 | Optional */ - optional?: boolean; -} - -// ============================================================================ -// 插件描述符 | Plugin Descriptor -// ============================================================================ - -/** - * 插件描述符 - * Plugin descriptor - * - * 所有字段都是可选的,PluginManager 会填充默认值。 - * All fields are optional, PluginManager will fill in defaults. - */ -export interface PluginDescriptor { - /** 插件唯一标识符 | Unique plugin ID */ - id: string; - /** 显示名称 | Display name */ - name: string; - /** 版本号 | Version */ - version: string; - /** 描述 | Description */ - description?: string; - /** 插件类别 | Plugin category */ - category?: PluginCategory; - /** 标签(用于搜索) | Tags (for search) */ - tags?: string[]; - /** 图标(Lucide 图标名) | Icon (Lucide icon name) */ - icon?: string; - /** 是否默认启用 | Enabled by default */ - enabledByDefault?: boolean; - /** 是否可以包含内容资产 | Can contain content assets */ - canContainContent?: boolean; - /** 是否为引擎内置插件 | Is engine built-in plugin */ - isEnginePlugin?: boolean; - /** 是否为核心插件(不可禁用) | Is core plugin (cannot be disabled) */ - isCore?: boolean; - /** 模块列表 | Module list */ - modules?: ModuleDescriptor[]; - /** 依赖列表 | Dependency list */ - dependencies?: PluginDependency[]; - /** 平台要求 | Platform requirements */ - platforms?: ('web' | 'desktop' | 'mobile')[]; -} - // ============================================================================ // 系统上下文 | System Context // ============================================================================ @@ -192,8 +98,8 @@ export interface IRuntimeModule { * This is the unified type that all plugin packages export. */ export interface IPlugin { - /** 插件描述符 | Plugin descriptor */ - readonly descriptor: PluginDescriptor; + /** 模块清单 | Module manifest */ + readonly manifest: ModuleManifest; /** 运行时模块(可选) | Runtime module (optional) */ readonly runtimeModule?: IRuntimeModule; /** 编辑器模块(可选,类型为 any 以避免循环依赖) | Editor module (optional, typed as any to avoid circular deps) */ @@ -210,18 +116,25 @@ class EngineRuntimeModule implements IRuntimeModule { } } -const descriptor: PluginDescriptor = { - id: '@esengine/engine-core', - name: 'Engine Core', - version: '1.0.0', +const manifest: ModuleManifest = { + id: 'engine-core', + name: '@esengine/engine-core', + displayName: 'Engine Core', description: 'Transform 等核心组件', - category: 'core', - enabledByDefault: true, - isEnginePlugin: true, - isCore: true + version: '1.0.0', + category: 'Core', + icon: 'Box', + isCore: true, + defaultEnabled: true, + isEngineModule: true, + dependencies: ['core', 'math'], + exports: { + components: ['TransformComponent', 'HierarchyComponent'], + systems: ['TransformSystem', 'HierarchySystem'] + } }; export const EnginePlugin: IPlugin = { - descriptor, + manifest, runtimeModule: new EngineRuntimeModule() }; diff --git a/packages/engine-core/src/ModuleManifest.ts b/packages/engine-core/src/ModuleManifest.ts new file mode 100644 index 00000000..df0abb71 --- /dev/null +++ b/packages/engine-core/src/ModuleManifest.ts @@ -0,0 +1,196 @@ +/** + * Module Manifest Types. + * 模块清单类型定义。 + * + * This is the single source of truth for module/plugin configuration. + * Each engine module should have a module.json file containing this information. + * 这是模块/插件配置的唯一数据源。 + * 每个引擎模块应该有一个包含此信息的 module.json 文件。 + */ + +/** + * Module category for organization in UI. + * 用于 UI 组织的模块分类。 + */ +export type ModuleCategory = + | 'Core' + | 'Rendering' + | 'Physics' + | 'AI' + | 'Audio' + | 'Networking' + | 'Other'; + +/** + * Platform requirements for the module. + * 模块的平台要求。 + */ +export type ModulePlatform = 'web' | 'desktop' | 'mobile'; + +/** + * Module exports definition. + * 模块导出定义。 + */ +export interface ModuleExports { + /** Exported component classes | 导出的组件类 */ + components?: string[]; + + /** Exported system classes | 导出的系统类 */ + systems?: string[]; + + /** Exported asset loaders | 导出的资源加载器 */ + loaders?: string[]; + + /** Other exported items | 其他导出项 */ + other?: string[]; +} + +/** + * Module manifest definition (Unified Plugin/Module config). + * 模块清单定义(统一的插件/模块配置)。 + * + * This interface matches the structure of module.json files. + * 此接口匹配 module.json 文件的结构。 + */ +export interface ModuleManifest { + // ==================== Core Identifiers ==================== + + /** Unique module identifier | 唯一模块标识符 */ + id: string; + + /** Package name (npm style) | 包名(npm 风格) */ + name: string; + + /** Display name for UI | UI 显示名称 */ + displayName: string; + + /** Module description | 模块描述 */ + description: string; + + /** Module version | 模块版本 */ + version: string; + + // ==================== Classification ==================== + + /** Category for grouping in UI | UI 分组分类 */ + category: ModuleCategory; + + /** Tags for search and filtering | 用于搜索和过滤的标签 */ + tags?: string[]; + + /** Icon name (Lucide icon) | 图标名(Lucide 图标) */ + icon?: string; + + // ==================== Lifecycle ==================== + + /** Whether this is a core module (cannot be disabled) | 是否为核心模块(不能禁用) */ + isCore: boolean; + + /** Whether enabled by default in new projects | 新项目中是否默认启用 */ + defaultEnabled: boolean; + + /** Whether this is an engine built-in module | 是否为引擎内置模块 */ + isEngineModule?: boolean; + + // ==================== Content ==================== + + /** Whether this module can contain content/assets | 是否可以包含内容/资源 */ + canContainContent?: boolean; + + /** Platform requirements | 平台要求 */ + platforms?: ModulePlatform[]; + + // ==================== Dependencies ==================== + + /** Module IDs this module depends on | 此模块依赖的模块 ID */ + dependencies: string[]; + + /** + * External package dependencies that need to be included in import map. + * 需要包含在 import map 中的外部包依赖。 + * + * These are runtime dependencies that are dynamically imported by the module. + * 这些是模块动态导入的运行时依赖。 + * + * Example: ["@esengine/rapier2d"] + */ + externalDependencies?: string[]; + + // ==================== Exports ==================== + + /** Exported items for dependency checking | 导出项用于依赖检查 */ + exports: ModuleExports; + + // ==================== Editor Integration ==================== + + /** + * Associated editor package name. + * 关联的编辑器包名。 + * e.g., "@esengine/tilemap-editor" for "@esengine/tilemap" + */ + editorPackage?: string; + + // ==================== Performance (auto-calculated at build time) ==================== + // ==================== 性能(构建时自动计算) ==================== + + /** JS bundle size in bytes | JS 包大小(字节) */ + jsSize?: number; + + /** Whether this module requires WASM | 是否需要 WASM */ + requiresWasm?: boolean; + + /** WASM file size in bytes | WASM 文件大小(字节) */ + wasmSize?: number; + + /** + * WASM file paths relative to module's node_modules or package root. + * WASM 文件路径,相对于模块的 node_modules 或包根目录。 + * + * Can be glob patterns like "*.wasm" or specific paths. + * 可以是 glob 模式如 "*.wasm" 或具体路径。 + * + * Example: ["@dimforge/rapier2d-compat/*.wasm"] + */ + wasmPaths?: string[]; + + /** + * Runtime WASM path relative to game output root. + * 运行时 WASM 路径,相对于游戏输出根目录。 + * + * Build pipeline copies WASM to this location. + * 构建管线将 WASM 复制到此位置。 + * + * Example: "wasm/rapier_wasm2d_bg.wasm" + */ + runtimeWasmPath?: string; + + // ==================== Build Configuration ==================== + // ==================== 构建配置 ==================== + + /** + * Output path for the built module file. + * 构建后模块文件的输出路径。 + * + * Example: "dist/index.mjs" + */ + outputPath?: string; + + /** + * Plugin export name for dynamic loading. + * 用于动态加载的插件导出名。 + * + * Example: "SpritePlugin" + */ + pluginExport?: string; + + /** + * Additional files to include when copying module. + * 复制模块时需要包含的额外文件。 + * + * Glob patterns relative to the module's dist directory. + * 相对于模块 dist 目录的 glob 模式。 + * + * Example: ["chunk-*.js", "worker.js"] + */ + includes?: string[]; +} diff --git a/packages/engine-core/src/index.ts b/packages/engine-core/src/index.ts index 2ca10a7a..2aa36d55 100644 --- a/packages/engine-core/src/index.ts +++ b/packages/engine-core/src/index.ts @@ -5,13 +5,16 @@ export { HierarchySystem } from './HierarchySystem'; export { EnginePlugin, // 类型导出 - type PluginCategory, type LoadingPhase, - type ModuleType, - type ModuleDescriptor, - type PluginDependency, - type PluginDescriptor, type SystemContext, type IRuntimeModule, type IPlugin } from './EnginePlugin'; + +// Module Manifest types (unified module/plugin configuration) +export { + type ModuleManifest, + type ModuleCategory, + type ModulePlatform, + type ModuleExports +} from './ModuleManifest'; diff --git a/packages/math/module.json b/packages/math/module.json new file mode 100644 index 00000000..e602d478 --- /dev/null +++ b/packages/math/module.json @@ -0,0 +1,21 @@ +{ + "id": "math", + "name": "@esengine/ecs-framework-math", + "displayName": "Math", + "description": "Vector, Matrix, Transform and other math utilities | 向量、矩阵、变换等数学工具", + "version": "1.0.0", + "category": "Core", + "icon": "Calculator", + "tags": ["math", "vector", "matrix", "transform"], + "isCore": true, + "defaultEnabled": true, + "isEngineModule": true, + "canContainContent": false, + "platforms": ["web", "desktop", "mobile"], + "dependencies": [], + "exports": { + "other": ["Vector2", "Vector3", "Matrix3x3", "Matrix4x4", "Quaternion", "Transform", "Rectangle", "Color"] + }, + "requiresWasm": false, + "outputPath": "dist/index.mjs" +} diff --git a/packages/math/package.json b/packages/math/package.json index 6931191a..f756ac63 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -24,10 +24,10 @@ "clean": "rimraf bin dist tsconfig.tsbuildinfo", "build:ts": "tsc", "prebuild": "npm run clean", - "build": "npm run build:ts", + "build": "npm run build:ts && node build-rollup.cjs", "build:watch": "tsc --watch", "rebuild": "npm run clean && npm run build", - "build:npm": "npm run build && node build-rollup.cjs", + "build:npm": "npm run build", "publish:npm": "npm run build:npm && cd dist && npm publish", "test": "jest --config jest.config.cjs", "test:watch": "jest --watch --config jest.config.cjs", diff --git a/packages/math/src/Color.ts b/packages/math/src/Color.ts new file mode 100644 index 00000000..cee64908 --- /dev/null +++ b/packages/math/src/Color.ts @@ -0,0 +1,556 @@ +/** + * Color utility class for game engine + * 游戏引擎颜色工具类 + * + * Provides color conversion, manipulation, and packing utilities. + * 提供颜色转换、操作和打包工具。 + */ + +/** + * RGBA color components + * RGBA 颜色分量 + */ +export interface RGBA { + r: number; + g: number; + b: number; + a: number; +} + +/** + * HSL color components + * HSL 颜色分量 + */ +export interface HSL { + h: number; + s: number; + l: number; +} + +/** + * Color class for color manipulation and conversion + * 颜色类,用于颜色操作和转换 + */ +export class Color { + /** Red component (0-255) | 红色分量 (0-255) */ + public r: number; + /** Green component (0-255) | 绿色分量 (0-255) */ + public g: number; + /** Blue component (0-255) | 蓝色分量 (0-255) */ + public b: number; + /** Alpha component (0-1) | 透明度分量 (0-1) */ + public a: number; + + // ===== Predefined Colors | 预定义颜色 ===== + + /** White (0xFFFFFF) | 白色 */ + static readonly WHITE = new Color(255, 255, 255); + /** Black (0x000000) | 黑色 */ + static readonly BLACK = new Color(0, 0, 0); + /** Red (0xFF0000) | 红色 */ + static readonly RED = new Color(255, 0, 0); + /** Green (0x00FF00) | 绿色 */ + static readonly GREEN = new Color(0, 255, 0); + /** Blue (0x0000FF) | 蓝色 */ + static readonly BLUE = new Color(0, 0, 255); + /** Yellow (0xFFFF00) | 黄色 */ + static readonly YELLOW = new Color(255, 255, 0); + /** Cyan (0x00FFFF) | 青色 */ + static readonly CYAN = new Color(0, 255, 255); + /** Magenta (0xFF00FF) | 品红色 */ + static readonly MAGENTA = new Color(255, 0, 255); + /** Transparent (0x00000000) | 透明 */ + static readonly TRANSPARENT = new Color(0, 0, 0, 0); + /** Gray (0x808080) | 灰色 */ + static readonly GRAY = new Color(128, 128, 128); + + /** + * Create a new Color instance + * 创建新的 Color 实例 + */ + constructor(r: number = 255, g: number = 255, b: number = 255, a: number = 1) { + this.r = Math.round(Math.max(0, Math.min(255, r))); + this.g = Math.round(Math.max(0, Math.min(255, g))); + this.b = Math.round(Math.max(0, Math.min(255, b))); + this.a = Math.max(0, Math.min(1, a)); + } + + // ===== Factory Methods | 工厂方法 ===== + + /** + * Create color from hex string + * 从十六进制字符串创建颜色 + * @param hex Hex string (e.g., "#FF0000", "#F00", "FF0000") | 十六进制字符串 + * @param alpha Optional alpha value (0-1) | 可选的透明度值 + */ + static fromHex(hex: string, alpha: number = 1): Color { + const { r, g, b } = Color.hexToRgb(hex); + return new Color(r, g, b, alpha); + } + + /** + * Create color from packed uint32 (0xRRGGBB or 0xAARRGGBB) + * 从打包的 uint32 创建颜色 + * @param value Packed color value | 打包的颜色值 + * @param hasAlpha Whether value includes alpha | 是否包含透明度 + */ + static fromUint32(value: number, hasAlpha: boolean = false): Color { + if (hasAlpha) { + const a = ((value >> 24) & 0xFF) / 255; + const r = (value >> 16) & 0xFF; + const g = (value >> 8) & 0xFF; + const b = value & 0xFF; + return new Color(r, g, b, a); + } else { + const r = (value >> 16) & 0xFF; + const g = (value >> 8) & 0xFF; + const b = value & 0xFF; + return new Color(r, g, b); + } + } + + /** + * Create color from HSL values + * 从 HSL 值创建颜色 + * @param h Hue (0-360) | 色相 + * @param s Saturation (0-1) | 饱和度 + * @param l Lightness (0-1) | 亮度 + * @param a Alpha (0-1) | 透明度 + */ + static fromHSL(h: number, s: number, l: number, a: number = 1): Color { + const { r, g, b } = Color.hslToRgb(h, s, l); + return new Color(r, g, b, a); + } + + /** + * Create color from normalized float values (0-1) + * 从归一化浮点值创建颜色 (0-1) + */ + static fromFloat(r: number, g: number, b: number, a: number = 1): Color { + return new Color(r * 255, g * 255, b * 255, a); + } + + // ===== Conversion Methods | 转换方法 ===== + + /** + * Convert hex string to RGB + * 将十六进制字符串转换为 RGB + */ + static hexToRgb(hex: string): { r: number; g: number; b: number } { + const colorHex = hex.replace('#', ''); + + let r = 255, g = 255, b = 255; + + if (colorHex.length === 6) { + r = parseInt(colorHex.substring(0, 2), 16); + g = parseInt(colorHex.substring(2, 4), 16); + b = parseInt(colorHex.substring(4, 6), 16); + } else if (colorHex.length === 3) { + r = parseInt(colorHex[0] + colorHex[0], 16); + g = parseInt(colorHex[1] + colorHex[1], 16); + b = parseInt(colorHex[2] + colorHex[2], 16); + } else if (colorHex.length === 8) { + // AARRGGBB format + r = parseInt(colorHex.substring(2, 4), 16); + g = parseInt(colorHex.substring(4, 6), 16); + b = parseInt(colorHex.substring(6, 8), 16); + } + + return { r, g, b }; + } + + /** + * Convert RGB to hex string + * 将 RGB 转换为十六进制字符串 + */ + static rgbToHex(r: number, g: number, b: number): string { + const toHex = (n: number) => Math.round(n).toString(16).padStart(2, '0'); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + + /** + * Convert HSL to RGB + * 将 HSL 转换为 RGB + */ + static hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } { + h = ((h % 360) + 360) % 360 / 360; + s = Math.max(0, Math.min(1, s)); + l = Math.max(0, Math.min(1, l)); + + let r: number, g: number, b: number; + + if (s === 0) { + r = g = b = l; + } else { + const hue2rgb = (p: number, q: number, t: number) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; + } + + /** + * Convert RGB to HSL + * 将 RGB 转换为 HSL + */ + static rgbToHsl(r: number, g: number, b: number): HSL { + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h = 0; + let s = 0; + const l = (max + min) / 2; + + if (max !== min) { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: + h = ((g - b) / d + (g < b ? 6 : 0)) / 6; + break; + case g: + h = ((b - r) / d + 2) / 6; + break; + case b: + h = ((r - g) / d + 4) / 6; + break; + } + } + + return { h: h * 360, s, l }; + } + + // ===== Packing Methods | 打包方法 ===== + + /** + * Pack color to uint32 (0xRRGGBB) + * 打包颜色为 uint32 (0xRRGGBB) + */ + static packRGB(r: number, g: number, b: number): number { + return ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); + } + + /** + * Pack color to uint32 (0xAARRGGBB) + * 打包颜色为 uint32 (0xAARRGGBB) + */ + static packARGB(r: number, g: number, b: number, a: number): number { + const alpha = Math.round(a * 255); + return ((alpha & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); + } + + /** + * Pack color to uint32 for WebGL (0xAABBGGRR) + * 打包颜色为 WebGL 格式的 uint32 (0xAABBGGRR) + */ + static packABGR(r: number, g: number, b: number, a: number): number { + const alpha = Math.round(a * 255); + return ((alpha & 0xFF) << 24) | ((b & 0xFF) << 16) | ((g & 0xFF) << 8) | (r & 0xFF); + } + + /** + * Pack hex string and alpha to WebGL uint32 (0xAABBGGRR) + * 将十六进制字符串和透明度打包为 WebGL 格式 uint32 + */ + static packHexAlpha(hex: string, alpha: number): number { + const { r, g, b } = Color.hexToRgb(hex); + return Color.packABGR(r, g, b, alpha); + } + + /** + * Unpack uint32 to RGBA (assumes 0xAARRGGBB) + * 解包 uint32 为 RGBA (假设格式为 0xAARRGGBB) + */ + static unpackARGB(value: number): RGBA { + return { + a: ((value >> 24) & 0xFF) / 255, + r: (value >> 16) & 0xFF, + g: (value >> 8) & 0xFF, + b: value & 0xFF + }; + } + + /** + * Unpack uint32 to RGBA (assumes 0xAABBGGRR - WebGL format) + * 解包 uint32 为 RGBA (假设格式为 0xAABBGGRR - WebGL 格式) + */ + static unpackABGR(value: number): RGBA { + return { + a: ((value >> 24) & 0xFF) / 255, + b: (value >> 16) & 0xFF, + g: (value >> 8) & 0xFF, + r: value & 0xFF + }; + } + + // ===== Color Operations | 颜色操作 ===== + + /** + * Interpolate between two colors + * 在两个颜色之间插值 + */ + static lerp(from: Color, to: Color, t: number): Color { + t = Math.max(0, Math.min(1, t)); + return new Color( + from.r + (to.r - from.r) * t, + from.g + (to.g - from.g) * t, + from.b + (to.b - from.b) * t, + from.a + (to.a - from.a) * t + ); + } + + /** + * Interpolate between two packed uint32 colors (0xRRGGBB) + * 在两个打包的 uint32 颜色之间插值 + */ + static lerpUint32(from: number, to: number, t: number): number { + t = Math.max(0, Math.min(1, t)); + const fromR = (from >> 16) & 0xFF; + const fromG = (from >> 8) & 0xFF; + const fromB = from & 0xFF; + const toR = (to >> 16) & 0xFF; + const toG = (to >> 8) & 0xFF; + const toB = to & 0xFF; + + const r = Math.round(fromR + (toR - fromR) * t); + const g = Math.round(fromG + (toG - fromG) * t); + const b = Math.round(fromB + (toB - fromB) * t); + + return (r << 16) | (g << 8) | b; + } + + /** + * Mix two colors + * 混合两个颜色 + */ + static mix(color1: Color, color2: Color, ratio: number = 0.5): Color { + return Color.lerp(color1, color2, ratio); + } + + /** + * Lighten a color + * 使颜色变亮 + */ + static lighten(color: Color, amount: number): Color { + const hsl = Color.rgbToHsl(color.r, color.g, color.b); + hsl.l = Math.min(1, hsl.l + amount); + const rgb = Color.hslToRgb(hsl.h, hsl.s, hsl.l); + return new Color(rgb.r, rgb.g, rgb.b, color.a); + } + + /** + * Darken a color + * 使颜色变暗 + */ + static darken(color: Color, amount: number): Color { + const hsl = Color.rgbToHsl(color.r, color.g, color.b); + hsl.l = Math.max(0, hsl.l - amount); + const rgb = Color.hslToRgb(hsl.h, hsl.s, hsl.l); + return new Color(rgb.r, rgb.g, rgb.b, color.a); + } + + /** + * Saturate a color + * 增加颜色饱和度 + */ + static saturate(color: Color, amount: number): Color { + const hsl = Color.rgbToHsl(color.r, color.g, color.b); + hsl.s = Math.min(1, hsl.s + amount); + const rgb = Color.hslToRgb(hsl.h, hsl.s, hsl.l); + return new Color(rgb.r, rgb.g, rgb.b, color.a); + } + + /** + * Desaturate a color + * 降低颜色饱和度 + */ + static desaturate(color: Color, amount: number): Color { + const hsl = Color.rgbToHsl(color.r, color.g, color.b); + hsl.s = Math.max(0, hsl.s - amount); + const rgb = Color.hslToRgb(hsl.h, hsl.s, hsl.l); + return new Color(rgb.r, rgb.g, rgb.b, color.a); + } + + /** + * Invert a color + * 反转颜色 + */ + static invert(color: Color): Color { + return new Color(255 - color.r, 255 - color.g, 255 - color.b, color.a); + } + + /** + * Convert color to grayscale + * 将颜色转换为灰度 + */ + static grayscale(color: Color): Color { + const gray = Math.round(0.299 * color.r + 0.587 * color.g + 0.114 * color.b); + return new Color(gray, gray, gray, color.a); + } + + /** + * Get color luminance (perceived brightness) + * 获取颜色亮度(感知亮度) + */ + static luminance(color: Color): number { + return (0.299 * color.r + 0.587 * color.g + 0.114 * color.b) / 255; + } + + /** + * Get contrast ratio between two colors + * 获取两个颜色之间的对比度 + */ + static contrastRatio(color1: Color, color2: Color): number { + const l1 = Color.luminance(color1); + const l2 = Color.luminance(color2); + const lighter = Math.max(l1, l2); + const darker = Math.min(l1, l2); + return (lighter + 0.05) / (darker + 0.05); + } + + // ===== Instance Methods | 实例方法 ===== + + /** + * Convert to hex string + * 转换为十六进制字符串 + */ + toHex(): string { + return Color.rgbToHex(this.r, this.g, this.b); + } + + /** + * Convert to hex string with alpha + * 转换为带透明度的十六进制字符串 + */ + toHexAlpha(): string { + const alphaHex = Math.round(this.a * 255).toString(16).padStart(2, '0'); + return `#${alphaHex}${this.toHex().slice(1)}`; + } + + /** + * Convert to CSS rgba string + * 转换为 CSS rgba 字符串 + */ + toRgba(): string { + return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`; + } + + /** + * Convert to CSS rgb string + * 转换为 CSS rgb 字符串 + */ + toRgb(): string { + return `rgb(${this.r}, ${this.g}, ${this.b})`; + } + + /** + * Convert to HSL + * 转换为 HSL + */ + toHSL(): HSL { + return Color.rgbToHsl(this.r, this.g, this.b); + } + + /** + * Pack to uint32 (0xRRGGBB) + * 打包为 uint32 (0xRRGGBB) + */ + toUint32(): number { + return Color.packRGB(this.r, this.g, this.b); + } + + /** + * Pack to uint32 with alpha (0xAARRGGBB) + * 打包为带透明度的 uint32 (0xAARRGGBB) + */ + toUint32Alpha(): number { + return Color.packARGB(this.r, this.g, this.b, this.a); + } + + /** + * Pack to WebGL uint32 (0xAABBGGRR) + * 打包为 WebGL 格式 uint32 (0xAABBGGRR) + */ + toWebGL(): number { + return Color.packABGR(this.r, this.g, this.b, this.a); + } + + /** + * Get normalized float array [r, g, b, a] (0-1) + * 获取归一化浮点数组 [r, g, b, a] (0-1) + */ + toFloatArray(): [number, number, number, number] { + return [this.r / 255, this.g / 255, this.b / 255, this.a]; + } + + /** + * Clone this color + * 克隆此颜色 + */ + clone(): Color { + return new Color(this.r, this.g, this.b, this.a); + } + + /** + * Set color values + * 设置颜色值 + */ + set(r: number, g: number, b: number, a?: number): this { + this.r = Math.round(Math.max(0, Math.min(255, r))); + this.g = Math.round(Math.max(0, Math.min(255, g))); + this.b = Math.round(Math.max(0, Math.min(255, b))); + if (a !== undefined) { + this.a = Math.max(0, Math.min(1, a)); + } + return this; + } + + /** + * Copy from another color + * 从另一个颜色复制 + */ + copy(other: Color): this { + this.r = other.r; + this.g = other.g; + this.b = other.b; + this.a = other.a; + return this; + } + + /** + * Check equality with another color + * 检查与另一个颜色是否相等 + */ + equals(other: Color): boolean { + return this.r === other.r && this.g === other.g && this.b === other.b && this.a === other.a; + } + + /** + * String representation + * 字符串表示 + */ + toString(): string { + return `Color(${this.r}, ${this.g}, ${this.b}, ${this.a})`; + } +} diff --git a/packages/math/src/index.ts b/packages/math/src/index.ts index 367c1bbe..4cc89248 100644 --- a/packages/math/src/index.ts +++ b/packages/math/src/index.ts @@ -6,6 +6,7 @@ * - 碰撞检测算法 * - 动画插值和缓动函数 * - 数学工具函数 + * - 颜色工具类 */ // 核心数学类 @@ -18,6 +19,9 @@ export { Circle } from './Circle'; // 数学工具 export { MathUtils } from './MathUtils'; +// 颜色工具 +export { Color, type RGBA, type HSL } from './Color'; + // 碰撞检测 export * from './Collision'; diff --git a/packages/runtime-core/module.json b/packages/runtime-core/module.json new file mode 100644 index 00000000..04462658 --- /dev/null +++ b/packages/runtime-core/module.json @@ -0,0 +1,19 @@ +{ + "id": "runtime-core", + "name": "@esengine/runtime-core", + "displayName": "Runtime Core", + "description": "Core runtime framework | 核心运行时框架", + "version": "1.0.0", + "category": "Core", + "icon": "Play", + "tags": ["runtime", "core", "framework"], + "isCore": true, + "defaultEnabled": true, + "isEngineModule": true, + "canContainContent": false, + "platforms": ["web", "desktop", "mobile"], + "dependencies": ["core", "asset-system"], + "exports": {}, + "requiresWasm": false, + "outputPath": "dist/index.js" +} diff --git a/packages/runtime-core/src/PluginLoader.ts b/packages/runtime-core/src/PluginLoader.ts index 2b7eb2ba..a3b1df61 100644 --- a/packages/runtime-core/src/PluginLoader.ts +++ b/packages/runtime-core/src/PluginLoader.ts @@ -42,7 +42,7 @@ export async function loadPlugin( const exportName = packageInfo.pluginExport || 'default'; const plugin = module[exportName] as IPlugin; - if (!plugin || !plugin.descriptor) { + if (!plugin || !plugin.manifest) { console.warn(`[PluginLoader] Invalid plugin export from ${packageId}`); return null; } diff --git a/packages/runtime-core/src/PluginManager.ts b/packages/runtime-core/src/PluginManager.ts index c3e10e63..1b972a40 100644 --- a/packages/runtime-core/src/PluginManager.ts +++ b/packages/runtime-core/src/PluginManager.ts @@ -5,35 +5,9 @@ import { ComponentRegistry, ServiceContainer } from '@esengine/ecs-framework'; import type { IScene } from '@esengine/ecs-framework'; +import type { IPlugin, IRuntimeModule, SystemContext, ModuleManifest } from '@esengine/engine-core'; -export interface SystemContext { - isEditor: boolean; - [key: string]: any; -} - -export interface PluginDescriptor { - id: string; - name: string; - version: string; - description?: string; - category?: string; - enabledByDefault?: boolean; - isEnginePlugin?: boolean; -} - -export interface IRuntimeModule { - registerComponents?(registry: typeof ComponentRegistry): void; - registerServices?(services: ServiceContainer): void; - createSystems?(scene: IScene, context: SystemContext): void; - onSystemsCreated?(scene: IScene, context: SystemContext): void; - onInitialize?(): Promise; - onDestroy?(): void; -} - -export interface IPlugin { - readonly descriptor: PluginDescriptor; - readonly runtimeModule?: IRuntimeModule; -} +export type { IPlugin, IRuntimeModule, SystemContext, ModuleManifest }; export class RuntimePluginManager { private _plugins = new Map(); @@ -41,12 +15,12 @@ export class RuntimePluginManager { private _bInitialized = false; register(plugin: IPlugin): void { - const id = plugin.descriptor.id; + const id = plugin.manifest.id; if (this._plugins.has(id)) { return; } this._plugins.set(id, plugin); - if (plugin.descriptor.enabledByDefault !== false) { + if (plugin.manifest.defaultEnabled !== false) { this._enabledPlugins.add(id); } } @@ -68,9 +42,9 @@ export class RuntimePluginManager { for (const id of config.enabledPlugins) { this._enabledPlugins.add(id); } - // 始终启用引擎核心插件 + // 始终启用引擎核心模块 for (const [id, plugin] of this._plugins) { - if (plugin.descriptor.isEnginePlugin) { + if (plugin.manifest.isEngineModule) { this._enabledPlugins.add(id); } } diff --git a/packages/runtime-core/src/RuntimeBootstrap.ts b/packages/runtime-core/src/RuntimeBootstrap.ts index 93933e5c..656778e7 100644 --- a/packages/runtime-core/src/RuntimeBootstrap.ts +++ b/packages/runtime-core/src/RuntimeBootstrap.ts @@ -9,7 +9,7 @@ import { runtimePluginManager, type IPlugin, type IRuntimeModule, - type PluginDescriptor, + type ModuleManifest, type SystemContext } from './PluginManager'; @@ -22,10 +22,10 @@ export interface RuntimeConfig { * 创建插件(简化工厂) */ export function createPlugin( - descriptor: PluginDescriptor, + manifest: ModuleManifest, runtimeModule: IRuntimeModule ): IPlugin { - return { descriptor, runtimeModule }; + return { manifest, runtimeModule }; } /** @@ -44,7 +44,7 @@ export async function initializeRuntime(config?: RuntimeConfig): Promise { runtimePluginManager.loadConfig({ enabledPlugins: config.enabledPlugins }); } else { for (const plugin of runtimePluginManager.getPlugins()) { - runtimePluginManager.enable(plugin.descriptor.id); + runtimePluginManager.enable(plugin.manifest.id); } } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 24fdf1c8..59b263d8 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -2,7 +2,7 @@ export { RuntimePluginManager, runtimePluginManager, type SystemContext, - type PluginDescriptor, + type ModuleManifest, type IRuntimeModule, type IPlugin } from './PluginManager';