Feature/tilemap editor (#237)
* feat: 添加 Tilemap 编辑器插件和组件生命周期支持 * feat(editor-core): 添加声明式插件注册 API * feat(editor-core): 改进tiledmap结构合并tileset进tiledmapeditor * feat: 添加 editor-runtime SDK 和插件系统改进 * fix(ci): 修复SceneResourceManager里变量未使用问题
This commit is contained in:
352
examples/core-demos/pnpm-lock.yaml
generated
Normal file
352
examples/core-demos/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,352 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@esengine/ecs-framework':
|
||||
specifier: file:../../packages/core
|
||||
version: file:../../packages/core
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.0.0
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^4.0.0
|
||||
version: 4.5.14
|
||||
|
||||
packages:
|
||||
|
||||
'@esbuild/android-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.18.20':
|
||||
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.18.20':
|
||||
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.18.20':
|
||||
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.18.20':
|
||||
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.18.20':
|
||||
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.18.20':
|
||||
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.18.20':
|
||||
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.18.20':
|
||||
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.18.20':
|
||||
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.18.20':
|
||||
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.18.20':
|
||||
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.18.20':
|
||||
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-x64@0.18.20':
|
||||
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.18.20':
|
||||
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/sunos-x64@0.18.20':
|
||||
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.18.20':
|
||||
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.18.20':
|
||||
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esengine/ecs-framework@file:../../packages/core':
|
||||
resolution: {directory: ../../packages/core, type: directory}
|
||||
|
||||
esbuild@0.18.20:
|
||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
postcss@8.5.6:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
rollup@3.29.5:
|
||||
resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==}
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
vite@4.5.14:
|
||||
resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': '>= 14'
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.4.0
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/android-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esengine/ecs-framework@file:../../packages/core':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
esbuild@0.18.20:
|
||||
optionalDependencies:
|
||||
'@esbuild/android-arm': 0.18.20
|
||||
'@esbuild/android-arm64': 0.18.20
|
||||
'@esbuild/android-x64': 0.18.20
|
||||
'@esbuild/darwin-arm64': 0.18.20
|
||||
'@esbuild/darwin-x64': 0.18.20
|
||||
'@esbuild/freebsd-arm64': 0.18.20
|
||||
'@esbuild/freebsd-x64': 0.18.20
|
||||
'@esbuild/linux-arm': 0.18.20
|
||||
'@esbuild/linux-arm64': 0.18.20
|
||||
'@esbuild/linux-ia32': 0.18.20
|
||||
'@esbuild/linux-loong64': 0.18.20
|
||||
'@esbuild/linux-mips64el': 0.18.20
|
||||
'@esbuild/linux-ppc64': 0.18.20
|
||||
'@esbuild/linux-riscv64': 0.18.20
|
||||
'@esbuild/linux-s390x': 0.18.20
|
||||
'@esbuild/linux-x64': 0.18.20
|
||||
'@esbuild/netbsd-x64': 0.18.20
|
||||
'@esbuild/openbsd-x64': 0.18.20
|
||||
'@esbuild/sunos-x64': 0.18.20
|
||||
'@esbuild/win32-arm64': 0.18.20
|
||||
'@esbuild/win32-ia32': 0.18.20
|
||||
'@esbuild/win32-x64': 0.18.20
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
postcss@8.5.6:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
rollup@3.29.5:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
vite@4.5.14:
|
||||
dependencies:
|
||||
esbuild: 0.18.20
|
||||
postcss: 8.5.6
|
||||
rollup: 3.29.5
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
@@ -86,11 +86,9 @@ export class AssetPathResolver {
|
||||
// 应用自定义转换器(如果提供)
|
||||
if (this.config.pathTransformer) {
|
||||
path = this.config.pathTransformer(path);
|
||||
// Re-validate after transformation
|
||||
const postTransform = PathValidator.validate(path);
|
||||
if (!postTransform.valid) {
|
||||
throw new Error(`Path transformer produced invalid path: ${postTransform.reason}`);
|
||||
}
|
||||
// Transformer output is trusted (may be absolute path or asset:// URL)
|
||||
// 转换器输出是可信的(可能是绝对路径或 asset:// URL)
|
||||
return path;
|
||||
}
|
||||
|
||||
// Platform-specific resolution
|
||||
|
||||
@@ -9,6 +9,7 @@ export * from './types/AssetTypes';
|
||||
// Interfaces
|
||||
export * from './interfaces/IAssetLoader';
|
||||
export * from './interfaces/IAssetManager';
|
||||
export * from './interfaces/IResourceComponent';
|
||||
|
||||
// Core
|
||||
export { AssetManager } from './core/AssetManager';
|
||||
@@ -30,6 +31,13 @@ export { BinaryLoader } from './loaders/BinaryLoader';
|
||||
export { EngineIntegration } from './integration/EngineIntegration';
|
||||
export type { IEngineBridge } from './integration/EngineIntegration';
|
||||
|
||||
// Services
|
||||
export { SceneResourceManager } from './services/SceneResourceManager';
|
||||
export type { IResourceLoader } from './services/SceneResourceManager';
|
||||
|
||||
// Utils
|
||||
export { UVHelper } from './utils/UVHelper';
|
||||
|
||||
// Default instance
|
||||
import { AssetManager } from './core/AssetManager';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { AssetManager } from '../core/AssetManager';
|
||||
import { AssetGUID } from '../types/AssetTypes';
|
||||
import { ITextureAsset } from '../interfaces/IAssetLoader';
|
||||
import { globalPathResolver } from '../core/AssetPathResolver';
|
||||
|
||||
/**
|
||||
* Engine bridge interface
|
||||
@@ -63,24 +64,35 @@ export class EngineIntegration {
|
||||
/**
|
||||
* Load texture for component
|
||||
* 为组件加载纹理
|
||||
*
|
||||
* 统一的路径解析入口:相对路径会被转换为 Tauri 可用的 asset:// URL
|
||||
* Unified path resolution entry: relative paths will be converted to Tauri-compatible asset:// URLs
|
||||
*/
|
||||
async loadTextureForComponent(texturePath: string): Promise<number> {
|
||||
// 检查是否已有纹理ID / Check if texture ID exists
|
||||
// 检查缓存(使用原始路径作为键)
|
||||
// Check cache (using original path as key)
|
||||
const existingId = this._pathToTextureId.get(texturePath);
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
|
||||
// 通过资产系统加载 / Load through asset system
|
||||
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(texturePath);
|
||||
// 使用 globalPathResolver 转换路径
|
||||
// Use globalPathResolver to transform the path
|
||||
const resolvedPath = globalPathResolver.resolve(texturePath);
|
||||
|
||||
// 通过资产系统加载(使用解析后的路径)
|
||||
// Load through asset system (using resolved path)
|
||||
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(resolvedPath);
|
||||
const textureAsset = result.asset;
|
||||
|
||||
// 如果有引擎桥接,上传到GPU / Upload to GPU if bridge exists
|
||||
// 如果有引擎桥接,上传到GPU(使用解析后的路径)
|
||||
// Upload to GPU if bridge exists (using resolved path)
|
||||
if (this._engineBridge && textureAsset.data) {
|
||||
await this._engineBridge.loadTexture(textureAsset.textureId, texturePath);
|
||||
await this._engineBridge.loadTexture(textureAsset.textureId, resolvedPath);
|
||||
}
|
||||
|
||||
// 缓存映射 / Cache mapping
|
||||
// 缓存映射(使用原始路径作为键,避免重复解析)
|
||||
// Cache mapping (using original path as key to avoid re-resolving)
|
||||
this._pathToTextureId.set(texturePath, textureAsset.textureId);
|
||||
|
||||
return textureAsset.textureId;
|
||||
@@ -150,6 +162,25 @@ export class EngineIntegration {
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载资源(通用方法,支持 IResourceLoader 接口)
|
||||
* Load resources in batch (generic method for IResourceLoader interface)
|
||||
*
|
||||
* @param paths 资源路径数组 / Array of resource paths
|
||||
* @param type 资源类型 / Resource type
|
||||
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
||||
*/
|
||||
async loadResourcesBatch(paths: string[], type: 'texture' | 'audio' | 'font' | 'data'): Promise<Map<string, number>> {
|
||||
// 目前只支持纹理 / Currently only supports textures
|
||||
if (type === 'texture') {
|
||||
return this.loadTexturesBatch(paths);
|
||||
}
|
||||
|
||||
// 其他资源类型暂未实现 / Other resource types not yet implemented
|
||||
console.warn(`[EngineIntegration] Resource type '${type}' not yet supported`);
|
||||
return new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload texture
|
||||
* 卸载纹理
|
||||
|
||||
62
packages/asset-system/src/interfaces/IResourceComponent.ts
Normal file
62
packages/asset-system/src/interfaces/IResourceComponent.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 资源组件接口 - 用于依赖运行时资源的组件(纹理、音频等)
|
||||
* Interface for components that depend on runtime resources (textures, audio, etc.)
|
||||
*
|
||||
* 实现此接口的组件可以参与 SceneResourceManager 管理的集中式资源加载
|
||||
* Components implementing this interface can participate in centralized resource loading managed by SceneResourceManager
|
||||
*/
|
||||
|
||||
/**
|
||||
* 资源引用 - 包含路径和运行时 ID
|
||||
* Resource reference with path and runtime ID
|
||||
*/
|
||||
export interface ResourceReference {
|
||||
/** 资源路径(例如 "assets/sprites/player.png")/ Asset path (e.g., "assets/sprites/player.png") */
|
||||
path: string;
|
||||
/** 引擎分配的运行时资源 ID(例如 GPU 上的纹理 ID)/ Runtime resource ID assigned by engine (e.g., texture ID on GPU) */
|
||||
runtimeId?: number;
|
||||
/** 资源类型标识符 / Resource type identifier */
|
||||
type: 'texture' | 'audio' | 'font' | 'data';
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源组件接口
|
||||
* Resource component interface
|
||||
*
|
||||
* 实现此接口的组件可以在场景启动前由 SceneResourceManager 集中加载资源
|
||||
* Components implementing this interface can have their resources loaded centrally by SceneResourceManager before the scene starts
|
||||
*/
|
||||
export interface IResourceComponent {
|
||||
/**
|
||||
* 获取此组件需要的所有资源引用
|
||||
* Get all resource references needed by this component
|
||||
*
|
||||
* 在场景加载期间调用以收集资源路径
|
||||
* Called during scene loading to collect resource paths
|
||||
*/
|
||||
getResourceReferences(): ResourceReference[];
|
||||
|
||||
/**
|
||||
* 设置已加载资源的运行时 ID
|
||||
* Set runtime IDs for loaded resources
|
||||
*
|
||||
* 在 SceneResourceManager 加载资源后调用
|
||||
* Called after resources are loaded by SceneResourceManager
|
||||
*
|
||||
* @param pathToId 资源路径到运行时 ID 的映射 / Map of resource paths to runtime IDs
|
||||
*/
|
||||
setResourceIds(pathToId: Map<string, number>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型守卫 - 检查组件是否实现了 IResourceComponent
|
||||
* Type guard to check if a component implements IResourceComponent
|
||||
*/
|
||||
export function isResourceComponent(component: any): component is IResourceComponent {
|
||||
return (
|
||||
component !== null &&
|
||||
typeof component === 'object' &&
|
||||
typeof component.getResourceReferences === 'function' &&
|
||||
typeof component.setResourceIds === 'function'
|
||||
);
|
||||
}
|
||||
155
packages/asset-system/src/services/SceneResourceManager.ts
Normal file
155
packages/asset-system/src/services/SceneResourceManager.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 场景资源管理器 - 集中式场景资源加载
|
||||
* SceneResourceManager - Centralized resource loading for scenes
|
||||
*
|
||||
* 扫描场景中所有组件,收集资源引用,批量加载资源,并将运行时 ID 分配回组件
|
||||
* Scans all components in a scene, collects resource references, batch-loads them, and assigns runtime IDs back to components
|
||||
*/
|
||||
|
||||
import type { Scene } from '@esengine/ecs-framework';
|
||||
import { isResourceComponent, type ResourceReference } from '../interfaces/IResourceComponent';
|
||||
|
||||
/**
|
||||
* 资源加载器接口
|
||||
* Resource loader interface
|
||||
*/
|
||||
export interface IResourceLoader {
|
||||
/**
|
||||
* 批量加载资源并返回路径到 ID 的映射
|
||||
* Load a batch of resources and return path-to-ID mapping
|
||||
* @param paths 资源路径数组 / Array of resource paths
|
||||
* @param type 资源类型 / Resource type
|
||||
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
||||
*/
|
||||
loadResourcesBatch(paths: string[], type: ResourceReference['type']): Promise<Map<string, number>>;
|
||||
}
|
||||
|
||||
export class SceneResourceManager {
|
||||
private resourceLoader: IResourceLoader | null = null;
|
||||
|
||||
/**
|
||||
* 设置资源加载器实现
|
||||
* Set the resource loader implementation
|
||||
*
|
||||
* 应由引擎集成层调用
|
||||
* This should be called by the engine integration layer
|
||||
*
|
||||
* @param loader 资源加载器实例 / Resource loader instance
|
||||
*/
|
||||
setResourceLoader(loader: IResourceLoader): void {
|
||||
this.resourceLoader = loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载场景所需的所有资源
|
||||
* Load all resources required by a scene
|
||||
*
|
||||
* 流程 / Process:
|
||||
* 1. 扫描所有实体并从 IResourceComponent 实现中收集资源引用
|
||||
* Scan all entities and collect resource references from IResourceComponent implementations
|
||||
* 2. 按类型分组资源(纹理、音频等)
|
||||
* Group resources by type (texture, audio, etc.)
|
||||
* 3. 批量加载每种资源类型
|
||||
* Batch load each resource type
|
||||
* 4. 将运行时 ID 分配回组件
|
||||
* Assign runtime IDs back to components
|
||||
*
|
||||
* @param scene 要加载资源的场景 / The scene to load resources for
|
||||
* @returns 当所有资源加载完成时解析的 Promise / Promise that resolves when all resources are loaded
|
||||
*/
|
||||
async loadSceneResources(scene: Scene): Promise<void> {
|
||||
if (!this.resourceLoader) {
|
||||
console.warn('[SceneResourceManager] No resource loader set, skipping resource loading');
|
||||
return;
|
||||
}
|
||||
|
||||
// 从组件收集所有资源引用 / Collect all resource references from components
|
||||
const resourceRefs = this.collectResourceReferences(scene);
|
||||
|
||||
if (resourceRefs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 按资源类型分组 / Group by resource type
|
||||
const resourcesByType = new Map<ResourceReference['type'], Set<string>>();
|
||||
for (const ref of resourceRefs) {
|
||||
if (!resourcesByType.has(ref.type)) {
|
||||
resourcesByType.set(ref.type, new Set());
|
||||
}
|
||||
resourcesByType.get(ref.type)!.add(ref.path);
|
||||
}
|
||||
|
||||
// 批量加载每种资源类型 / Load each resource type in batch
|
||||
const allResourceIds = new Map<string, number>();
|
||||
|
||||
for (const [type, paths] of resourcesByType) {
|
||||
const pathsArray = Array.from(paths);
|
||||
|
||||
try {
|
||||
const resourceIds = await this.resourceLoader.loadResourcesBatch(pathsArray, type);
|
||||
|
||||
// 合并到总映射表 / Merge into combined map
|
||||
for (const [path, id] of resourceIds) {
|
||||
allResourceIds.set(path, id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[SceneResourceManager] Failed to load ${type} resources:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 将资源 ID 分配回组件 / Assign resource IDs back to components
|
||||
this.assignResourceIds(scene, allResourceIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从场景实体收集所有资源引用
|
||||
* Collect all resource references from scene entities
|
||||
*/
|
||||
private collectResourceReferences(scene: Scene): ResourceReference[] {
|
||||
const refs: ResourceReference[] = [];
|
||||
|
||||
for (const entity of scene.entities.buffer) {
|
||||
for (const component of entity.components) {
|
||||
if (isResourceComponent(component)) {
|
||||
const componentRefs = component.getResourceReferences();
|
||||
refs.push(...componentRefs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将已加载的资源 ID 分配回组件
|
||||
* Assign loaded resource IDs back to components
|
||||
*
|
||||
* @param scene 场景 / Scene
|
||||
* @param pathToId 路径到 ID 的映射 / Path to ID mapping
|
||||
*/
|
||||
private assignResourceIds(scene: Scene, pathToId: Map<string, number>): void {
|
||||
for (const entity of scene.entities.buffer) {
|
||||
for (const component of entity.components) {
|
||||
if (isResourceComponent(component)) {
|
||||
component.setResourceIds(pathToId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载场景使用的所有资源
|
||||
* Unload all resources used by a scene
|
||||
*
|
||||
* 在场景销毁时调用
|
||||
* Called when a scene is being destroyed
|
||||
*
|
||||
* @param scene 要卸载资源的场景 / The scene to unload resources for
|
||||
*/
|
||||
async unloadSceneResources(_scene: Scene): Promise<void> {
|
||||
// TODO: 实现资源卸载 / Implement resource unloading
|
||||
// 需要跟踪资源引用计数,仅在不再使用时卸载
|
||||
// Need to track resource reference counts and only unload when no longer used
|
||||
console.log('[SceneResourceManager] Scene resource unloading not yet implemented');
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,10 @@ export enum AssetType {
|
||||
AnimationClip = 'animation',
|
||||
/** 行为树 */
|
||||
BehaviorTree = 'behaviortree',
|
||||
/** 瓦片地图 */
|
||||
Tilemap = 'tilemap',
|
||||
/** 瓦片集 */
|
||||
Tileset = 'tileset',
|
||||
/** JSON数据 */
|
||||
Json = 'json',
|
||||
/** 文本 */
|
||||
|
||||
81
packages/asset-system/src/utils/UVHelper.ts
Normal file
81
packages/asset-system/src/utils/UVHelper.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* UV Coordinate Helper
|
||||
* UV 坐标辅助工具
|
||||
*
|
||||
* 引擎使用图像坐标系:
|
||||
* Engine uses image coordinate system:
|
||||
* - 原点 (0, 0) 在左上角 | Origin at top-left
|
||||
* - V 轴向下增长 | V-axis increases downward
|
||||
* - UV 格式:[u0, v0, u1, v1] 其中 v0 < v1
|
||||
*/
|
||||
export class UVHelper {
|
||||
/**
|
||||
* Calculate UV coordinates for a texture region
|
||||
* 计算纹理区域的 UV 坐标
|
||||
*/
|
||||
static calculateUV(
|
||||
imageRect: { x: number; y: number; width: number; height: number },
|
||||
textureSize: { width: number; height: number }
|
||||
): [number, number, number, number] {
|
||||
const { x, y, width, height } = imageRect;
|
||||
const { width: tw, height: th } = textureSize;
|
||||
|
||||
return [
|
||||
x / tw, // u0
|
||||
y / th, // v0
|
||||
(x + width) / tw, // u1
|
||||
(y + height) / th // v1
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate UV coordinates for a tile in a tileset
|
||||
* 计算 tileset 中某个 tile 的 UV 坐标
|
||||
*/
|
||||
static calculateTileUV(
|
||||
tileIndex: number,
|
||||
tilesetInfo: {
|
||||
columns: number;
|
||||
tileWidth: number;
|
||||
tileHeight: number;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
margin?: number;
|
||||
spacing?: number;
|
||||
}
|
||||
): [number, number, number, number] | null {
|
||||
if (tileIndex < 0) return null;
|
||||
|
||||
const {
|
||||
columns,
|
||||
tileWidth,
|
||||
tileHeight,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
margin = 0,
|
||||
spacing = 0
|
||||
} = tilesetInfo;
|
||||
|
||||
const col = tileIndex % columns;
|
||||
const row = Math.floor(tileIndex / columns);
|
||||
const x = margin + col * (tileWidth + spacing);
|
||||
const y = margin + row * (tileHeight + spacing);
|
||||
|
||||
return this.calculateUV(
|
||||
{ x, y, width: tileWidth, height: tileHeight },
|
||||
{ width: imageWidth, height: imageHeight }
|
||||
);
|
||||
}
|
||||
|
||||
static validateUV(uv: [number, number, number, number]): boolean {
|
||||
const [u0, v0, u1, v1] = uv;
|
||||
return u0 >= 0 && u0 <= 1 && u1 >= 0 && u1 <= 1 &&
|
||||
v0 >= 0 && v0 <= 1 && v1 >= 0 && v1 <= 1 &&
|
||||
u0 < u1 && v0 < v1;
|
||||
}
|
||||
|
||||
static debugPrint(uv: [number, number, number, number], label?: string): void {
|
||||
const prefix = label ? `[${label}] ` : '';
|
||||
console.log(`${prefix}UV: [${uv.map(n => n.toFixed(4)).join(', ')}]`);
|
||||
}
|
||||
}
|
||||
@@ -25,32 +25,21 @@
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@esengine/behavior-tree": "workspace:*",
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/editor-core": "workspace:*",
|
||||
"@esengine/editor-runtime": "workspace:*",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-replace": "^6.0.3",
|
||||
"@types/react": "^18.3.18",
|
||||
"lucide-react": "^0.469.0",
|
||||
"react": "^18.3.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.28.1",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"rollup-plugin-dts": "^6.1.1",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"typescript": "^5.8.2",
|
||||
"zustand": "^5.0.2"
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/behavior-tree": "*",
|
||||
"@esengine/ecs-framework": "*",
|
||||
"@esengine/editor-core": "*",
|
||||
"@tauri-apps/api": "*",
|
||||
"@tauri-apps/plugin-dialog": "*",
|
||||
"@tauri-apps/plugin-http": "*",
|
||||
"lucide-react": "^0.469.0",
|
||||
"react": "^18.3.1",
|
||||
"tsyringe": "*",
|
||||
"zustand": "^5.0.2"
|
||||
"@esengine/editor-runtime": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"mobx": "^6.15.0",
|
||||
|
||||
2039
packages/behavior-tree-editor/pnpm-lock.yaml
generated
Normal file
2039
packages/behavior-tree-editor/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,12 @@
|
||||
const resolve = require('@rollup/plugin-node-resolve');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const replace = require('@rollup/plugin-replace');
|
||||
const dts = require('rollup-plugin-dts').default;
|
||||
const postcss = require('rollup-plugin-postcss');
|
||||
|
||||
const external = [
|
||||
'react',
|
||||
'react/jsx-runtime',
|
||||
'zustand',
|
||||
'zustand/middleware',
|
||||
'lucide-react',
|
||||
'@esengine/ecs-framework',
|
||||
'@esengine/editor-core',
|
||||
'@esengine/editor-runtime',
|
||||
'@esengine/behavior-tree',
|
||||
'tsyringe',
|
||||
'@tauri-apps/api/core',
|
||||
'@tauri-apps/plugin-dialog'
|
||||
];
|
||||
|
||||
module.exports = [
|
||||
@@ -28,6 +20,10 @@ module.exports = [
|
||||
inlineDynamicImports: true
|
||||
},
|
||||
plugins: [
|
||||
replace({
|
||||
preventAssignment: true,
|
||||
'process.env.NODE_ENV': JSON.stringify('production')
|
||||
}),
|
||||
resolve({
|
||||
extensions: ['.js', '.jsx']
|
||||
}),
|
||||
@@ -60,7 +56,12 @@ module.exports = [
|
||||
],
|
||||
external: [
|
||||
...external,
|
||||
/\.css$/
|
||||
/\.css$/,
|
||||
// 排除 React 相关类型,避免 rollup-plugin-dts 解析问题
|
||||
'react',
|
||||
'react-dom',
|
||||
/^@types\//,
|
||||
/^@esengine\//
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
import type { Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import {
|
||||
IEditorPlugin,
|
||||
type Core,
|
||||
type ServiceContainer,
|
||||
type IService,
|
||||
type ServiceType,
|
||||
type IEditorPlugin,
|
||||
EditorPluginCategory,
|
||||
CompilerRegistry,
|
||||
ICompilerRegistry,
|
||||
InspectorRegistry,
|
||||
IInspectorRegistry,
|
||||
PanelPosition,
|
||||
type FileCreationTemplate,
|
||||
type FileActionHandler,
|
||||
type PanelDescriptor
|
||||
} from '@esengine/editor-core';
|
||||
type PanelDescriptor,
|
||||
createElement,
|
||||
Icons,
|
||||
createLogger,
|
||||
} from '@esengine/editor-runtime';
|
||||
import { BehaviorTreeService } from './services/BehaviorTreeService';
|
||||
import { FileSystemService } from './services/FileSystemService';
|
||||
import { BehaviorTreeCompiler } from './compiler/BehaviorTreeCompiler';
|
||||
import { BehaviorTreeNodeInspectorProvider } from './providers/BehaviorTreeNodeInspectorProvider';
|
||||
import { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
|
||||
import { useBehaviorTreeDataStore } from './stores';
|
||||
import { createElement } from 'react';
|
||||
import { GitBranch } from 'lucide-react';
|
||||
import { createRootNode } from './domain/constants/RootNode';
|
||||
import type { IService, ServiceType } from '@esengine/ecs-framework';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import { PluginContext } from './PluginContext';
|
||||
|
||||
const { GitBranch } = Icons;
|
||||
|
||||
const logger = createLogger('BehaviorTreePlugin');
|
||||
|
||||
@@ -38,6 +45,8 @@ export class BehaviorTreePlugin implements IEditorPlugin {
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
this.services = services;
|
||||
// 设置插件上下文,让内部服务可以访问服务容器
|
||||
PluginContext.setServices(services);
|
||||
this.registerServices(services);
|
||||
this.registerCompilers(services);
|
||||
this.registerInspectors(services);
|
||||
@@ -53,6 +62,7 @@ export class BehaviorTreePlugin implements IEditorPlugin {
|
||||
|
||||
this.registeredServices.clear();
|
||||
useBehaviorTreeDataStore.getState().reset();
|
||||
PluginContext.clear();
|
||||
this.services = undefined;
|
||||
}
|
||||
|
||||
@@ -88,7 +98,7 @@ export class BehaviorTreePlugin implements IEditorPlugin {
|
||||
|
||||
private registerCompilers(services: ServiceContainer): void {
|
||||
try {
|
||||
const compilerRegistry = services.resolve(CompilerRegistry);
|
||||
const compilerRegistry = services.resolve<CompilerRegistry>(ICompilerRegistry);
|
||||
const compiler = new BehaviorTreeCompiler();
|
||||
compilerRegistry.register(compiler);
|
||||
logger.info('Successfully registered BehaviorTreeCompiler');
|
||||
@@ -98,11 +108,15 @@ export class BehaviorTreePlugin implements IEditorPlugin {
|
||||
}
|
||||
|
||||
private registerInspectors(services: ServiceContainer): void {
|
||||
const inspectorRegistry = services.resolve(InspectorRegistry);
|
||||
try {
|
||||
const inspectorRegistry = services.resolve<InspectorRegistry>(IInspectorRegistry);
|
||||
if (inspectorRegistry) {
|
||||
const provider = new BehaviorTreeNodeInspectorProvider();
|
||||
inspectorRegistry.register(provider);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to register inspector:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private registerFileActions(services: ServiceContainer): void {
|
||||
|
||||
26
packages/behavior-tree-editor/src/PluginContext.ts
Normal file
26
packages/behavior-tree-editor/src/PluginContext.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { ServiceContainer } from '@esengine/editor-runtime';
|
||||
|
||||
/**
|
||||
* 插件上下文
|
||||
* 存储插件安装时传入的服务容器引用
|
||||
*/
|
||||
class PluginContextClass {
|
||||
private _services: ServiceContainer | null = null;
|
||||
|
||||
setServices(services: ServiceContainer): void {
|
||||
this._services = services;
|
||||
}
|
||||
|
||||
getServices(): ServiceContainer {
|
||||
if (!this._services) {
|
||||
throw new Error('PluginContext not initialized. Make sure the plugin is properly installed.');
|
||||
}
|
||||
return this._services;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._services = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const PluginContext = new PluginContextClass();
|
||||
@@ -1,4 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
import { createStore } from '@esengine/editor-runtime';
|
||||
|
||||
const create = createStore;
|
||||
import { NodeTemplates, NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
||||
import { Node } from '../../domain/models/Node';
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ICompiler, CompileResult, CompilerContext, IFileSystem } from '@esengine/editor-core';
|
||||
import { File, FolderTree, FolderOpen } from 'lucide-react';
|
||||
import {
|
||||
React,
|
||||
useState,
|
||||
useEffect,
|
||||
type ICompiler,
|
||||
type CompileResult,
|
||||
type CompilerContext,
|
||||
type IFileSystem,
|
||||
Icons,
|
||||
createLogger,
|
||||
} from '@esengine/editor-runtime';
|
||||
import { GlobalBlackboardTypeGenerator } from '../generators/GlobalBlackboardTypeGenerator';
|
||||
import { EditorFormatConverter, BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';
|
||||
import { useBehaviorTreeDataStore } from '../application/state/BehaviorTreeDataStore';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
const { File, FolderTree, FolderOpen } = Icons;
|
||||
|
||||
const logger = createLogger('BehaviorTreeCompiler');
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
||||
import { React, useEffect, useMemo, useRef, useState, useCallback } from '@esengine/editor-runtime';
|
||||
import { NodeTemplate, BlackboardValueType } from '@esengine/behavior-tree';
|
||||
import { useBehaviorTreeDataStore, BehaviorTreeNode, ROOT_NODE_ID } from '../stores';
|
||||
import { useUIStore } from '../stores';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Clipboard, Edit2, Trash2, ChevronDown, ChevronRight, Globe, GripVertical, ChevronLeft, Plus, Copy } from 'lucide-react';
|
||||
import { React, useState, Icons } from '@esengine/editor-runtime';
|
||||
|
||||
const { Clipboard, Edit2, Trash2, ChevronDown, ChevronRight, Globe, GripVertical, ChevronLeft, Plus, Copy } = Icons;
|
||||
|
||||
type SimpleBlackboardType = 'number' | 'string' | 'boolean' | 'object';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useCallback, forwardRef, useState, useEffect } from 'react';
|
||||
import { React, useRef, useCallback, forwardRef, useState, useEffect } from '@esengine/editor-runtime';
|
||||
import { useCanvasInteraction } from '../../hooks/useCanvasInteraction';
|
||||
import { EditorConfig } from '../../types';
|
||||
import { GridBackground } from './GridBackground';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { React, useMemo } from '@esengine/editor-runtime';
|
||||
|
||||
interface GridBackgroundProps {
|
||||
canvasOffset: { x: number; y: number };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useRef, useEffect, ReactNode } from 'react';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import { React, useState, useRef, useEffect, type ReactNode, Icons } from '@esengine/editor-runtime';
|
||||
|
||||
const { GripVertical } = Icons;
|
||||
|
||||
interface DraggablePanelProps {
|
||||
title: string | ReactNode;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { React, useMemo } from '@esengine/editor-runtime';
|
||||
import { ConnectionRenderer } from './ConnectionRenderer';
|
||||
import { ConnectionViewData } from '../../types';
|
||||
import { Node } from '../../domain/models/Node';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { React, useMemo } from '@esengine/editor-runtime';
|
||||
import { ConnectionViewData } from '../../types';
|
||||
import { Node } from '../../domain/models/Node';
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Trash2, Replace, Plus } from 'lucide-react';
|
||||
import { React, Icons } from '@esengine/editor-runtime';
|
||||
|
||||
const { Trash2, Replace, Plus } = Icons;
|
||||
|
||||
interface NodeContextMenuProps {
|
||||
visible: boolean;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useRef, useEffect, useState, useMemo } from 'react';
|
||||
import { React, useRef, useEffect, useState, useMemo, Icons } from '@esengine/editor-runtime';
|
||||
import type { LucideIcon } from '@esengine/editor-runtime';
|
||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { Search, X, LucideIcon, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { NodeFactory } from '../../infrastructure/factories/NodeFactory';
|
||||
|
||||
const { Search, X, ChevronDown, ChevronRight } = Icons;
|
||||
|
||||
interface QuickCreateMenuProps {
|
||||
visible: boolean;
|
||||
position: { x: number; y: number };
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
TreePine,
|
||||
Database,
|
||||
AlertTriangle,
|
||||
AlertCircle,
|
||||
LucideIcon
|
||||
} from 'lucide-react';
|
||||
import { React, Icons } from '@esengine/editor-runtime';
|
||||
import type { LucideIcon } from '@esengine/editor-runtime';
|
||||
import { PropertyDefinition } from '@esengine/behavior-tree';
|
||||
|
||||
import { Node as BehaviorTreeNodeType } from '../../domain/models/Node';
|
||||
import { Connection } from '../../domain/models/Connection';
|
||||
import { ROOT_NODE_ID } from '../../domain/constants/RootNode';
|
||||
import type { NodeExecutionStatus } from '../../stores';
|
||||
|
||||
import { BehaviorTreeExecutor } from '../../utils/BehaviorTreeExecutor';
|
||||
import { BlackboardValue } from '../../domain/models/Blackboard';
|
||||
|
||||
const { TreePine, Database, AlertTriangle, AlertCircle } = Icons;
|
||||
|
||||
type BlackboardVariables = Record<string, BlackboardValue>;
|
||||
|
||||
interface BehaviorTreeNodeProps {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { React, useMemo, Icons } from '@esengine/editor-runtime';
|
||||
import type { LucideIcon } from '@esengine/editor-runtime';
|
||||
|
||||
import { NodeViewData } from '../../types';
|
||||
|
||||
const LucideIcons = Icons;
|
||||
|
||||
/**
|
||||
* 图标映射
|
||||
*/
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Core, createLogger } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from '@esengine/editor-core';
|
||||
import { open, save } from '@tauri-apps/plugin-dialog';
|
||||
import {
|
||||
React,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
Core,
|
||||
createLogger,
|
||||
MessageHub,
|
||||
open,
|
||||
save,
|
||||
Icons,
|
||||
} from '@esengine/editor-runtime';
|
||||
import { useBehaviorTreeDataStore } from '../../stores';
|
||||
import { BehaviorTreeEditor } from '../BehaviorTreeEditor';
|
||||
import { BehaviorTreeService } from '../../services/BehaviorTreeService';
|
||||
import { showToast } from '../../services/NotificationService';
|
||||
import { FolderOpen } from 'lucide-react';
|
||||
import { Node as BehaviorTreeNode } from '../../domain/models/Node';
|
||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
||||
import './BehaviorTreeEditorPanel.css';
|
||||
|
||||
const { FolderOpen } = Icons;
|
||||
|
||||
const logger = createLogger('BehaviorTreeEditorPanel');
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Play, Pause, Square, SkipForward, Undo, Redo, ZoomIn, Save, FolderOpen, Download, Clipboard, Home } from 'lucide-react';
|
||||
import { React, Icons } from '@esengine/editor-runtime';
|
||||
|
||||
const { Play, Pause, Square, SkipForward, Undo, Redo, ZoomIn, Save, FolderOpen, Download, Clipboard, Home } = Icons;
|
||||
|
||||
type ExecutionMode = 'idle' | 'running' | 'paused';
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { NodeTemplate, NodeType } from '@esengine/behavior-tree';
|
||||
import {
|
||||
import { Icons } from '@esengine/editor-runtime';
|
||||
import type { LucideIcon } from '@esengine/editor-runtime';
|
||||
|
||||
const {
|
||||
List, GitBranch, Layers, Shuffle, RotateCcw,
|
||||
Repeat, CheckCircle, XCircle, CheckCheck, HelpCircle, Snowflake, Timer,
|
||||
Clock, FileText, Edit, Calculator, Code,
|
||||
Equal, Dices, Settings,
|
||||
Database, TreePine,
|
||||
LucideIcon
|
||||
} from 'lucide-react';
|
||||
Database, TreePine
|
||||
} = Icons;
|
||||
|
||||
export const ICON_MAP: Record<string, LucideIcon> = {
|
||||
List,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, React } from '@esengine/editor-runtime';
|
||||
import { useBehaviorTreeDataStore, useUIStore } from '../stores';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RefObject, useEffect, useRef } from 'react';
|
||||
import { type RefObject, useEffect, useRef, React } from '@esengine/editor-runtime';
|
||||
import { BehaviorTreeNode, ROOT_NODE_ID } from '../stores';
|
||||
|
||||
interface QuickCreateMenuState {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { CommandManager } from '@esengine/editor-core';
|
||||
import { useRef, useCallback, useMemo, useEffect, CommandManager } from '@esengine/editor-runtime';
|
||||
|
||||
/**
|
||||
* 撤销/重做功能 Hook
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { CommandManager } from '@esengine/editor-core';
|
||||
import { useCallback, useMemo, CommandManager, createLogger } from '@esengine/editor-runtime';
|
||||
import { ConnectionType } from '../domain/models/Connection';
|
||||
import { IValidator } from '../domain/interfaces/IValidator';
|
||||
import { TreeStateAdapter } from '../application/state/BehaviorTreeDataStore';
|
||||
import { AddConnectionUseCase } from '../application/use-cases/AddConnectionUseCase';
|
||||
import { RemoveConnectionUseCase } from '../application/use-cases/RemoveConnectionUseCase';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
const logger = createLogger('useConnectionOperations');
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, React } from '@esengine/editor-runtime';
|
||||
import { BehaviorTreeNode, ROOT_NODE_ID } from '../stores';
|
||||
|
||||
interface ContextMenuState {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useState, RefObject } from 'react';
|
||||
import { useState, type RefObject, React, createLogger } from '@esengine/editor-runtime';
|
||||
import { NodeTemplate, NodeType } from '@esengine/behavior-tree';
|
||||
import { Position } from '../domain/value-objects/Position';
|
||||
import { useNodeOperations } from './useNodeOperations';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
const logger = createLogger('useDropHandler');
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ask } from '@tauri-apps/plugin-dialog';
|
||||
import { useCallback, React, ask } from '@esengine/editor-runtime';
|
||||
import { BehaviorTreeNode } from '../stores';
|
||||
|
||||
interface UseEditorHandlersParams {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useRef, useState } from '@esengine/editor-runtime';
|
||||
import { BehaviorTreeExecutor } from '../utils/BehaviorTreeExecutor';
|
||||
|
||||
export function useEditorState() {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { useState, useEffect, useMemo, useRef, createLogger } from '@esengine/editor-runtime';
|
||||
import { ExecutionController, ExecutionMode } from '../application/services/ExecutionController';
|
||||
import { BlackboardManager } from '../application/services/BlackboardManager';
|
||||
import { BehaviorTreeNode, Connection, useBehaviorTreeDataStore } from '../stores';
|
||||
import { ExecutionLog } from '../utils/BehaviorTreeExecutor';
|
||||
import { BlackboardValue } from '../domain/models/Blackboard';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
const logger = createLogger('useExecutionController');
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect } from '@esengine/editor-runtime';
|
||||
import { Connection, ROOT_NODE_ID } from '../stores';
|
||||
import { useNodeOperations } from './useNodeOperations';
|
||||
import { useConnectionOperations } from './useConnectionOperations';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useCallback, RefObject } from 'react';
|
||||
import { useRef, useCallback, type RefObject, React } from '@esengine/editor-runtime';
|
||||
import { BehaviorTreeNode, ROOT_NODE_ID } from '../stores';
|
||||
import { Position } from '../domain/value-objects/Position';
|
||||
import { useNodeOperations } from './useNodeOperations';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, CommandManager } from '@esengine/editor-runtime';
|
||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { CommandManager } from '@esengine/editor-core';
|
||||
import { Position } from '../domain/value-objects/Position';
|
||||
import { INodeFactory } from '../domain/interfaces/INodeFactory';
|
||||
import { TreeStateAdapter } from '../application/state/BehaviorTreeDataStore';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect, useRef } from '@esengine/editor-runtime';
|
||||
import { BehaviorTreeNode } from '../stores';
|
||||
import { ExecutionMode } from '../application/services/ExecutionController';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RefObject } from 'react';
|
||||
import { type RefObject, React } from '@esengine/editor-runtime';
|
||||
import { BehaviorTreeNode, Connection, ROOT_NODE_ID, useUIStore } from '../stores';
|
||||
import { PropertyDefinition } from '@esengine/behavior-tree';
|
||||
import { useConnectionOperations } from './useConnectionOperations';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, RefObject } from 'react';
|
||||
import { useState, type RefObject } from '@esengine/editor-runtime';
|
||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { BehaviorTreeNode, Connection, useBehaviorTreeDataStore } from '../stores';
|
||||
import { Node } from '../domain/models/Node';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BehaviorTreePlugin } from './BehaviorTreePlugin';
|
||||
export default new BehaviorTreePlugin();
|
||||
|
||||
export { BehaviorTreePlugin } from './BehaviorTreePlugin';
|
||||
export { PluginContext } from './PluginContext';
|
||||
export { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
|
||||
export * from './BehaviorTreeModule';
|
||||
export * from './services/BehaviorTreeService';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { React, createLogger } from '@esengine/editor-runtime';
|
||||
import type { LucideIcon } from '@esengine/editor-runtime';
|
||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { Node as BehaviorTreeNode } from '../domain/models/Node';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
const logger = createLogger('IEditorExtensions');
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { IInspectorProvider, InspectorContext, MessageHub, FieldEditorRegistry, FieldEditorContext } from '@esengine/editor-core';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
React,
|
||||
useState,
|
||||
useCallback,
|
||||
type IInspectorProvider,
|
||||
type InspectorContext,
|
||||
MessageHub,
|
||||
FieldEditorRegistry,
|
||||
type FieldEditorContext,
|
||||
Core,
|
||||
} from '@esengine/editor-runtime';
|
||||
import { Node as BehaviorTreeNode } from '../domain/models/Node';
|
||||
import { PropertyDefinition } from '@esengine/behavior-tree';
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { singleton } from 'tsyringe';
|
||||
import { Core, IService, createLogger } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from '@esengine/editor-core';
|
||||
import {
|
||||
singleton,
|
||||
type IService,
|
||||
createLogger,
|
||||
MessageHub,
|
||||
IMessageHub,
|
||||
} from '@esengine/editor-runtime';
|
||||
import { useBehaviorTreeDataStore } from '../application/state/BehaviorTreeDataStore';
|
||||
import type { BehaviorTree } from '../domain/models/BehaviorTree';
|
||||
import { FileSystemService } from './FileSystemService';
|
||||
import { PluginContext } from '../PluginContext';
|
||||
|
||||
const logger = createLogger('BehaviorTreeService');
|
||||
|
||||
@@ -15,8 +20,10 @@ export class BehaviorTreeService implements IService {
|
||||
|
||||
async loadFromFile(filePath: string): Promise<void> {
|
||||
try {
|
||||
const services = PluginContext.getServices();
|
||||
|
||||
// 运行时解析 FileSystemService
|
||||
const fileSystem = Core.services.resolve(FileSystemService);
|
||||
const fileSystem = services.resolve(FileSystemService);
|
||||
if (!fileSystem) {
|
||||
throw new Error('FileSystemService not found. Please ensure the BehaviorTreePlugin is properly installed.');
|
||||
}
|
||||
@@ -29,7 +36,7 @@ export class BehaviorTreeService implements IService {
|
||||
// 在 store 中保存文件信息,Panel 挂载时读取
|
||||
store.setCurrentFile(filePath, fileName);
|
||||
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
const messageHub = services.resolve<MessageHub>(IMessageHub);
|
||||
if (messageHub) {
|
||||
messageHub.publish('dynamic-panel:open', {
|
||||
panelId: 'behavior-tree-editor',
|
||||
@@ -50,8 +57,10 @@ export class BehaviorTreeService implements IService {
|
||||
|
||||
async saveToFile(filePath: string, metadata?: { name: string; description: string }): Promise<void> {
|
||||
try {
|
||||
const services = PluginContext.getServices();
|
||||
|
||||
// 运行时解析 FileSystemService
|
||||
const fileSystem = Core.services.resolve(FileSystemService);
|
||||
const fileSystem = services.resolve(FileSystemService);
|
||||
if (!fileSystem) {
|
||||
throw new Error('FileSystemService not found. Please ensure the BehaviorTreePlugin is properly installed.');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { singleton } from 'tsyringe';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
import { singleton, invoke, type IService } from '@esengine/editor-runtime';
|
||||
|
||||
/**
|
||||
* 文件系统服务
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Core, createLogger } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from '@esengine/editor-core';
|
||||
import { Core, createLogger, MessageHub } from '@esengine/editor-runtime';
|
||||
|
||||
const logger = createLogger('NotificationService');
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
import { createStore } from '@esengine/editor-runtime';
|
||||
|
||||
const create = createStore;
|
||||
|
||||
/**
|
||||
* 节点执行统计信息
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
import { createStore } from '@esengine/editor-runtime';
|
||||
|
||||
const create = createStore;
|
||||
|
||||
/**
|
||||
* UI 状态 Store
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { RefObject } from 'react';
|
||||
import { type RefObject, createLogger } from '@esengine/editor-runtime';
|
||||
import { Node as BehaviorTreeNode } from '../domain/models/Node';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
const logger = createLogger('portUtils');
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"build:ts": "tsc",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm run build:ts",
|
||||
"build:esm": "vite build",
|
||||
"build:watch": "tsc --watch",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"build:npm": "npm run build && node build-rollup.cjs",
|
||||
@@ -56,7 +57,8 @@
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3"
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.0.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
|
||||
4312
packages/behavior-tree/pnpm-lock.yaml
generated
Normal file
4312
packages/behavior-tree/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,11 @@
|
||||
export { BehaviorTreeData, BehaviorNodeData, NodeRuntimeState, createDefaultRuntimeState } from './BehaviorTreeData';
|
||||
export type { BehaviorTreeData, BehaviorNodeData, NodeRuntimeState } from './BehaviorTreeData';
|
||||
export { createDefaultRuntimeState } from './BehaviorTreeData';
|
||||
export { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||
export { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
|
||||
export { INodeExecutor, NodeExecutionContext, NodeExecutorRegistry, BindingHelper } from './NodeExecutor';
|
||||
export type { INodeExecutor, NodeExecutionContext } from './NodeExecutor';
|
||||
export { NodeExecutorRegistry, BindingHelper } from './NodeExecutor';
|
||||
export { BehaviorTreeExecutionSystem } from './BehaviorTreeExecutionSystem';
|
||||
export { NodeMetadata, ConfigFieldDefinition, NodeMetadataRegistry, NodeExecutorMetadata } from './NodeMetadata';
|
||||
export type { NodeMetadata, ConfigFieldDefinition, NodeExecutorMetadata } from './NodeMetadata';
|
||||
export { NodeMetadataRegistry } from './NodeMetadata';
|
||||
|
||||
export * from './Executors';
|
||||
|
||||
22
packages/behavior-tree/vite.config.ts
Normal file
22
packages/behavior-tree/vite.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
formats: ['es'],
|
||||
fileName: () => 'behavior-tree.js'
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
exports: 'named',
|
||||
inlineDynamicImports: true
|
||||
}
|
||||
},
|
||||
outDir: 'dist',
|
||||
target: 'es2020',
|
||||
minify: false,
|
||||
sourcemap: true
|
||||
}
|
||||
});
|
||||
5550
packages/core/pnpm-lock.yaml
generated
Normal file
5550
packages/core/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,13 @@ export interface IService {
|
||||
*/
|
||||
export type ServiceType<T extends IService> = new (...args: any[]) => T;
|
||||
|
||||
/**
|
||||
* 服务标识符
|
||||
*
|
||||
* 支持类构造函数或 Symbol 作为服务标识符
|
||||
*/
|
||||
export type ServiceIdentifier<T extends IService = IService> = ServiceType<T> | symbol;
|
||||
|
||||
/**
|
||||
* 服务生命周期
|
||||
*/
|
||||
@@ -43,9 +50,14 @@ export enum ServiceLifetime {
|
||||
*/
|
||||
interface ServiceRegistration<T extends IService> {
|
||||
/**
|
||||
* 服务类型
|
||||
* 服务标识符
|
||||
*/
|
||||
type: ServiceType<T>;
|
||||
identifier: ServiceIdentifier<T>;
|
||||
|
||||
/**
|
||||
* 服务类型(用于构造实例)
|
||||
*/
|
||||
type?: ServiceType<T>;
|
||||
|
||||
/**
|
||||
* 服务实例(单例模式)
|
||||
@@ -96,12 +108,12 @@ export class ServiceContainer {
|
||||
/**
|
||||
* 服务注册表
|
||||
*/
|
||||
private _services: Map<ServiceType<IService>, ServiceRegistration<IService>> = new Map();
|
||||
private _services: Map<ServiceIdentifier, ServiceRegistration<IService>> = new Map();
|
||||
|
||||
/**
|
||||
* 正在解析的服务栈(用于循环依赖检测)
|
||||
*/
|
||||
private _resolving: Set<ServiceType<IService>> = new Set();
|
||||
private _resolving: Set<ServiceIdentifier> = new Set();
|
||||
|
||||
/**
|
||||
* 可更新的服务列表
|
||||
@@ -132,12 +144,13 @@ export class ServiceContainer {
|
||||
type: ServiceType<T>,
|
||||
factory?: (container: ServiceContainer) => T
|
||||
): void {
|
||||
if (this._services.has(type as ServiceType<IService>)) {
|
||||
if (this._services.has(type as ServiceIdentifier)) {
|
||||
logger.warn(`Service ${type.name} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._services.set(type as ServiceType<IService>, {
|
||||
this._services.set(type as ServiceIdentifier, {
|
||||
identifier: type as ServiceIdentifier,
|
||||
type: type as ServiceType<IService>,
|
||||
...(factory && { factory: factory as (container: ServiceContainer) => IService }),
|
||||
lifetime: ServiceLifetime.Singleton
|
||||
@@ -164,12 +177,13 @@ export class ServiceContainer {
|
||||
type: ServiceType<T>,
|
||||
factory?: (container: ServiceContainer) => T
|
||||
): void {
|
||||
if (this._services.has(type as ServiceType<IService>)) {
|
||||
if (this._services.has(type as ServiceIdentifier)) {
|
||||
logger.warn(`Service ${type.name} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._services.set(type as ServiceType<IService>, {
|
||||
this._services.set(type as ServiceIdentifier, {
|
||||
identifier: type as ServiceIdentifier,
|
||||
type: type as ServiceType<IService>,
|
||||
...(factory && { factory: factory as (container: ServiceContainer) => IService }),
|
||||
lifetime: ServiceLifetime.Transient
|
||||
@@ -183,65 +197,77 @@ export class ServiceContainer {
|
||||
*
|
||||
* 直接注册已创建的实例,自动视为单例。
|
||||
*
|
||||
* @param type - 服务类型(构造函数,仅用作标识)
|
||||
* @param identifier - 服务标识符(构造函数或 Symbol)
|
||||
* @param instance - 服务实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const config = new Config();
|
||||
* container.registerInstance(Config, config);
|
||||
*
|
||||
* // 使用 Symbol 作为标识符(适用于接口)
|
||||
* const IFileSystem = Symbol('IFileSystem');
|
||||
* container.registerInstance(IFileSystem, new TauriFileSystem());
|
||||
* ```
|
||||
*/
|
||||
public registerInstance<T extends IService>(type: ServiceType<T>, instance: T): void {
|
||||
if (this._services.has(type as ServiceType<IService>)) {
|
||||
logger.warn(`Service ${type.name} is already registered`);
|
||||
public registerInstance<T extends IService>(identifier: ServiceIdentifier<T>, instance: T): void {
|
||||
if (this._services.has(identifier)) {
|
||||
const name = typeof identifier === 'symbol' ? identifier.description : identifier.name;
|
||||
logger.warn(`Service ${name} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._services.set(type as ServiceType<IService>, {
|
||||
type: type as ServiceType<IService>,
|
||||
this._services.set(identifier, {
|
||||
identifier,
|
||||
instance: instance as IService,
|
||||
lifetime: ServiceLifetime.Singleton
|
||||
});
|
||||
|
||||
// 如果使用了@Updatable装饰器,添加到可更新列表
|
||||
if (checkUpdatable(type)) {
|
||||
const metadata = getUpdatableMetadata(type);
|
||||
if (typeof identifier !== 'symbol' && checkUpdatable(identifier)) {
|
||||
const metadata = getUpdatableMetadata(identifier);
|
||||
const priority = metadata?.priority ?? 0;
|
||||
this._updatableServices.push({ instance, priority });
|
||||
|
||||
// 按优先级排序(数值越小越先执行)
|
||||
this._updatableServices.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
logger.debug(`Service ${type.name} is updatable (priority: ${priority}), added to update list`);
|
||||
logger.debug(`Service ${identifier.name} is updatable (priority: ${priority}), added to update list`);
|
||||
}
|
||||
|
||||
logger.debug(`Registered service instance: ${type.name}`);
|
||||
const name = typeof identifier === 'symbol' ? identifier.description : identifier.name;
|
||||
logger.debug(`Registered service instance: ${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析服务
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @param identifier - 服务标识符(构造函数或 Symbol)
|
||||
* @returns 服务实例
|
||||
* @throws 如果服务未注册或存在循环依赖
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const timer = container.resolve(TimerManager);
|
||||
*
|
||||
* // 使用 Symbol
|
||||
* const fileSystem = container.resolve(IFileSystem);
|
||||
* ```
|
||||
*/
|
||||
public resolve<T extends IService>(type: ServiceType<T>): T {
|
||||
const registration = this._services.get(type as ServiceType<IService>);
|
||||
public resolve<T extends IService>(identifier: ServiceIdentifier<T>): T {
|
||||
const registration = this._services.get(identifier);
|
||||
const name = typeof identifier === 'symbol' ? identifier.description : identifier.name;
|
||||
|
||||
if (!registration) {
|
||||
throw new Error(`Service ${type.name} is not registered`);
|
||||
throw new Error(`Service ${name} is not registered`);
|
||||
}
|
||||
|
||||
// 检测循环依赖
|
||||
if (this._resolving.has(type as ServiceType<IService>)) {
|
||||
const chain = Array.from(this._resolving).map((t) => t.name).join(' -> ');
|
||||
throw new Error(`Circular dependency detected: ${chain} -> ${type.name}`);
|
||||
if (this._resolving.has(identifier)) {
|
||||
const chain = Array.from(this._resolving).map((t) =>
|
||||
typeof t === 'symbol' ? t.description : t.name
|
||||
).join(' -> ');
|
||||
throw new Error(`Circular dependency detected: ${chain} -> ${name}`);
|
||||
}
|
||||
|
||||
// 如果是单例且已经有实例,直接返回
|
||||
@@ -250,7 +276,7 @@ export class ServiceContainer {
|
||||
}
|
||||
|
||||
// 添加到解析栈
|
||||
this._resolving.add(type as ServiceType<IService>);
|
||||
this._resolving.add(identifier);
|
||||
|
||||
try {
|
||||
// 创建实例
|
||||
@@ -259,9 +285,11 @@ export class ServiceContainer {
|
||||
if (registration.factory) {
|
||||
// 使用工厂函数
|
||||
instance = registration.factory(this);
|
||||
} else {
|
||||
} else if (registration.type) {
|
||||
// 直接构造
|
||||
instance = new (registration.type)();
|
||||
} else {
|
||||
throw new Error(`Service ${name} has no factory or type to construct`);
|
||||
}
|
||||
|
||||
// 如果是单例,缓存实例
|
||||
@@ -269,7 +297,7 @@ export class ServiceContainer {
|
||||
registration.instance = instance;
|
||||
|
||||
// 如果使用了@Updatable装饰器,添加到可更新列表
|
||||
if (checkUpdatable(registration.type)) {
|
||||
if (registration.type && checkUpdatable(registration.type)) {
|
||||
const metadata = getUpdatableMetadata(registration.type);
|
||||
const priority = metadata?.priority ?? 0;
|
||||
this._updatableServices.push({ instance, priority });
|
||||
@@ -277,14 +305,14 @@ export class ServiceContainer {
|
||||
// 按优先级排序(数值越小越先执行)
|
||||
this._updatableServices.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
logger.debug(`Service ${type.name} is updatable (priority: ${priority}), added to update list`);
|
||||
logger.debug(`Service ${name} is updatable (priority: ${priority}), added to update list`);
|
||||
}
|
||||
}
|
||||
|
||||
return instance as T;
|
||||
} finally {
|
||||
// 从解析栈移除
|
||||
this._resolving.delete(type as ServiceType<IService>);
|
||||
this._resolving.delete(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +321,7 @@ export class ServiceContainer {
|
||||
*
|
||||
* 如果服务未注册,返回null而不是抛出异常。
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @param identifier - 服务标识符(构造函数或 Symbol)
|
||||
* @returns 服务实例或null
|
||||
*
|
||||
* @example
|
||||
@@ -304,9 +332,9 @@ export class ServiceContainer {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public tryResolve<T extends IService>(type: ServiceType<T>): T | null {
|
||||
public tryResolve<T extends IService>(identifier: ServiceIdentifier<T>): T | null {
|
||||
try {
|
||||
return this.resolve(type);
|
||||
return this.resolve(identifier);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -315,21 +343,21 @@ export class ServiceContainer {
|
||||
/**
|
||||
* 检查服务是否已注册
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @param identifier - 服务标识符(构造函数或 Symbol)
|
||||
* @returns 是否已注册
|
||||
*/
|
||||
public isRegistered<T extends IService>(type: ServiceType<T>): boolean {
|
||||
return this._services.has(type as ServiceType<IService>);
|
||||
public isRegistered<T extends IService>(identifier: ServiceIdentifier<T>): boolean {
|
||||
return this._services.has(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销服务
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @param identifier - 服务标识符(构造函数或 Symbol)
|
||||
* @returns 是否成功注销
|
||||
*/
|
||||
public unregister<T extends IService>(type: ServiceType<T>): boolean {
|
||||
const registration = this._services.get(type as ServiceType<IService>);
|
||||
public unregister<T extends IService>(identifier: ServiceIdentifier<T>): boolean {
|
||||
const registration = this._services.get(identifier);
|
||||
if (!registration) {
|
||||
return false;
|
||||
}
|
||||
@@ -345,8 +373,9 @@ export class ServiceContainer {
|
||||
registration.instance.dispose();
|
||||
}
|
||||
|
||||
this._services.delete(type as ServiceType<IService>);
|
||||
logger.debug(`Unregistered service: ${type.name}`);
|
||||
this._services.delete(identifier);
|
||||
const name = typeof identifier === 'symbol' ? identifier.description : identifier.name;
|
||||
logger.debug(`Unregistered service: ${name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -367,11 +396,11 @@ export class ServiceContainer {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的服务类型
|
||||
* 获取所有已注册的服务标识符
|
||||
*
|
||||
* @returns 服务类型数组
|
||||
* @returns 服务标识符数组
|
||||
*/
|
||||
public getRegisteredServices(): ServiceType<IService>[] {
|
||||
public getRegisteredServices(): ServiceIdentifier[] {
|
||||
return Array.from(this._services.keys());
|
||||
}
|
||||
|
||||
|
||||
@@ -84,4 +84,31 @@ export abstract class Component implements IComponent {
|
||||
* 虽然保留此方法,但建议将复杂的清理逻辑放在 System 中处理。
|
||||
*/
|
||||
public onRemovedFromEntity(): void {}
|
||||
|
||||
/**
|
||||
* 组件反序列化后的回调
|
||||
*
|
||||
* 当组件从场景文件加载或快照恢复后调用,可以在此方法中恢复运行时数据。
|
||||
*
|
||||
* @remarks
|
||||
* 这是一个生命周期钩子,用于恢复无法序列化的运行时数据。
|
||||
* 例如:从图片路径重新加载图片尺寸信息,重建缓存等。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class TilemapComponent extends Component {
|
||||
* public tilesetImage: string = '';
|
||||
* private _tilesetData: TilesetData | undefined;
|
||||
*
|
||||
* public async onDeserialized(): Promise<void> {
|
||||
* if (this.tilesetImage) {
|
||||
* // 重新加载 tileset 图片并恢复运行时数据
|
||||
* const img = await loadImage(this.tilesetImage);
|
||||
* this.setTilesetInfo(img.width, img.height, ...);
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public onDeserialized(): void | Promise<void> {}
|
||||
}
|
||||
|
||||
@@ -281,6 +281,40 @@ export class SceneSerializer {
|
||||
if (serializedScene.sceneData) {
|
||||
this.deserializeSceneData(serializedScene.sceneData, scene.sceneData);
|
||||
}
|
||||
|
||||
// 调用所有组件的 onDeserialized 生命周期方法
|
||||
// Call onDeserialized lifecycle method on all components
|
||||
const deserializedPromises: Promise<void>[] = [];
|
||||
for (const entity of entities) {
|
||||
this.callOnDeserializedRecursively(entity, deserializedPromises);
|
||||
}
|
||||
|
||||
// 如果有异步的 onDeserialized,在后台执行
|
||||
if (deserializedPromises.length > 0) {
|
||||
Promise.all(deserializedPromises).catch(error => {
|
||||
console.error('Error in onDeserialized:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归调用实体及其子实体所有组件的 onDeserialized 方法
|
||||
*/
|
||||
private static callOnDeserializedRecursively(entity: Entity, promises: Promise<void>[]): void {
|
||||
for (const component of entity.components) {
|
||||
try {
|
||||
const result = component.onDeserialized();
|
||||
if (result instanceof Promise) {
|
||||
promises.push(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error calling onDeserialized on component ${component.constructor.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of entity.children) {
|
||||
this.callOnDeserializedRecursively(child, promises);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface IComponent {
|
||||
onAddedToEntity(): void;
|
||||
/** 组件从实体移除时的回调 */
|
||||
onRemovedFromEntity(): void;
|
||||
/** 组件反序列化后的回调 */
|
||||
onDeserialized(): void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// 核心模块
|
||||
export { Core } from './Core';
|
||||
export { ServiceContainer, ServiceLifetime } from './Core/ServiceContainer';
|
||||
export type { IService, ServiceType } from './Core/ServiceContainer';
|
||||
export type { IService, ServiceType, ServiceIdentifier } from './Core/ServiceContainer';
|
||||
|
||||
// 插件系统
|
||||
export { PluginManager } from './Core/PluginManager';
|
||||
|
||||
304
packages/ecs-engine-bindgen/pnpm-lock.yaml
generated
Normal file
304
packages/ecs-engine-bindgen/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,304 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@esengine/ecs-framework':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
es-engine:
|
||||
specifier: workspace:*
|
||||
version: link:../engine/pkg
|
||||
devDependencies:
|
||||
rimraf:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.10
|
||||
typescript:
|
||||
specifier: ^5.8.0
|
||||
version: 5.9.3
|
||||
|
||||
packages:
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-regex@6.2.2:
|
||||
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@6.2.3:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
brace-expansion@2.0.2:
|
||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
glob@10.5.0:
|
||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||
hasBin: true
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minipass@7.1.2:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
rimraf@5.0.10:
|
||||
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
|
||||
hasBin: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shebang-regex@3.0.0:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
string-width@5.1.2:
|
||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@7.1.2:
|
||||
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
string-width-cjs: string-width@4.2.3
|
||||
strip-ansi: 7.1.2
|
||||
strip-ansi-cjs: strip-ansi@6.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
brace-expansion@2.0.2:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
glob@10.5.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
minimatch@9.0.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
rimraf@5.0.10:
|
||||
dependencies:
|
||||
glob: 10.5.0
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
string-width@5.1.2:
|
||||
dependencies:
|
||||
eastasianwidth: 0.2.0
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
strip-ansi@7.1.2:
|
||||
dependencies:
|
||||
ansi-regex: 6.2.2
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.3
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.1.2
|
||||
@@ -8,6 +8,6 @@
|
||||
export { EngineBridge, EngineBridgeConfig } from './core/EngineBridge';
|
||||
export { RenderBatcher } from './core/RenderBatcher';
|
||||
export { SpriteRenderHelper, ITransformComponent } from './core/SpriteRenderHelper';
|
||||
export { EngineRenderSystem, type TransformComponentType } from './systems/EngineRenderSystem';
|
||||
export { EngineRenderSystem, type TransformComponentType, type IRenderDataProvider, type GizmoDataProviderFn, type HasGizmoProviderFn } from './systems/EngineRenderSystem';
|
||||
export { CameraSystem } from './systems/CameraSystem';
|
||||
export * from './types';
|
||||
|
||||
@@ -10,6 +10,87 @@ import { RenderBatcher } from '../core/RenderBatcher';
|
||||
import type { SpriteRenderData } from '../types';
|
||||
import type { ITransformComponent } from '../core/SpriteRenderHelper';
|
||||
|
||||
/**
|
||||
* Render data from a provider
|
||||
* 提供者的渲染数据
|
||||
*/
|
||||
export interface ProviderRenderData {
|
||||
transforms: Float32Array;
|
||||
textureIds: Uint32Array;
|
||||
uvs: Float32Array;
|
||||
colors: Uint32Array;
|
||||
tileCount: number;
|
||||
/** Sorting order for render ordering | 渲染排序顺序 */
|
||||
sortingOrder: number;
|
||||
/** Texture path for loading (optional, used if textureId is 0) */
|
||||
texturePath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for additional render data providers (e.g., tilemap)
|
||||
* 额外渲染数据提供者接口(如瓦片地图)
|
||||
*/
|
||||
export interface IRenderDataProvider {
|
||||
getRenderData(): readonly ProviderRenderData[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal gizmo color interface (duck-typed, compatible with editor-core GizmoColor)
|
||||
* 内部 gizmo 颜色接口(鸭子类型,与 editor-core GizmoColor 兼容)
|
||||
* @internal
|
||||
*/
|
||||
interface GizmoColorInternal {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal gizmo render data type (duck-typed, compatible with editor-core types)
|
||||
* 内部 gizmo 渲染数据类型(鸭子类型,与 editor-core 类型兼容)
|
||||
* @internal
|
||||
*/
|
||||
interface GizmoRenderDataInternal {
|
||||
type: 'rect' | 'circle' | 'line' | 'grid';
|
||||
color: GizmoColorInternal;
|
||||
// Rect specific
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
rotation?: number;
|
||||
originX?: number;
|
||||
originY?: number;
|
||||
showHandles?: boolean;
|
||||
// Circle specific
|
||||
radius?: number;
|
||||
// Line specific
|
||||
points?: Array<{ x: number; y: number }>;
|
||||
closed?: boolean;
|
||||
// Grid specific
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function type for getting gizmo data from a component.
|
||||
* Used to inject GizmoRegistry functionality from editor layer.
|
||||
* 从组件获取 gizmo 数据的函数类型。
|
||||
* 用于从编辑器层注入 GizmoRegistry 功能。
|
||||
*/
|
||||
export type GizmoDataProviderFn = (
|
||||
component: Component,
|
||||
entity: Entity,
|
||||
isSelected: boolean
|
||||
) => GizmoRenderDataInternal[];
|
||||
|
||||
/**
|
||||
* Function type for checking if a component has gizmo provider.
|
||||
* 检查组件是否有 gizmo 提供者的函数类型。
|
||||
*/
|
||||
export type HasGizmoProviderFn = (component: Component) => boolean;
|
||||
|
||||
/**
|
||||
* Type for transform component constructor.
|
||||
* 变换组件构造函数类型。
|
||||
@@ -55,6 +136,15 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
// 可重用的映射以避免每帧分配
|
||||
private entityRenderMap: Map<number, SpriteRenderData> = new Map();
|
||||
|
||||
// Additional render data providers (e.g., tilemap)
|
||||
// 额外的渲染数据提供者(如瓦片地图)
|
||||
private renderDataProviders: IRenderDataProvider[] = [];
|
||||
|
||||
// Gizmo registry functions (injected from editor layer)
|
||||
// Gizmo 注册表函数(从编辑器层注入)
|
||||
private gizmoDataProvider: GizmoDataProviderFn | null = null;
|
||||
private hasGizmoProvider: HasGizmoProviderFn | null = null;
|
||||
|
||||
/**
|
||||
* Create a new engine render system.
|
||||
* 创建新的引擎渲染系统。
|
||||
@@ -86,7 +176,6 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
* 处理实体之前调用。
|
||||
*/
|
||||
protected override onBegin(): void {
|
||||
|
||||
// Clear the batch | 清空批处理
|
||||
this.batcher.clear();
|
||||
|
||||
@@ -108,6 +197,12 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
// 清空并重用映射用于绘制gizmo
|
||||
this.entityRenderMap.clear();
|
||||
|
||||
// Collect all render items with sorting order
|
||||
// 收集所有渲染项及其排序顺序
|
||||
const renderItems: Array<{ sortingOrder: number; sprites: SpriteRenderData[] }> = [];
|
||||
|
||||
// Collect sprites from entities
|
||||
// 收集实体的 sprites
|
||||
for (const entity of entities) {
|
||||
const sprite = entity.getComponent(SpriteComponent);
|
||||
const transform = entity.getComponent(this.transformType) as unknown as ITransformComponent | null;
|
||||
@@ -159,35 +254,76 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
color
|
||||
};
|
||||
|
||||
this.batcher.addSprite(renderData);
|
||||
renderItems.push({ sortingOrder: sprite.sortingOrder, sprites: [renderData] });
|
||||
this.entityRenderMap.set(entity.id, renderData);
|
||||
}
|
||||
|
||||
// Submit batch and render at the end of process | 在process结束时提交批处理并渲染
|
||||
// Collect render data from providers (e.g., tilemap)
|
||||
// 收集来自提供者的渲染数据(如瓦片地图)
|
||||
for (const provider of this.renderDataProviders) {
|
||||
const renderDataList = provider.getRenderData();
|
||||
for (const data of renderDataList) {
|
||||
// Get texture ID - load from path if needed
|
||||
let textureId = data.textureIds[0] || 0;
|
||||
if (textureId === 0 && data.texturePath) {
|
||||
textureId = this.bridge.getOrLoadTextureByPath(data.texturePath);
|
||||
}
|
||||
|
||||
// Convert tilemap render data to sprites
|
||||
const tilemapSprites: SpriteRenderData[] = [];
|
||||
for (let i = 0; i < data.tileCount; i++) {
|
||||
const tOffset = i * 7;
|
||||
const uvOffset = i * 4;
|
||||
|
||||
const renderData: SpriteRenderData = {
|
||||
x: data.transforms[tOffset],
|
||||
y: data.transforms[tOffset + 1],
|
||||
rotation: data.transforms[tOffset + 2],
|
||||
scaleX: data.transforms[tOffset + 3],
|
||||
scaleY: data.transforms[tOffset + 4],
|
||||
originX: data.transforms[tOffset + 5],
|
||||
originY: data.transforms[tOffset + 6],
|
||||
textureId,
|
||||
uv: [data.uvs[uvOffset], data.uvs[uvOffset + 1], data.uvs[uvOffset + 2], data.uvs[uvOffset + 3]],
|
||||
color: data.colors[i]
|
||||
};
|
||||
|
||||
tilemapSprites.push(renderData);
|
||||
}
|
||||
|
||||
if (tilemapSprites.length > 0) {
|
||||
renderItems.push({ sortingOrder: data.sortingOrder, sprites: tilemapSprites });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by sortingOrder (lower values render first, appear behind)
|
||||
// 按 sortingOrder 排序(值越小越先渲染,显示在后面)
|
||||
renderItems.sort((a, b) => a.sortingOrder - b.sortingOrder);
|
||||
|
||||
// Submit all sprites in sorted order
|
||||
// 按排序顺序提交所有 sprites
|
||||
for (const item of renderItems) {
|
||||
for (const sprite of item.sprites) {
|
||||
this.batcher.addSprite(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.batcher.isEmpty) {
|
||||
const sprites = this.batcher.getSprites();
|
||||
this.bridge.submitSprites(sprites);
|
||||
}
|
||||
|
||||
// Draw gizmos for all entities with IGizmoProvider components
|
||||
// 为所有具有 IGizmoProvider 组件的实体绘制 Gizmo
|
||||
if (this.showGizmos) {
|
||||
this.drawComponentGizmos();
|
||||
}
|
||||
|
||||
// Draw gizmos for selected entities (always, even if no sprites)
|
||||
// 为选中的实体绘制Gizmo(始终绘制,即使没有精灵)
|
||||
if (this.showGizmos && this.selectedEntityIds.size > 0) {
|
||||
for (const entityId of this.selectedEntityIds) {
|
||||
const renderData = this.entityRenderMap.get(entityId);
|
||||
if (renderData) {
|
||||
this.bridge.addGizmoRect(
|
||||
renderData.x,
|
||||
renderData.y,
|
||||
renderData.scaleX,
|
||||
renderData.scaleY,
|
||||
renderData.rotation,
|
||||
renderData.originX,
|
||||
renderData.originY,
|
||||
0.0, 1.0, 0.5, 1.0, // Green color | 绿色
|
||||
true // Show transform handles for selection gizmo
|
||||
);
|
||||
}
|
||||
}
|
||||
this.drawSelectedEntityGizmos();
|
||||
}
|
||||
|
||||
// Draw camera frustum gizmos
|
||||
@@ -199,6 +335,296 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
this.bridge.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw gizmos from components that have registered gizmo providers.
|
||||
* 绘制已注册 gizmo 提供者的组件的 gizmo。
|
||||
*/
|
||||
private drawComponentGizmos(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene || !this.gizmoDataProvider || !this.hasGizmoProvider) return;
|
||||
|
||||
// Iterate all entities in the scene
|
||||
// 遍历场景中的所有实体
|
||||
for (const entity of scene.entities.buffer) {
|
||||
const isSelected = this.selectedEntityIds.has(entity.id);
|
||||
|
||||
// Check each component for gizmo provider
|
||||
// 检查每个组件是否有 gizmo 提供者
|
||||
for (const component of entity.components) {
|
||||
if (this.hasGizmoProvider(component)) {
|
||||
try {
|
||||
const gizmoDataArray = this.gizmoDataProvider(component, entity, isSelected);
|
||||
for (const gizmoData of gizmoDataArray) {
|
||||
this.renderGizmoData(gizmoData);
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently ignore errors from gizmo providers
|
||||
// 静默忽略 gizmo 提供者的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single gizmo data item.
|
||||
* 渲染单个 gizmo 数据项。
|
||||
*/
|
||||
private renderGizmoData(data: GizmoRenderDataInternal): void {
|
||||
const { r, g, b, a } = data.color;
|
||||
|
||||
switch (data.type) {
|
||||
case 'rect':
|
||||
if (data.x !== undefined && data.y !== undefined &&
|
||||
data.width !== undefined && data.height !== undefined) {
|
||||
this.bridge.addGizmoRect(
|
||||
data.x,
|
||||
data.y,
|
||||
data.width,
|
||||
data.height,
|
||||
data.rotation ?? 0,
|
||||
data.originX ?? 0.5,
|
||||
data.originY ?? 0.5,
|
||||
r, g, b, a,
|
||||
data.showHandles ?? false
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'grid':
|
||||
// Render grid as multiple line segments
|
||||
// 将网格渲染为多条线段
|
||||
if (data.x !== undefined && data.y !== undefined &&
|
||||
data.width !== undefined && data.height !== undefined &&
|
||||
data.cols !== undefined && data.rows !== undefined) {
|
||||
this.renderGridGizmo(data.x, data.y, data.width, data.height, data.cols, data.rows, r, g, b, a);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'line':
|
||||
// Lines are rendered as connected rect segments (thin)
|
||||
// 线条渲染为连接的细矩形段
|
||||
if (data.points && data.points.length >= 2) {
|
||||
this.renderLineGizmo(data.points, data.closed ?? false, r, g, b, a);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'circle':
|
||||
// Circle rendered as polygon approximation
|
||||
// 圆形渲染为多边形近似
|
||||
if (data.x !== undefined && data.y !== undefined && data.radius !== undefined) {
|
||||
this.renderCircleGizmo(data.x, data.y, data.radius, r, g, b, a);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a grid gizmo using line segments.
|
||||
* 使用线段渲染网格 gizmo。
|
||||
*/
|
||||
private renderGridGizmo(
|
||||
x: number, y: number, width: number, height: number,
|
||||
cols: number, rows: number,
|
||||
r: number, g: number, b: number, a: number
|
||||
): void {
|
||||
const cellWidth = width / cols;
|
||||
const cellHeight = height / rows;
|
||||
const lineThickness = 1;
|
||||
|
||||
// Vertical lines | 垂直线
|
||||
for (let col = 0; col <= cols; col++) {
|
||||
const lineX = x + col * cellWidth;
|
||||
this.bridge.addGizmoRect(
|
||||
lineX, y + height / 2,
|
||||
lineThickness, height,
|
||||
0, 0.5, 0.5,
|
||||
r, g, b, a,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Horizontal lines | 水平线
|
||||
for (let row = 0; row <= rows; row++) {
|
||||
const lineY = y + row * cellHeight;
|
||||
this.bridge.addGizmoRect(
|
||||
x + width / 2, lineY,
|
||||
width, lineThickness,
|
||||
0, 0.5, 0.5,
|
||||
r, g, b, a,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a line gizmo.
|
||||
* 渲染线条 gizmo。
|
||||
*/
|
||||
private renderLineGizmo(
|
||||
points: Array<{ x: number; y: number }>,
|
||||
closed: boolean,
|
||||
r: number, g: number, b: number, a: number
|
||||
): void {
|
||||
const lineThickness = 2;
|
||||
const count = closed ? points.length : points.length - 1;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const p1 = points[i];
|
||||
const p2 = points[(i + 1) % points.length];
|
||||
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
const angle = Math.atan2(dy, dx);
|
||||
|
||||
// Draw line segment as thin rect
|
||||
// 将线段绘制为细矩形
|
||||
this.bridge.addGizmoRect(
|
||||
(p1.x + p2.x) / 2,
|
||||
(p1.y + p2.y) / 2,
|
||||
length, lineThickness,
|
||||
angle, 0.5, 0.5,
|
||||
r, g, b, a,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a circle gizmo as polygon.
|
||||
* 将圆形 gizmo 渲染为多边形。
|
||||
*/
|
||||
private renderCircleGizmo(
|
||||
x: number, y: number, radius: number,
|
||||
r: number, g: number, b: number, a: number
|
||||
): void {
|
||||
const segments = 32;
|
||||
const points: Array<{ x: number; y: number }> = [];
|
||||
|
||||
for (let i = 0; i < segments; i++) {
|
||||
const angle = (i / segments) * Math.PI * 2;
|
||||
points.push({
|
||||
x: x + Math.cos(angle) * radius,
|
||||
y: y + Math.sin(angle) * radius
|
||||
});
|
||||
}
|
||||
|
||||
this.renderLineGizmo(points, true, r, g, b, a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw gizmos for selected entities with transform handles.
|
||||
* 为选中的实体绘制带有变换手柄的 gizmo。
|
||||
*
|
||||
* This method ensures that selected entities show transform handles
|
||||
* regardless of whether they have sprite data in entityRenderMap.
|
||||
* 此方法确保选中的实体显示变换手柄,无论它们是否在 entityRenderMap 中有精灵数据。
|
||||
*/
|
||||
private drawSelectedEntityGizmos(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene) return;
|
||||
|
||||
// Determine if we should show handles based on transform mode
|
||||
// 根据变换模式确定是否显示手柄
|
||||
const shouldShowHandles = this.transformMode !== 'select';
|
||||
|
||||
for (const entityId of this.selectedEntityIds) {
|
||||
// Find the entity
|
||||
// 查找实体
|
||||
const entity = scene.entities.findEntityById(entityId);
|
||||
if (!entity) continue;
|
||||
|
||||
// Get transform component
|
||||
// 获取变换组件
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
if (!transform) continue;
|
||||
|
||||
// First check if we have sprite data from entityRenderMap
|
||||
// 首先检查是否有来自 entityRenderMap 的精灵数据
|
||||
const spriteData = this.entityRenderMap.get(entityId);
|
||||
if (spriteData) {
|
||||
// Use sprite data for selection gizmo
|
||||
// 使用精灵数据绘制选择 gizmo
|
||||
this.bridge.addGizmoRect(
|
||||
spriteData.x,
|
||||
spriteData.y,
|
||||
spriteData.scaleX,
|
||||
spriteData.scaleY,
|
||||
spriteData.rotation,
|
||||
spriteData.originX,
|
||||
spriteData.originY,
|
||||
0.0, 0.8, 1.0, 1.0, // Selection color (cyan)
|
||||
shouldShowHandles
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// For entities without sprite data, try to get gizmo data from components via registry
|
||||
// 对于没有精灵数据的实体,尝试通过注册表从组件获取 gizmo 数据
|
||||
let foundGizmo = false;
|
||||
if (this.gizmoDataProvider && this.hasGizmoProvider) {
|
||||
for (const component of entity.components) {
|
||||
if (this.hasGizmoProvider(component)) {
|
||||
try {
|
||||
const gizmoDataArray = this.gizmoDataProvider(component, entity, true);
|
||||
// Use the first rect gizmo for selection handles
|
||||
// 使用第一个矩形 gizmo 来绘制选择手柄
|
||||
for (const gizmoData of gizmoDataArray) {
|
||||
if (gizmoData.type === 'rect' &&
|
||||
gizmoData.x !== undefined && gizmoData.y !== undefined &&
|
||||
gizmoData.width !== undefined && gizmoData.height !== undefined) {
|
||||
|
||||
// Draw selection gizmo with handles
|
||||
// 绘制带手柄的选择 gizmo
|
||||
this.bridge.addGizmoRect(
|
||||
gizmoData.x,
|
||||
gizmoData.y,
|
||||
gizmoData.width,
|
||||
gizmoData.height,
|
||||
gizmoData.rotation ?? 0,
|
||||
gizmoData.originX ?? 0.5,
|
||||
gizmoData.originY ?? 0.5,
|
||||
0.0, 0.8, 1.0, 1.0, // Selection color (cyan)
|
||||
shouldShowHandles
|
||||
);
|
||||
foundGizmo = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundGizmo) break;
|
||||
} catch (e) {
|
||||
// Silently ignore errors
|
||||
// 静默忽略错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no gizmo provider found, draw a default gizmo at transform position
|
||||
// 如果没有找到 gizmo 提供者,在变换位置绘制默认 gizmo
|
||||
if (!foundGizmo) {
|
||||
const rotation = typeof transform.rotation === 'number'
|
||||
? transform.rotation
|
||||
: transform.rotation.z;
|
||||
|
||||
// Draw a small default gizmo at entity position
|
||||
// 在实体位置绘制一个小的默认 gizmo
|
||||
this.bridge.addGizmoRect(
|
||||
transform.position.x,
|
||||
transform.position.y,
|
||||
32, // Default size
|
||||
32,
|
||||
rotation,
|
||||
0.5,
|
||||
0.5,
|
||||
0.0, 0.8, 1.0, 1.0, // Selection color (cyan)
|
||||
shouldShowHandles
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw camera frustum gizmos for all cameras in scene.
|
||||
* 为场景中所有相机绘制视锥体 gizmo。
|
||||
@@ -249,6 +675,26 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set gizmo registry functions.
|
||||
* 设置 gizmo 注册表函数。
|
||||
*
|
||||
* This allows the editor layer to inject GizmoRegistry functionality
|
||||
* without creating a direct dependency from engine to editor.
|
||||
* 这允许编辑器层注入 GizmoRegistry 功能,
|
||||
* 而不会创建从引擎到编辑器的直接依赖。
|
||||
*
|
||||
* @param provider - Function to get gizmo data from a component
|
||||
* @param hasProvider - Function to check if a component has a gizmo provider
|
||||
*/
|
||||
setGizmoRegistry(
|
||||
provider: GizmoDataProviderFn,
|
||||
hasProvider: HasGizmoProviderFn
|
||||
): void {
|
||||
this.gizmoDataProvider = provider;
|
||||
this.hasGizmoProvider = hasProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set gizmo visibility.
|
||||
* 设置Gizmo可见性。
|
||||
@@ -331,6 +777,27 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register a render data provider.
|
||||
* 注册渲染数据提供者。
|
||||
*/
|
||||
addRenderDataProvider(provider: IRenderDataProvider): void {
|
||||
if (!this.renderDataProviders.includes(provider)) {
|
||||
this.renderDataProviders.push(provider);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a render data provider.
|
||||
* 移除渲染数据提供者。
|
||||
*/
|
||||
removeRenderDataProvider(provider: IRenderDataProvider): void {
|
||||
const index = this.renderDataProviders.indexOf(provider);
|
||||
if (index >= 0) {
|
||||
this.renderDataProviders.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of sprites rendered.
|
||||
* 获取渲染的精灵数量。
|
||||
|
||||
310
packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts
vendored
Normal file
310
packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts
vendored
Normal file
@@ -0,0 +1,310 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Initialize panic hook for better error messages in console.
|
||||
* 初始化panic hook以在控制台显示更好的错误信息。
|
||||
*/
|
||||
export function init(): void;
|
||||
/**
|
||||
* Game engine main interface exposed to JavaScript.
|
||||
* 暴露给JavaScript的游戏引擎主接口。
|
||||
*
|
||||
* This is the primary entry point for the engine from TypeScript/JavaScript.
|
||||
* 这是从TypeScript/JavaScript访问引擎的主要入口点。
|
||||
*/
|
||||
export class GameEngine {
|
||||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
/**
|
||||
* Get camera state.
|
||||
* 获取相机状态。
|
||||
*
|
||||
* # Returns | 返回
|
||||
* Array of [x, y, zoom, rotation] | 数组 [x, y, zoom, rotation]
|
||||
*/
|
||||
getCamera(): Float32Array;
|
||||
/**
|
||||
* Set camera position, zoom, and rotation.
|
||||
* 设置相机位置、缩放和旋转。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `x` - Camera X position | 相机X位置
|
||||
* * `y` - Camera Y position | 相机Y位置
|
||||
* * `zoom` - Zoom level | 缩放级别
|
||||
* * `rotation` - Rotation in radians | 旋转角度(弧度)
|
||||
*/
|
||||
setCamera(x: number, y: number, zoom: number, rotation: number): void;
|
||||
/**
|
||||
* Check if a key is currently pressed.
|
||||
* 检查某个键是否当前被按下。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `key_code` - The key code to check | 要检查的键码
|
||||
*/
|
||||
isKeyDown(key_code: string): boolean;
|
||||
/**
|
||||
* Load a texture from URL.
|
||||
* 从URL加载纹理。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `id` - Unique texture identifier | 唯一纹理标识符
|
||||
* * `url` - Image URL to load | 要加载的图片URL
|
||||
*/
|
||||
loadTexture(id: number, url: string): void;
|
||||
/**
|
||||
* Update input state. Should be called once per frame.
|
||||
* 更新输入状态。应该每帧调用一次。
|
||||
*/
|
||||
updateInput(): void;
|
||||
/**
|
||||
* Create a new game engine from external WebGL context.
|
||||
* 从外部 WebGL 上下文创建引擎。
|
||||
*
|
||||
* This is designed for WeChat MiniGame and similar environments.
|
||||
* 适用于微信小游戏等环境。
|
||||
*/
|
||||
static fromExternal(gl_context: any, width: number, height: number): GameEngine;
|
||||
/**
|
||||
* Set grid visibility.
|
||||
* 设置网格可见性。
|
||||
*/
|
||||
setShowGrid(show: boolean): void;
|
||||
/**
|
||||
* Add a rectangle gizmo outline.
|
||||
* 添加矩形Gizmo边框。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `x` - Center X position | 中心X位置
|
||||
* * `y` - Center Y position | 中心Y位置
|
||||
* * `width` - Rectangle width | 矩形宽度
|
||||
* * `height` - Rectangle height | 矩形高度
|
||||
* * `rotation` - Rotation in radians | 旋转角度(弧度)
|
||||
* * `origin_x` - Origin X (0-1) | 原点X (0-1)
|
||||
* * `origin_y` - Origin Y (0-1) | 原点Y (0-1)
|
||||
* * `r`, `g`, `b`, `a` - Color (0.0-1.0) | 颜色
|
||||
* * `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;
|
||||
/**
|
||||
* Resize a specific viewport.
|
||||
* 调整特定视口大小。
|
||||
*/
|
||||
resizeViewport(viewport_id: string, width: number, height: number): void;
|
||||
/**
|
||||
* Set clear color (background color).
|
||||
* 设置清除颜色(背景颜色)。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `r`, `g`, `b`, `a` - Color components (0.0-1.0) | 颜色分量 (0.0-1.0)
|
||||
*/
|
||||
setClearColor(r: number, g: number, b: number, a: number): void;
|
||||
/**
|
||||
* Set gizmo visibility.
|
||||
* 设置辅助工具可见性。
|
||||
*/
|
||||
setShowGizmos(show: boolean): void;
|
||||
/**
|
||||
* Get all registered viewport IDs.
|
||||
* 获取所有已注册的视口ID。
|
||||
*/
|
||||
getViewportIds(): string[];
|
||||
/**
|
||||
* Register a new viewport.
|
||||
* 注册新视口。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `id` - Unique viewport identifier | 唯一视口标识符
|
||||
* * `canvas_id` - HTML canvas element ID | HTML canvas元素ID
|
||||
*/
|
||||
registerViewport(id: string, canvas_id: string): void;
|
||||
/**
|
||||
* Render to a specific viewport.
|
||||
* 渲染到特定视口。
|
||||
*/
|
||||
renderToViewport(viewport_id: string): void;
|
||||
/**
|
||||
* Set transform tool mode.
|
||||
* 设置变换工具模式。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `mode` - 0=Select, 1=Move, 2=Rotate, 3=Scale
|
||||
*/
|
||||
setTransformMode(mode: number): void;
|
||||
/**
|
||||
* Get or load texture by path.
|
||||
* 按路径获取或加载纹理。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `path` - Image path/URL | 图片路径/URL
|
||||
*/
|
||||
getOrLoadTextureByPath(path: string): number;
|
||||
/**
|
||||
* Get camera for a specific viewport.
|
||||
* 获取特定视口的相机。
|
||||
*/
|
||||
getViewportCamera(viewport_id: string): Float32Array | undefined;
|
||||
/**
|
||||
* Set the active viewport.
|
||||
* 设置活动视口。
|
||||
*/
|
||||
setActiveViewport(id: string): boolean;
|
||||
/**
|
||||
* Set camera for a specific viewport.
|
||||
* 为特定视口设置相机。
|
||||
*/
|
||||
setViewportCamera(viewport_id: string, x: number, y: number, zoom: number, rotation: number): void;
|
||||
/**
|
||||
* Set viewport configuration.
|
||||
* 设置视口配置。
|
||||
*/
|
||||
setViewportConfig(viewport_id: string, show_grid: boolean, show_gizmos: boolean): void;
|
||||
/**
|
||||
* Submit sprite batch data for rendering.
|
||||
* 提交精灵批次数据进行渲染。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `transforms` - Float32Array [x, y, rotation, scaleX, scaleY, originX, originY] per sprite
|
||||
* 每个精灵的变换数据
|
||||
* * `texture_ids` - Uint32Array of texture IDs | 纹理ID数组
|
||||
* * `uvs` - Float32Array [u0, v0, u1, v1] per sprite | 每个精灵的UV坐标
|
||||
* * `colors` - Uint32Array of packed RGBA colors | 打包的RGBA颜色数组
|
||||
*/
|
||||
submitSpriteBatch(transforms: Float32Array, texture_ids: Uint32Array, uvs: Float32Array, colors: Uint32Array): void;
|
||||
/**
|
||||
* Unregister a viewport.
|
||||
* 注销视口。
|
||||
*/
|
||||
unregisterViewport(id: string): void;
|
||||
/**
|
||||
* Load texture by path, returning texture ID.
|
||||
* 按路径加载纹理,返回纹理ID。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `path` - Image path/URL to load | 要加载的图片路径/URL
|
||||
*/
|
||||
loadTextureByPath(path: string): number;
|
||||
/**
|
||||
* Get texture ID by path.
|
||||
* 按路径获取纹理ID。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `path` - Image path to lookup | 要查找的图片路径
|
||||
*/
|
||||
getTextureIdByPath(path: string): number | undefined;
|
||||
/**
|
||||
* Create a new game engine instance.
|
||||
* 创建新的游戏引擎实例。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `canvas_id` - The HTML canvas element ID | HTML canvas元素ID
|
||||
*
|
||||
* # Returns | 返回
|
||||
* A new GameEngine instance or an error | 新的GameEngine实例或错误
|
||||
*/
|
||||
constructor(canvas_id: string);
|
||||
/**
|
||||
* Clear the screen with specified color.
|
||||
* 使用指定颜色清除屏幕。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `r` - Red component (0.0-1.0) | 红色分量
|
||||
* * `g` - Green component (0.0-1.0) | 绿色分量
|
||||
* * `b` - Blue component (0.0-1.0) | 蓝色分量
|
||||
* * `a` - Alpha component (0.0-1.0) | 透明度分量
|
||||
*/
|
||||
clear(r: number, g: number, b: number, a: number): void;
|
||||
/**
|
||||
* Render the current frame.
|
||||
* 渲染当前帧。
|
||||
*/
|
||||
render(): void;
|
||||
/**
|
||||
* Resize viewport.
|
||||
* 调整视口大小。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `width` - New viewport width | 新视口宽度
|
||||
* * `height` - New viewport height | 新视口高度
|
||||
*/
|
||||
resize(width: number, height: number): void;
|
||||
/**
|
||||
* Get canvas width.
|
||||
* 获取画布宽度。
|
||||
*/
|
||||
readonly width: number;
|
||||
/**
|
||||
* Get canvas height.
|
||||
* 获取画布高度。
|
||||
*/
|
||||
readonly height: number;
|
||||
}
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly __wbg_gameengine_free: (a: number, b: 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_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_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_render: (a: number) => [number, number];
|
||||
readonly gameengine_renderToViewport: (a: number, b: number, c: number) => [number, number];
|
||||
readonly gameengine_resize: (a: number, b: number, c: number) => void;
|
||||
readonly gameengine_resizeViewport: (a: number, b: number, c: number, d: number, e: number) => void;
|
||||
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_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_unregisterViewport: (a: number, b: number, c: number) => void;
|
||||
readonly gameengine_updateInput: (a: number) => void;
|
||||
readonly gameengine_width: (a: number) => number;
|
||||
readonly init: () => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__hdbeb4a641c76f980: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen__closure__destroy__h201da39d82f7cf6e: (a: number, b: number) => void;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
readonly __externref_table_alloc: () => number;
|
||||
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly __externref_drop_slice: (a: number, b: number) => void;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||
@@ -9,6 +9,7 @@
|
||||
"outDir": "./bin",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ECS Framework Editor</title>
|
||||
<!-- ES Module Shims: 为不支持 Import Maps 的浏览器提供 polyfill -->
|
||||
<script async src="https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<!-- Import Map 将由 PluginLoader 在运行时动态注入 -->
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,20 +5,21 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"build:sdk": "cd ../editor-runtime && pnpm build",
|
||||
"build": "npm run build:sdk && tsc && vite build",
|
||||
"build:watch": "vite build --watch",
|
||||
"tauri": "tauri",
|
||||
"kill-dev": "node scripts/kill-dev-server.js",
|
||||
"tauri:dev": "npm run kill-dev && tauri dev",
|
||||
"tauri:dev": "npm run build:sdk && tauri dev",
|
||||
"bundle:runtime": "node scripts/bundle-runtime.mjs",
|
||||
"tauri:build": "npm run bundle:runtime && tauri build",
|
||||
"tauri:build": "npm run build:sdk && npm run bundle:runtime && tauri build",
|
||||
"version": "node scripts/sync-version.js && git add src-tauri/tauri.conf.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@esengine/asset-system": "workspace:*",
|
||||
"@esengine/behavior-tree": "workspace:*",
|
||||
"@esengine/ecs-components": "workspace:*",
|
||||
"@esengine/tilemap": "workspace:*",
|
||||
"@esengine/tilemap-editor": "workspace:*",
|
||||
"@esengine/ecs-engine-bindgen": "workspace:*",
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/editor-core": "workspace:*",
|
||||
|
||||
3127
packages/editor-app/pnpm-lock.yaml
generated
Normal file
3127
packages/editor-app/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
packages/editor-app/public/assets/react-dom-shim.js
vendored
Normal file
15
packages/editor-app/public/assets/react-dom-shim.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
// React DOM shim - 从全局变量导出 ReactDOM
|
||||
const ReactDOM = window.ReactDOM;
|
||||
export default ReactDOM;
|
||||
export const {
|
||||
createPortal,
|
||||
flushSync,
|
||||
hydrate,
|
||||
render,
|
||||
unmountComponentAtNode,
|
||||
unstable_batchedUpdates,
|
||||
unstable_renderSubtreeIntoContainer,
|
||||
version,
|
||||
createRoot,
|
||||
hydrateRoot
|
||||
} = ReactDOM;
|
||||
4
packages/editor-app/public/assets/react-jsx-runtime-shim.js
vendored
Normal file
4
packages/editor-app/public/assets/react-jsx-runtime-shim.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
// React JSX Runtime shim - 从全局变量导出
|
||||
const ReactJSXRuntime = window.ReactJSXRuntime;
|
||||
export const { jsx, jsxs, Fragment } = ReactJSXRuntime;
|
||||
export default ReactJSXRuntime;
|
||||
40
packages/editor-app/public/assets/react-shim.js
vendored
Normal file
40
packages/editor-app/public/assets/react-shim.js
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
// React shim - 从全局变量导出 React
|
||||
// 这个文件用于 Import Map,让插件的 import 'react' 能正确解析到主应用的 React
|
||||
const React = window.React;
|
||||
export default React;
|
||||
export const {
|
||||
Children,
|
||||
Component,
|
||||
Fragment,
|
||||
Profiler,
|
||||
PureComponent,
|
||||
StrictMode,
|
||||
Suspense,
|
||||
cloneElement,
|
||||
createContext,
|
||||
createElement,
|
||||
createFactory,
|
||||
createRef,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
lazy,
|
||||
memo,
|
||||
startTransition,
|
||||
unstable_act,
|
||||
useCallback,
|
||||
useContext,
|
||||
useDebugValue,
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
useId,
|
||||
useImperativeHandle,
|
||||
useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
useTransition,
|
||||
version
|
||||
} = React;
|
||||
@@ -36,10 +36,10 @@ pub async fn build_plugin(plugin_folder: String, app: AppHandle) -> Result<Strin
|
||||
.map_err(|e| format!("Failed to create .build-cache directory: {}", e))?;
|
||||
}
|
||||
|
||||
let npm_command = if cfg!(target_os = "windows") {
|
||||
"npm.cmd"
|
||||
let pnpm_command = if cfg!(target_os = "windows") {
|
||||
"pnpm.cmd"
|
||||
} else {
|
||||
"npm"
|
||||
"pnpm"
|
||||
};
|
||||
|
||||
// Step 1: Install dependencies
|
||||
@@ -52,15 +52,15 @@ pub async fn build_plugin(plugin_folder: String, app: AppHandle) -> Result<Strin
|
||||
)
|
||||
.ok();
|
||||
|
||||
let install_output = Command::new(npm_command)
|
||||
let install_output = Command::new(&pnpm_command)
|
||||
.args(["install"])
|
||||
.current_dir(&plugin_folder)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run npm install: {}", e))?;
|
||||
.map_err(|e| format!("Failed to run pnpm install: {}", e))?;
|
||||
|
||||
if !install_output.status.success() {
|
||||
return Err(format!(
|
||||
"npm install failed: {}",
|
||||
"pnpm install failed: {}",
|
||||
String::from_utf8_lossy(&install_output.stderr)
|
||||
));
|
||||
}
|
||||
@@ -75,15 +75,15 @@ pub async fn build_plugin(plugin_folder: String, app: AppHandle) -> Result<Strin
|
||||
)
|
||||
.ok();
|
||||
|
||||
let build_output = Command::new(npm_command)
|
||||
let build_output = Command::new(&pnpm_command)
|
||||
.args(["run", "build"])
|
||||
.current_dir(&plugin_folder)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run npm run build: {}", e))?;
|
||||
.map_err(|e| format!("Failed to run pnpm run build: {}", e))?;
|
||||
|
||||
if !build_output.status.success() {
|
||||
return Err(format!(
|
||||
"npm run build failed: {}",
|
||||
"pnpm run build failed: {}",
|
||||
String::from_utf8_lossy(&build_output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -101,6 +101,10 @@ fn handle_project_protocol(
|
||||
let uri = request.uri();
|
||||
let path = uri.path();
|
||||
|
||||
// Debug logging
|
||||
println!("[project://] Full URI: {}", uri);
|
||||
println!("[project://] Path: {}", path);
|
||||
|
||||
let file_path = {
|
||||
let paths = match project_paths.lock() {
|
||||
Ok(p) => p,
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"version": "1.0.8",
|
||||
"identifier": "com.esengine.editor",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "npm run build:watch",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
@@ -67,7 +66,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null,
|
||||
"csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval' tauri: project: asset: https: http: data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' tauri: project: asset: https: http: blob:; style-src 'self' 'unsafe-inline' tauri: https: http:; img-src 'self' tauri: project: asset: https: http: data: blob:; connect-src 'self' tauri: project: asset: https: http: ws: wss:",
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": {
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import * as ReactJSXRuntime from 'react/jsx-runtime';
|
||||
import { Core, createLogger, Scene } from '@esengine/ecs-framework';
|
||||
import * as ECSFramework from '@esengine/ecs-framework';
|
||||
|
||||
// 将 React 暴露到全局,供动态加载的插件使用
|
||||
// editor-runtime.js 将 React 设为 external,需要从全局获取
|
||||
(window as any).React = React;
|
||||
(window as any).ReactDOM = ReactDOM;
|
||||
(window as any).ReactJSXRuntime = ReactJSXRuntime;
|
||||
import {
|
||||
EditorPluginManager,
|
||||
UIRegistry,
|
||||
@@ -13,6 +21,7 @@ import {
|
||||
SceneManagerService,
|
||||
ProjectService,
|
||||
CompilerRegistry,
|
||||
ICompilerRegistry,
|
||||
InspectorRegistry,
|
||||
INotification,
|
||||
CommandManager
|
||||
@@ -64,6 +73,11 @@ Core.services.registerInstance(LocaleService, localeService);
|
||||
Core.services.registerSingleton(GlobalBlackboardService);
|
||||
Core.services.registerSingleton(CompilerRegistry);
|
||||
|
||||
// 在 CompilerRegistry 实例化后,也用 Symbol 注册,用于跨包插件访问
|
||||
// 注意:registerSingleton 会延迟实例化,所以需要在第一次使用后再注册 Symbol
|
||||
const compilerRegistryInstance = Core.services.resolve(CompilerRegistry);
|
||||
Core.services.registerInstance(ICompilerRegistry, compilerRegistryInstance);
|
||||
|
||||
const logger = createLogger('App');
|
||||
|
||||
function App() {
|
||||
@@ -368,34 +382,17 @@ function App() {
|
||||
|
||||
await projectService.openProject(projectPath);
|
||||
|
||||
await fetch('/@user-project-set-path', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: projectPath })
|
||||
});
|
||||
// 设置 Tauri project:// 协议的基础路径(用于加载插件等项目文件)
|
||||
await TauriAPI.setProjectBasePath(projectPath);
|
||||
|
||||
setStatus(t('header.status.projectOpened'));
|
||||
|
||||
setLoadingMessage(locale === 'zh' ? '步骤 2/2: 加载场景...' : 'Step 2/2: Loading scene...');
|
||||
setLoadingMessage(locale === 'zh' ? '步骤 2/2: 初始化场景...' : 'Step 2/2: Initializing scene...');
|
||||
|
||||
const sceneManagerService = Core.services.resolve(SceneManagerService);
|
||||
const scenesPath = projectService.getScenesPath();
|
||||
if (scenesPath && sceneManagerService) {
|
||||
try {
|
||||
const sceneFiles = await TauriAPI.scanDirectory(scenesPath, '*.ecs');
|
||||
|
||||
if (sceneFiles.length > 0) {
|
||||
const defaultScenePath = projectService.getDefaultScenePath();
|
||||
const sceneToLoad = sceneFiles.find((f) => f === defaultScenePath) || sceneFiles[0];
|
||||
|
||||
await sceneManagerService.openScene(sceneToLoad);
|
||||
} else {
|
||||
if (sceneManagerService) {
|
||||
await sceneManagerService.newScene();
|
||||
}
|
||||
} catch {
|
||||
await sceneManagerService.newScene();
|
||||
}
|
||||
}
|
||||
|
||||
const settings = SettingsService.getInstance();
|
||||
settings.addRecentProject(projectPath);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { invoke, convertFileSrc } from '@tauri-apps/api/core';
|
||||
|
||||
/**
|
||||
* 文件过滤器定义
|
||||
@@ -298,6 +298,19 @@ export class TauriAPI {
|
||||
static async generateQRCode(text: string): Promise<string> {
|
||||
return await invoke<string>('generate_qrcode', { text });
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本地文件路径转换为 Tauri 可访问的 asset URL
|
||||
* @param filePath 本地文件路径
|
||||
* @param protocol 协议类型 (默认: 'asset')
|
||||
* @returns 转换后的 URL,可用于 img src、audio src 等
|
||||
* @example
|
||||
* const url = TauriAPI.convertFileSrc('C:\\Users\\...\\image.png');
|
||||
* // 返回: 'https://asset.localhost/C:/Users/.../image.png'
|
||||
*/
|
||||
static convertFileSrc(filePath: string, protocol?: string): string {
|
||||
return convertFileSrc(filePath, protocol);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DirectoryEntry {
|
||||
|
||||
@@ -2,13 +2,17 @@ import type { EditorPluginManager } from '@esengine/editor-core';
|
||||
import { SceneInspectorPlugin } from '../../plugins/SceneInspectorPlugin';
|
||||
import { ProfilerPlugin } from '../../plugins/ProfilerPlugin';
|
||||
import { EditorAppearancePlugin } from '../../plugins/EditorAppearancePlugin';
|
||||
import { GizmoPlugin } from '../../plugins/GizmoPlugin';
|
||||
import { TilemapEditorPlugin } from '@esengine/tilemap-editor';
|
||||
|
||||
export class PluginInstaller {
|
||||
async installBuiltinPlugins(pluginManager: EditorPluginManager): Promise<void> {
|
||||
const plugins = [
|
||||
new GizmoPlugin(),
|
||||
new SceneInspectorPlugin(),
|
||||
new ProfilerPlugin(),
|
||||
new EditorAppearancePlugin()
|
||||
new EditorAppearancePlugin(),
|
||||
new TilemapEditorPlugin()
|
||||
];
|
||||
|
||||
for (const plugin of plugins) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Core, ComponentRegistry as CoreComponentRegistry } from '@esengine/ecs-
|
||||
import {
|
||||
UIRegistry,
|
||||
MessageHub,
|
||||
IMessageHub,
|
||||
SerializerRegistry,
|
||||
EntityStoreService,
|
||||
ComponentRegistry,
|
||||
@@ -12,10 +13,17 @@ import {
|
||||
SettingsRegistry,
|
||||
SceneManagerService,
|
||||
FileActionRegistry,
|
||||
EntityCreationRegistry,
|
||||
EditorPluginManager,
|
||||
InspectorRegistry,
|
||||
IInspectorRegistry,
|
||||
PropertyRendererRegistry,
|
||||
FieldEditorRegistry
|
||||
FieldEditorRegistry,
|
||||
ComponentActionRegistry,
|
||||
IDialogService,
|
||||
IFileSystemService,
|
||||
CompilerRegistry,
|
||||
ICompilerRegistry
|
||||
} from '@esengine/editor-core';
|
||||
import {
|
||||
TransformComponent,
|
||||
@@ -128,9 +136,12 @@ export class ServiceRegistry {
|
||||
const settingsRegistry = new SettingsRegistry();
|
||||
const sceneManager = new SceneManagerService(messageHub, fileAPI, projectService, entityStore);
|
||||
const fileActionRegistry = new FileActionRegistry();
|
||||
const entityCreationRegistry = new EntityCreationRegistry();
|
||||
const componentActionRegistry = new ComponentActionRegistry();
|
||||
|
||||
Core.services.registerInstance(UIRegistry, uiRegistry);
|
||||
Core.services.registerInstance(MessageHub, messageHub);
|
||||
Core.services.registerInstance(IMessageHub, messageHub); // Symbol 注册用于跨包插件访问
|
||||
Core.services.registerInstance(SerializerRegistry, serializerRegistry);
|
||||
Core.services.registerInstance(EntityStoreService, entityStore);
|
||||
Core.services.registerInstance(ComponentRegistry, componentRegistry);
|
||||
@@ -141,6 +152,8 @@ export class ServiceRegistry {
|
||||
Core.services.registerInstance(SettingsRegistry, settingsRegistry);
|
||||
Core.services.registerInstance(SceneManagerService, sceneManager);
|
||||
Core.services.registerInstance(FileActionRegistry, fileActionRegistry);
|
||||
Core.services.registerInstance(EntityCreationRegistry, entityCreationRegistry);
|
||||
Core.services.registerInstance(ComponentActionRegistry, componentActionRegistry);
|
||||
|
||||
const pluginManager = new EditorPluginManager();
|
||||
pluginManager.initialize(coreInstance, Core.services);
|
||||
@@ -155,10 +168,12 @@ export class ServiceRegistry {
|
||||
const dialog = new TauriDialogService();
|
||||
const notification = new NotificationService();
|
||||
Core.services.registerInstance(NotificationService, notification);
|
||||
Core.services.registerInstance(IDialogService, dialog);
|
||||
Core.services.registerInstance(IFileSystemService, fileSystem);
|
||||
|
||||
const inspectorRegistry = new InspectorRegistry();
|
||||
|
||||
Core.services.registerInstance(InspectorRegistry, inspectorRegistry);
|
||||
Core.services.registerInstance(IInspectorRegistry, inspectorRegistry); // Symbol 注册用于跨包插件访问
|
||||
|
||||
const propertyRendererRegistry = new PropertyRendererRegistry();
|
||||
Core.services.registerInstance(PropertyRendererRegistry, propertyRendererRegistry);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Core, Entity } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import { TransformComponent } from '@esengine/ecs-components';
|
||||
import { TilemapComponent } from '@esengine/tilemap';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
|
||||
/**
|
||||
* Tilemap创建选项
|
||||
*/
|
||||
export interface TilemapCreationOptions {
|
||||
/** 地图宽度(瓦片数),默认10 */
|
||||
width?: number;
|
||||
/** 地图高度(瓦片数),默认10 */
|
||||
height?: number;
|
||||
/** 瓦片宽度(像素),默认32 */
|
||||
tileWidth?: number;
|
||||
/** 瓦片高度(像素),默认32 */
|
||||
tileHeight?: number;
|
||||
/** 渲染层级,默认0 */
|
||||
sortingOrder?: number;
|
||||
/** 初始Tileset源路径 */
|
||||
tilesetSource?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带Tilemap组件的实体命令
|
||||
*/
|
||||
export class CreateTilemapEntityCommand extends BaseCommand {
|
||||
private entity: Entity | null = null;
|
||||
private entityId: number | null = null;
|
||||
|
||||
constructor(
|
||||
private entityStore: EntityStoreService,
|
||||
private messageHub: MessageHub,
|
||||
private entityName: string,
|
||||
private parentEntity?: Entity,
|
||||
private options: TilemapCreationOptions = {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
throw new Error('场景未初始化');
|
||||
}
|
||||
|
||||
this.entity = scene.createEntity(this.entityName);
|
||||
this.entityId = this.entity.id;
|
||||
|
||||
// 添加Transform组件
|
||||
this.entity.addComponent(new TransformComponent());
|
||||
|
||||
// 创建并配置Tilemap组件
|
||||
const tilemapComponent = new TilemapComponent();
|
||||
|
||||
// 应用配置选项
|
||||
const {
|
||||
width = 10,
|
||||
height = 10,
|
||||
tileWidth = 32,
|
||||
tileHeight = 32,
|
||||
sortingOrder = 0,
|
||||
tilesetSource
|
||||
} = this.options;
|
||||
|
||||
tilemapComponent.tileWidth = tileWidth;
|
||||
tilemapComponent.tileHeight = tileHeight;
|
||||
tilemapComponent.sortingOrder = sortingOrder;
|
||||
|
||||
// 初始化空白地图
|
||||
tilemapComponent.initializeEmpty(width, height);
|
||||
|
||||
// 添加初始 Tileset
|
||||
if (tilesetSource) {
|
||||
tilemapComponent.addTileset(tilesetSource);
|
||||
}
|
||||
|
||||
this.entity.addComponent(tilemapComponent);
|
||||
|
||||
if (this.parentEntity) {
|
||||
this.parentEntity.addChild(this.entity);
|
||||
}
|
||||
|
||||
this.entityStore.addEntity(this.entity, this.parentEntity);
|
||||
this.entityStore.selectEntity(this.entity);
|
||||
|
||||
this.messageHub.publish('entity:added', { entity: this.entity });
|
||||
this.messageHub.publish('tilemap:created', {
|
||||
entity: this.entity,
|
||||
component: tilemapComponent
|
||||
});
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.entity) return;
|
||||
|
||||
this.entityStore.removeEntity(this.entity);
|
||||
this.entity.destroy();
|
||||
|
||||
this.messageHub.publish('entity:removed', { entityId: this.entityId });
|
||||
|
||||
this.entity = null;
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `创建Tilemap实体: ${this.entityName}`;
|
||||
}
|
||||
|
||||
getCreatedEntity(): Entity | null {
|
||||
return this.entity;
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,6 @@ export { CreateEntityCommand } from './CreateEntityCommand';
|
||||
export { CreateSpriteEntityCommand } from './CreateSpriteEntityCommand';
|
||||
export { CreateAnimatedSpriteEntityCommand } from './CreateAnimatedSpriteEntityCommand';
|
||||
export { CreateCameraEntityCommand } from './CreateCameraEntityCommand';
|
||||
export { CreateTilemapEntityCommand } from './CreateTilemapEntityCommand';
|
||||
export { DeleteEntityCommand } from './DeleteEntityCommand';
|
||||
|
||||
|
||||
@@ -95,27 +95,40 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP
|
||||
|
||||
const unsubscribe = messageHub.subscribe('asset:reveal', async (data: any) => {
|
||||
const filePath = data.path;
|
||||
if (filePath) {
|
||||
const lastSlashIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
|
||||
const dirPath = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : null;
|
||||
if (!filePath || !projectPath) return;
|
||||
|
||||
// Convert relative path to absolute path if needed
|
||||
let absoluteFilePath = filePath;
|
||||
if (!filePath.includes(':') && !filePath.startsWith('/')) {
|
||||
absoluteFilePath = `${projectPath}/${filePath}`.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
const lastSlashIndex = Math.max(absoluteFilePath.lastIndexOf('/'), absoluteFilePath.lastIndexOf('\\'));
|
||||
const dirPath = lastSlashIndex > 0 ? absoluteFilePath.substring(0, lastSlashIndex) : null;
|
||||
|
||||
if (dirPath) {
|
||||
try {
|
||||
const dirExists = await TauriAPI.pathExists(dirPath);
|
||||
if (!dirExists) return;
|
||||
|
||||
setCurrentPath(dirPath);
|
||||
// Load assets first, then set selection after list is populated
|
||||
await loadAssets(dirPath);
|
||||
setSelectedPaths(new Set([filePath]));
|
||||
setSelectedPaths(new Set([absoluteFilePath]));
|
||||
|
||||
// Expand tree to reveal the file
|
||||
if (showDetailView) {
|
||||
detailViewFileTreeRef.current?.revealPath(filePath);
|
||||
detailViewFileTreeRef.current?.revealPath(absoluteFilePath);
|
||||
} else {
|
||||
treeOnlyViewFileTreeRef.current?.revealPath(filePath);
|
||||
treeOnlyViewFileTreeRef.current?.revealPath(absoluteFilePath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[AssetBrowser] Failed to reveal asset: ${absoluteFilePath}`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [showDetailView]);
|
||||
}, [showDetailView, projectPath]);
|
||||
|
||||
const loadAssets = async (path: string) => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Core, IService, ServiceType } from '@esengine/ecs-framework';
|
||||
import { CompilerRegistry, ICompiler, CompilerContext, CompileResult, IFileSystem, IDialog, FileEntry } from '@esengine/editor-core';
|
||||
import { X, Play, Loader2 } from 'lucide-react';
|
||||
import { open as tauriOpen, save as tauriSave, message as tauriMessage, confirm as tauriConfirm } from '@tauri-apps/plugin-dialog';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { invoke, convertFileSrc } from '@tauri-apps/api/core';
|
||||
import '../styles/CompilerConfigDialog.css';
|
||||
|
||||
interface DirectoryEntry {
|
||||
@@ -98,7 +98,11 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
return entries
|
||||
.filter((e) => !e.is_dir && e.name.endsWith(ext))
|
||||
.map((e) => e.name.replace(ext, ''));
|
||||
}
|
||||
},
|
||||
convertToAssetUrl: (filePath: string) => {
|
||||
return convertFileSrc(filePath);
|
||||
},
|
||||
dispose: () => {}
|
||||
});
|
||||
|
||||
const createDialog = (): IDialog => ({
|
||||
@@ -124,7 +128,8 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
},
|
||||
showConfirm: async (title: string, message: string) => {
|
||||
return await tauriConfirm(message, { title });
|
||||
}
|
||||
},
|
||||
dispose: () => {}
|
||||
});
|
||||
|
||||
const createContext = (): CompilerContext => ({
|
||||
|
||||
@@ -91,7 +91,11 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
|
||||
};
|
||||
|
||||
const handlePropertyChange = (component: any, propertyName: string, value: any) => {
|
||||
if (!selectedEntity) return;
|
||||
console.log('[EntityInspector] handlePropertyChange called:', propertyName, value);
|
||||
if (!selectedEntity) {
|
||||
console.log('[EntityInspector] No selectedEntity, returning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Actually update the component property
|
||||
// 实际更新组件属性
|
||||
@@ -103,6 +107,10 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
|
||||
propertyName,
|
||||
value
|
||||
});
|
||||
|
||||
// Also publish scene:modified so other panels can react
|
||||
console.log('[EntityInspector] Publishing scene:modified');
|
||||
messageHub.publish('scene:modified', {});
|
||||
};
|
||||
|
||||
const renderRemoteProperty = (key: string, value: any) => {
|
||||
|
||||
@@ -92,10 +92,16 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
// Expand tree to reveal a specific file path
|
||||
const revealPath = async (targetPath: string) => {
|
||||
if (!rootPath || !targetPath.startsWith(rootPath)) return;
|
||||
if (!rootPath) return;
|
||||
|
||||
// Normalize paths to use forward slashes for comparison
|
||||
const normalizedTargetPath = targetPath.replace(/\\/g, '/');
|
||||
const normalizedRootPath = rootPath.replace(/\\/g, '/');
|
||||
|
||||
if (!normalizedTargetPath.startsWith(normalizedRootPath)) return;
|
||||
|
||||
// Get path segments between root and target
|
||||
const relativePath = targetPath.substring(rootPath.length).replace(/^[/\\]/, '');
|
||||
const relativePath = normalizedTargetPath.substring(normalizedRootPath.length).replace(/^[/\\]/, '');
|
||||
const segments = relativePath.split(/[/\\]/);
|
||||
|
||||
// Build list of folder paths to expand
|
||||
@@ -748,9 +754,20 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
};
|
||||
|
||||
const renderNode = (node: TreeNode, level: number = 0) => {
|
||||
const isSelected = selectedPaths
|
||||
? selectedPaths.has(node.path)
|
||||
: (internalSelectedPath || selectedPath) === node.path;
|
||||
// Normalize paths for comparison (handle forward/backward slashes)
|
||||
const normalizedNodePath = node.path.replace(/\\/g, '/');
|
||||
const normalizedInternalPath = internalSelectedPath?.replace(/\\/g, '/');
|
||||
const normalizedSelectedPath = selectedPath?.replace(/\\/g, '/');
|
||||
|
||||
// Check if this node is selected, normalizing paths for comparison
|
||||
let isSelected = false;
|
||||
if (selectedPaths) {
|
||||
// Check both original path and normalized path in selectedPaths set
|
||||
isSelected = selectedPaths.has(node.path) || selectedPaths.has(normalizedNodePath);
|
||||
} else {
|
||||
isSelected = (normalizedInternalPath || normalizedSelectedPath) === normalizedNodePath;
|
||||
}
|
||||
|
||||
const isRenaming = renamingNode === node.path;
|
||||
const indent = level * 16;
|
||||
|
||||
|
||||
@@ -215,6 +215,8 @@ export function Inspector({ entityStore: _entityStore, messageHub, inspectorRegi
|
||||
propertyName,
|
||||
value
|
||||
});
|
||||
// Also publish scene:modified so other panels can react to changes
|
||||
messageHub.publish('scene:modified', {});
|
||||
};
|
||||
|
||||
const renderRemoteProperty = (key: string, value: any) => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Component, Core } from '@esengine/ecs-framework';
|
||||
import { PropertyMetadataService, PropertyMetadata, PropertyAction, MessageHub } from '@esengine/editor-core';
|
||||
import { PropertyMetadataService, PropertyMetadata, PropertyAction, MessageHub, IFileSystemService } from '@esengine/editor-core';
|
||||
import type { IFileSystem } from '@esengine/editor-core';
|
||||
import { ChevronRight, ChevronDown, ArrowRight, Lock } from 'lucide-react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { AnimationClipsFieldEditor } from '../infrastructure/field-editors/AnimationClipsFieldEditor';
|
||||
import { AssetSaveDialog } from './dialogs/AssetSaveDialog';
|
||||
import '../styles/PropertyInspector.css';
|
||||
|
||||
const animationClipsEditor = new AnimationClipsFieldEditor();
|
||||
@@ -80,6 +82,12 @@ export function PropertyInspector({ component, entity, version, onChange, onActi
|
||||
if (onChange) {
|
||||
onChange(propertyName, value);
|
||||
}
|
||||
|
||||
// Always publish scene:modified so other panels can react to changes
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
if (messageHub) {
|
||||
messageHub.publish('scene:modified', {});
|
||||
}
|
||||
};
|
||||
|
||||
// Read value directly from component to avoid state sync issues
|
||||
@@ -187,6 +195,7 @@ export function PropertyInspector({ component, entity, version, onChange, onActi
|
||||
fileExtension={metadata.fileExtension}
|
||||
readOnly={metadata.readOnly || !!controlledBy}
|
||||
controlledBy={controlledBy}
|
||||
entityId={entity?.id?.toString()}
|
||||
onChange={(newValue) => handleChange(propertyName, newValue)}
|
||||
/>
|
||||
);
|
||||
@@ -469,6 +478,7 @@ function ColorField({ label, value, readOnly, onChange }: ColorFieldProps) {
|
||||
const v = Math.max(0, Math.min(100, 100 - ((e.clientY - rect.top) / rect.height) * 100));
|
||||
const newColor = hsvToHex(hsv.h, s, v);
|
||||
setTempColor(newColor);
|
||||
onChange(newColor); // Real-time update
|
||||
};
|
||||
|
||||
const handleHueChange = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
@@ -476,6 +486,7 @@ function ColorField({ label, value, readOnly, onChange }: ColorFieldProps) {
|
||||
const h = Math.max(0, Math.min(360, ((e.clientX - rect.left) / rect.width) * 360));
|
||||
const newColor = hsvToHex(h, hsv.s, hsv.v);
|
||||
setTempColor(newColor);
|
||||
onChange(newColor); // Real-time update
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -857,11 +868,90 @@ interface AssetDropFieldProps {
|
||||
fileExtension?: string;
|
||||
readOnly?: boolean;
|
||||
controlledBy?: string;
|
||||
entityId?: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
function AssetDropField({ label, value, fileExtension, readOnly, controlledBy, onChange }: AssetDropFieldProps) {
|
||||
function AssetDropField({ label, value, fileExtension, readOnly, controlledBy, entityId, onChange }: AssetDropFieldProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
||||
|
||||
const canCreate = fileExtension && ['.tilemap.json', '.btree'].includes(fileExtension);
|
||||
|
||||
const handleCreate = () => {
|
||||
setShowSaveDialog(true);
|
||||
};
|
||||
|
||||
const handleSaveAsset = async (relativePath: string) => {
|
||||
const fileSystem = Core.services.tryResolve<IFileSystem>(IFileSystemService);
|
||||
const messageHub = Core.services.tryResolve(MessageHub);
|
||||
|
||||
if (!fileSystem) {
|
||||
console.error('[AssetDropField] FileSystem service not available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get absolute path from project
|
||||
const projectService = Core.services.tryResolve(
|
||||
(await import('@esengine/editor-core')).ProjectService
|
||||
);
|
||||
const currentProject = projectService?.getCurrentProject();
|
||||
if (!currentProject) {
|
||||
console.error('[AssetDropField] No project loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
const absolutePath = `${currentProject.path}/${relativePath}`.replace(/\\/g, '/');
|
||||
|
||||
// Create default content based on file type
|
||||
let defaultContent = '';
|
||||
if (fileExtension === '.tilemap.json') {
|
||||
defaultContent = JSON.stringify({
|
||||
name: 'New Tilemap',
|
||||
version: 2,
|
||||
width: 20,
|
||||
height: 15,
|
||||
tileWidth: 16,
|
||||
tileHeight: 16,
|
||||
layers: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Layer 0',
|
||||
visible: true,
|
||||
opacity: 1,
|
||||
data: new Array(20 * 15).fill(0)
|
||||
}
|
||||
],
|
||||
tilesets: []
|
||||
}, null, 2);
|
||||
} else if (fileExtension === '.btree') {
|
||||
defaultContent = JSON.stringify({
|
||||
name: 'New Behavior Tree',
|
||||
version: 1,
|
||||
nodes: [],
|
||||
connections: []
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
// Write file
|
||||
await fileSystem.writeFile(absolutePath, defaultContent);
|
||||
|
||||
// Update component with relative path
|
||||
onChange(relativePath);
|
||||
|
||||
// Open editor panel if tilemap
|
||||
if (messageHub && fileExtension === '.tilemap.json' && entityId) {
|
||||
const { useTilemapEditorStore } = await import('@esengine/tilemap-editor');
|
||||
useTilemapEditorStore.getState().setEntityId(entityId);
|
||||
messageHub.publish('dynamic-panel:open', { panelId: 'tilemap-editor', title: 'Tilemap Editor' });
|
||||
}
|
||||
|
||||
console.log('[AssetDropField] Created asset:', relativePath);
|
||||
} catch (error) {
|
||||
console.error('[AssetDropField] Failed to create asset:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnter = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -890,8 +980,14 @@ function AssetDropField({ label, value, fileExtension, readOnly, controlledBy, o
|
||||
if (assetPath) {
|
||||
if (fileExtension) {
|
||||
const extensions = fileExtension.split(',').map((ext) => ext.trim().toLowerCase());
|
||||
const fileExt = assetPath.toLowerCase().split('.').pop();
|
||||
if (fileExt && extensions.some((ext) => ext === `.${fileExt}` || ext === fileExt)) {
|
||||
const lowerPath = assetPath.toLowerCase();
|
||||
// Check if the path ends with any of the specified extensions
|
||||
// This handles both simple extensions (.json) and compound extensions (.tilemap.json)
|
||||
const isValidExtension = extensions.some((ext) => {
|
||||
const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`;
|
||||
return lowerPath.endsWith(normalizedExt);
|
||||
});
|
||||
if (isValidExtension) {
|
||||
onChange(assetPath);
|
||||
}
|
||||
} else {
|
||||
@@ -943,6 +1039,18 @@ function AssetDropField({ label, value, fileExtension, readOnly, controlledBy, o
|
||||
{value ? getFileName(value) : 'None'}
|
||||
</span>
|
||||
<div className="property-asset-actions">
|
||||
{canCreate && !readOnly && !value && (
|
||||
<button
|
||||
className="property-asset-btn property-asset-btn-create"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCreate();
|
||||
}}
|
||||
title="创建新资产"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
)}
|
||||
{value && (
|
||||
<button
|
||||
className="property-asset-btn"
|
||||
@@ -957,6 +1065,16 @@ function AssetDropField({ label, value, fileExtension, readOnly, controlledBy, o
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Dialog */}
|
||||
<AssetSaveDialog
|
||||
isOpen={showSaveDialog}
|
||||
onClose={() => setShowSaveDialog(false)}
|
||||
onSave={handleSaveAsset}
|
||||
title={fileExtension === '.tilemap.json' ? '创建 Tilemap 资产' : '创建资产'}
|
||||
defaultFileName={fileExtension === '.tilemap.json' ? 'new-tilemap' : 'new-asset'}
|
||||
fileExtension={fileExtension}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Entity, Core } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub, SceneManagerService, CommandManager } from '@esengine/editor-core';
|
||||
import { EntityStoreService, MessageHub, SceneManagerService, CommandManager, EntityCreationRegistry, EntityCreationTemplate } from '@esengine/editor-core';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import { Box, Layers, Wifi, Search, Plus, Trash2, Monitor, Globe, Image, Camera, Film } from 'lucide-react';
|
||||
import { ProfilerService, RemoteEntity } from '../services/ProfilerService';
|
||||
@@ -31,10 +31,32 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; entityId: number | null } | null>(null);
|
||||
const [draggedEntityId, setDraggedEntityId] = useState<number | null>(null);
|
||||
const [dropTargetIndex, setDropTargetIndex] = useState<number | null>(null);
|
||||
const [pluginTemplates, setPluginTemplates] = useState<EntityCreationTemplate[]>([]);
|
||||
const { t, locale } = useLocale();
|
||||
|
||||
const isShowingRemote = viewMode === 'remote' && isRemoteConnected;
|
||||
|
||||
// Get entity creation templates from plugins
|
||||
useEffect(() => {
|
||||
const updateTemplates = () => {
|
||||
const registry = Core.services.resolve(EntityCreationRegistry);
|
||||
if (registry) {
|
||||
setPluginTemplates(registry.getAll());
|
||||
}
|
||||
};
|
||||
|
||||
updateTemplates();
|
||||
|
||||
// Update when plugins are installed
|
||||
const unsubInstalled = messageHub.subscribe('plugin:installed', updateTemplates);
|
||||
const unsubUninstalled = messageHub.subscribe('plugin:uninstalled', updateTemplates);
|
||||
|
||||
return () => {
|
||||
unsubInstalled();
|
||||
unsubUninstalled();
|
||||
};
|
||||
}, [messageHub]);
|
||||
|
||||
// Subscribe to scene changes
|
||||
useEffect(() => {
|
||||
const sceneManager = Core.services.resolve(SceneManagerService);
|
||||
@@ -535,6 +557,23 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
<Camera size={12} />
|
||||
<span>{locale === 'zh' ? '创建相机' : 'Create Camera'}</span>
|
||||
</button>
|
||||
{pluginTemplates.length > 0 && (
|
||||
<>
|
||||
<div className="context-menu-divider" />
|
||||
{pluginTemplates.map((template) => (
|
||||
<button
|
||||
key={template.id}
|
||||
onClick={async () => {
|
||||
await template.create(contextMenu.entityId ?? undefined);
|
||||
closeContextMenu();
|
||||
}}
|
||||
>
|
||||
{template.icon || <Plus size={12} />}
|
||||
<span>{template.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{contextMenu.entityId && (
|
||||
<>
|
||||
<div className="context-menu-divider" />
|
||||
|
||||
@@ -43,7 +43,7 @@ function generateRuntimeHtml(): string {
|
||||
<canvas id="runtime-canvas"></canvas>
|
||||
<script src="/runtime.browser.js"></script>
|
||||
<script type="module">
|
||||
import * as esEngine from '/engine.js';
|
||||
import * as esEngine from '/es_engine.js';
|
||||
(async function() {
|
||||
try {
|
||||
// Set canvas size before creating runtime
|
||||
@@ -361,7 +361,8 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Sync camera state to engine
|
||||
// Sync camera state to engine and publish camera:updated event
|
||||
// 同步相机状态到引擎并发布 camera:updated 事件
|
||||
useEffect(() => {
|
||||
if (engine.state.initialized) {
|
||||
EngineService.getInstance().setCamera({
|
||||
@@ -370,6 +371,17 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
zoom: camera2DZoom,
|
||||
rotation: 0
|
||||
});
|
||||
|
||||
// Publish camera update event for other systems
|
||||
// 发布相机更新事件供其他系统使用
|
||||
const hub = messageHubRef.current;
|
||||
if (hub) {
|
||||
hub.publish('camera:updated', {
|
||||
x: camera2DOffset.x,
|
||||
y: camera2DOffset.y,
|
||||
zoom: camera2DZoom
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [camera2DOffset, camera2DZoom, engine.state.initialized]);
|
||||
|
||||
@@ -473,11 +485,11 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
const handleStop = async () => {
|
||||
setPlayState('stopped');
|
||||
engine.stop();
|
||||
// Restore scene snapshot
|
||||
EngineService.getInstance().restoreSceneSnapshot();
|
||||
await EngineService.getInstance().restoreSceneSnapshot();
|
||||
// Restore editor camera state
|
||||
setCamera2DOffset({ x: editorCameraRef.current.x, y: editorCameraRef.current.y });
|
||||
setCamera2DZoom(editorCameraRef.current.zoom);
|
||||
|
||||
@@ -197,3 +197,119 @@
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Asset Save Dialog specific styles */
|
||||
.asset-save-filename {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #333;
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.asset-save-filename label {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-save-filename input {
|
||||
flex: 1;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.asset-save-filename input:focus {
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
.asset-save-extension {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* New folder styles */
|
||||
.asset-save-new-folder-btn {
|
||||
padding: 8px 16px;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.asset-save-new-folder-btn button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: #333;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.asset-save-new-folder-btn button:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.asset-save-new-folder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-top: 1px solid #333;
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.asset-save-new-folder input {
|
||||
flex: 1;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.asset-save-new-folder input:focus {
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
.asset-save-new-folder button {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.asset-save-new-folder button:first-of-type {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.asset-save-new-folder button:first-of-type:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
|
||||
.asset-save-new-folder button:first-of-type:disabled {
|
||||
background: #333;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.asset-save-new-folder button:last-child {
|
||||
background: #333;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.asset-save-new-folder button:last-child:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
@@ -149,19 +149,34 @@ export function AssetPickerDialog({
|
||||
}
|
||||
}, [toggleFolder]);
|
||||
|
||||
// Convert absolute path to relative path based on project root
|
||||
const toRelativePath = useCallback((absolutePath: string): string => {
|
||||
const projectService = Core.services.tryResolve(ProjectService);
|
||||
const currentProject = projectService?.getCurrentProject();
|
||||
if (currentProject) {
|
||||
const projectPath = currentProject.path.replace(/\\/g, '/');
|
||||
const normalizedAbsolute = absolutePath.replace(/\\/g, '/');
|
||||
if (normalizedAbsolute.startsWith(projectPath)) {
|
||||
// Return relative path from project root
|
||||
return normalizedAbsolute.substring(projectPath.length + 1);
|
||||
}
|
||||
}
|
||||
return absolutePath;
|
||||
}, []);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (selectedPath) {
|
||||
onSelect(selectedPath);
|
||||
onSelect(toRelativePath(selectedPath));
|
||||
onClose();
|
||||
}
|
||||
}, [selectedPath, onSelect, onClose]);
|
||||
}, [selectedPath, onSelect, onClose, toRelativePath]);
|
||||
|
||||
const handleDoubleClick = useCallback((node: FileNode) => {
|
||||
if (!node.isDirectory) {
|
||||
onSelect(node.path);
|
||||
onSelect(toRelativePath(node.path));
|
||||
onClose();
|
||||
}
|
||||
}, [onSelect, onClose]);
|
||||
}, [onSelect, onClose, toRelativePath]);
|
||||
|
||||
const getFileIcon = (name: string) => {
|
||||
const ext = name.split('.').pop()?.toLowerCase();
|
||||
|
||||
374
packages/editor-app/src/components/dialogs/AssetSaveDialog.tsx
Normal file
374
packages/editor-app/src/components/dialogs/AssetSaveDialog.tsx
Normal file
@@ -0,0 +1,374 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { X, Search, Folder, FolderOpen, FolderPlus } from 'lucide-react';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { ProjectService, IFileSystemService } from '@esengine/editor-core';
|
||||
import type { IFileSystem } from '@esengine/editor-core';
|
||||
import './AssetPickerDialog.css';
|
||||
|
||||
interface AssetSaveDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (path: string) => void;
|
||||
title?: string;
|
||||
defaultFileName?: string;
|
||||
fileExtension?: string; // e.g., '.tilemap.json'
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface FileNode {
|
||||
name: string;
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
children?: FileNode[];
|
||||
}
|
||||
|
||||
export function AssetSaveDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
title = 'Save Asset',
|
||||
defaultFileName = 'new-asset',
|
||||
fileExtension = '',
|
||||
placeholder = 'Search folders...'
|
||||
}: AssetSaveDialogProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||
const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
|
||||
const [fileName, setFileName] = useState(defaultFileName);
|
||||
const [folders, setFolders] = useState<FileNode[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [projectPath, setProjectPath] = useState('');
|
||||
const [showNewFolderInput, setShowNewFolderInput] = useState(false);
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
|
||||
// Load project folders
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const loadFolders = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const projectService = Core.services.tryResolve(ProjectService);
|
||||
const fileSystem = Core.services.tryResolve<IFileSystem>(IFileSystemService);
|
||||
|
||||
const currentProject = projectService?.getCurrentProject();
|
||||
if (projectService && currentProject && fileSystem) {
|
||||
const projPath = currentProject.path;
|
||||
setProjectPath(projPath);
|
||||
const assetsPath = `${projPath}/assets`;
|
||||
|
||||
// Set default selected folder to assets
|
||||
setSelectedFolder(assetsPath);
|
||||
|
||||
const buildTree = async (dirPath: string): Promise<FileNode[]> => {
|
||||
const entries = await fileSystem.listDirectory(dirPath);
|
||||
const nodes: FileNode[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
// Only include directories
|
||||
if (entry.isDirectory) {
|
||||
const node: FileNode = {
|
||||
name: entry.name,
|
||||
path: entry.path,
|
||||
isDirectory: true
|
||||
};
|
||||
|
||||
try {
|
||||
node.children = await buildTree(entry.path);
|
||||
} catch {
|
||||
node.children = [];
|
||||
}
|
||||
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort alphabetically
|
||||
return nodes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
const tree = await buildTree(assetsPath);
|
||||
// Add root assets folder
|
||||
const rootNode: FileNode = {
|
||||
name: 'assets',
|
||||
path: assetsPath,
|
||||
isDirectory: true,
|
||||
children: tree
|
||||
};
|
||||
setFolders([rootNode]);
|
||||
setExpandedFolders(new Set([assetsPath]));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load folders:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadFolders();
|
||||
setFileName(defaultFileName);
|
||||
setSearchTerm('');
|
||||
}, [isOpen, defaultFileName]);
|
||||
|
||||
// Filter folders based on search
|
||||
const filteredFolders = useMemo(() => {
|
||||
if (!searchTerm) return folders;
|
||||
|
||||
const filterNode = (node: FileNode): FileNode | null => {
|
||||
const matchesSearch = node.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
if (node.children) {
|
||||
const filteredChildren = node.children
|
||||
.map(filterNode)
|
||||
.filter((n): n is FileNode => n !== null);
|
||||
|
||||
if (filteredChildren.length > 0 || matchesSearch) {
|
||||
return { ...node, children: filteredChildren };
|
||||
}
|
||||
}
|
||||
|
||||
return matchesSearch ? node : null;
|
||||
};
|
||||
|
||||
return folders
|
||||
.map(filterNode)
|
||||
.filter((n): n is FileNode => n !== null);
|
||||
}, [folders, searchTerm]);
|
||||
|
||||
const toggleFolder = useCallback((path: string) => {
|
||||
setExpandedFolders((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(path)) {
|
||||
next.delete(path);
|
||||
} else {
|
||||
next.add(path);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSelectFolder = useCallback((node: FileNode) => {
|
||||
setSelectedFolder(node.path);
|
||||
if (!expandedFolders.has(node.path)) {
|
||||
toggleFolder(node.path);
|
||||
}
|
||||
}, [expandedFolders, toggleFolder]);
|
||||
|
||||
// Convert absolute path to relative path based on project root
|
||||
const toRelativePath = useCallback((absolutePath: string): string => {
|
||||
if (projectPath) {
|
||||
const normalizedProject = projectPath.replace(/\\/g, '/');
|
||||
const normalizedAbsolute = absolutePath.replace(/\\/g, '/');
|
||||
if (normalizedAbsolute.startsWith(normalizedProject)) {
|
||||
return normalizedAbsolute.substring(normalizedProject.length + 1);
|
||||
}
|
||||
}
|
||||
return absolutePath;
|
||||
}, [projectPath]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (selectedFolder && fileName) {
|
||||
// Ensure file has correct extension
|
||||
let finalFileName = fileName;
|
||||
if (fileExtension && !finalFileName.endsWith(fileExtension)) {
|
||||
finalFileName += fileExtension;
|
||||
}
|
||||
|
||||
const fullPath = `${selectedFolder}/${finalFileName}`.replace(/\\/g, '/');
|
||||
onSave(toRelativePath(fullPath));
|
||||
onClose();
|
||||
}
|
||||
}, [selectedFolder, fileName, fileExtension, onSave, onClose, toRelativePath]);
|
||||
|
||||
const handleCreateFolder = useCallback(async () => {
|
||||
if (!selectedFolder || !newFolderName.trim()) return;
|
||||
|
||||
const fileSystem = Core.services.tryResolve<IFileSystem>(IFileSystemService);
|
||||
if (!fileSystem) return;
|
||||
|
||||
try {
|
||||
const newFolderPath = `${selectedFolder}/${newFolderName.trim()}`.replace(/\\/g, '/');
|
||||
await fileSystem.createDirectory(newFolderPath);
|
||||
|
||||
// Add new folder to tree
|
||||
const addFolderToTree = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.map(node => {
|
||||
if (node.path === selectedFolder) {
|
||||
const newNode: FileNode = {
|
||||
name: newFolderName.trim(),
|
||||
path: newFolderPath,
|
||||
isDirectory: true,
|
||||
children: []
|
||||
};
|
||||
return {
|
||||
...node,
|
||||
children: [...(node.children || []), newNode].sort((a, b) => a.name.localeCompare(b.name))
|
||||
};
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: addFolderToTree(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
setFolders(addFolderToTree(folders));
|
||||
setSelectedFolder(newFolderPath);
|
||||
setExpandedFolders(prev => new Set([...prev, selectedFolder]));
|
||||
setShowNewFolderInput(false);
|
||||
setNewFolderName('');
|
||||
} catch (error) {
|
||||
console.error('Failed to create folder:', error);
|
||||
}
|
||||
}, [selectedFolder, newFolderName, folders]);
|
||||
|
||||
const renderNode = (node: FileNode, depth: number = 0) => {
|
||||
const isExpanded = expandedFolders.has(node.path);
|
||||
const isSelected = selectedFolder === node.path;
|
||||
|
||||
return (
|
||||
<div key={node.path}>
|
||||
<div
|
||||
className={`asset-picker-item ${isSelected ? 'selected' : ''}`}
|
||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||
onClick={() => handleSelectFolder(node)}
|
||||
onDoubleClick={() => toggleFolder(node.path)}
|
||||
>
|
||||
<span className="asset-picker-item__icon">
|
||||
{isExpanded ? <FolderOpen size={14} /> : <Folder size={14} />}
|
||||
</span>
|
||||
<span className="asset-picker-item__name">{node.name}</span>
|
||||
</div>
|
||||
{isExpanded && node.children && (
|
||||
<div className="asset-picker-children">
|
||||
{node.children.map((child) => renderNode(child, depth + 1))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getDisplayPath = () => {
|
||||
if (!selectedFolder) return '';
|
||||
const relativePath = toRelativePath(selectedFolder);
|
||||
let finalFileName = fileName;
|
||||
if (fileExtension && !finalFileName.endsWith(fileExtension)) {
|
||||
finalFileName += fileExtension;
|
||||
}
|
||||
return `${relativePath}/${finalFileName}`;
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="asset-picker-overlay" onClick={onClose}>
|
||||
<div className="asset-picker-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="asset-picker-header">
|
||||
<h3>{title}</h3>
|
||||
<button className="asset-picker-close" onClick={onClose}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="asset-picker-search">
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="asset-picker-content">
|
||||
{loading ? (
|
||||
<div className="asset-picker-loading">Loading folders...</div>
|
||||
) : filteredFolders.length === 0 ? (
|
||||
<div className="asset-picker-empty">No folders found</div>
|
||||
) : (
|
||||
<div className="asset-picker-tree">
|
||||
{filteredFolders.map((node) => renderNode(node))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* New folder input */}
|
||||
{showNewFolderInput && (
|
||||
<div className="asset-save-new-folder">
|
||||
<input
|
||||
type="text"
|
||||
value={newFolderName}
|
||||
onChange={(e) => setNewFolderName(e.target.value)}
|
||||
placeholder="New folder name"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleCreateFolder();
|
||||
if (e.key === 'Escape') {
|
||||
setShowNewFolderInput(false);
|
||||
setNewFolderName('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button onClick={handleCreateFolder} disabled={!newFolderName.trim()}>
|
||||
Create
|
||||
</button>
|
||||
<button onClick={() => {
|
||||
setShowNewFolderInput(false);
|
||||
setNewFolderName('');
|
||||
}}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* New folder button */}
|
||||
{!showNewFolderInput && selectedFolder && (
|
||||
<div className="asset-save-new-folder-btn">
|
||||
<button onClick={() => setShowNewFolderInput(true)}>
|
||||
<FolderPlus size={14} />
|
||||
New Folder
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="asset-save-filename">
|
||||
<label>File name:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fileName}
|
||||
onChange={(e) => setFileName(e.target.value)}
|
||||
placeholder="Enter file name"
|
||||
autoFocus
|
||||
/>
|
||||
{fileExtension && (
|
||||
<span className="asset-save-extension">{fileExtension}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="asset-picker-footer">
|
||||
<div className="asset-picker-selected">
|
||||
{selectedFolder ? (
|
||||
<span title={getDisplayPath()}>
|
||||
{getDisplayPath()}
|
||||
</span>
|
||||
) : (
|
||||
<span className="placeholder">Select a folder</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="asset-picker-actions">
|
||||
<button className="btn-cancel" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn-confirm"
|
||||
onClick={handleSave}
|
||||
disabled={!selectedFolder || !fileName}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -113,6 +113,16 @@
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
/* 创建按钮特殊样式 */
|
||||
.asset-field__button--create {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.asset-field__button--create:hover {
|
||||
background: #1a3a1a;
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
/* 禁用状态 */
|
||||
.asset-field__container[disabled] {
|
||||
opacity: 0.6;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useCallback } from 'react';
|
||||
import { FileText, Search, X, FolderOpen, ArrowRight, Package } from 'lucide-react';
|
||||
import { FileText, Search, X, FolderOpen, ArrowRight, Package, Plus } from 'lucide-react';
|
||||
import { AssetPickerDialog } from '../../../components/dialogs/AssetPickerDialog';
|
||||
import './AssetField.css';
|
||||
|
||||
@@ -11,6 +11,7 @@ interface AssetFieldProps {
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
onNavigate?: (path: string) => void; // 导航到资产
|
||||
onCreate?: () => void; // 创建新资产
|
||||
}
|
||||
|
||||
export function AssetField({
|
||||
@@ -20,7 +21,8 @@ export function AssetField({
|
||||
fileExtension = '',
|
||||
placeholder = 'None',
|
||||
readonly = false,
|
||||
onNavigate
|
||||
onNavigate,
|
||||
onCreate
|
||||
}: AssetFieldProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
@@ -137,6 +139,20 @@ export function AssetField({
|
||||
|
||||
{/* 操作按钮组 */}
|
||||
<div className="asset-field__actions">
|
||||
{/* 创建按钮 */}
|
||||
{onCreate && !readonly && !value && (
|
||||
<button
|
||||
className="asset-field__button asset-field__button--create"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCreate();
|
||||
}}
|
||||
title="创建新资产"
|
||||
>
|
||||
<Plus size={12} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 浏览按钮 */}
|
||||
{!readonly && (
|
||||
<button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Settings, ChevronDown, ChevronRight, X, Plus, Box } from 'lucide-react';
|
||||
import { Entity, Component, Core, getComponentDependencies, getComponentTypeName, getComponentInstanceTypeName } from '@esengine/ecs-framework';
|
||||
import { MessageHub, CommandManager, ComponentRegistry } from '@esengine/editor-core';
|
||||
import { MessageHub, CommandManager, ComponentRegistry, ComponentActionRegistry } from '@esengine/editor-core';
|
||||
import { PropertyInspector } from '../../PropertyInspector';
|
||||
import { NotificationService } from '../../../services/NotificationService';
|
||||
import { RemoveComponentCommand, UpdateComponentCommand, AddComponentCommand } from '../../../application/commands/component';
|
||||
@@ -21,6 +21,7 @@ export function EntityInspector({ entity, messageHub, commandManager, componentV
|
||||
const [localVersion, setLocalVersion] = useState(0);
|
||||
|
||||
const componentRegistry = Core.services.resolve(ComponentRegistry);
|
||||
const componentActionRegistry = Core.services.resolve(ComponentActionRegistry);
|
||||
const availableComponents = componentRegistry?.getAllComponents() || [];
|
||||
|
||||
const toggleComponentExpanded = (index: number) => {
|
||||
@@ -252,6 +253,32 @@ export function EntityInspector({ entity, messageHub, commandManager, componentV
|
||||
}
|
||||
onAction={handlePropertyAction}
|
||||
/>
|
||||
{/* Dynamic component actions from plugins */}
|
||||
{componentActionRegistry?.getActionsForComponent(componentName).map((action) => (
|
||||
<button
|
||||
key={action.id}
|
||||
className="component-action-btn"
|
||||
onClick={() => action.execute(component, entity)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
padding: '8px 12px',
|
||||
width: '100%',
|
||||
marginTop: '8px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
background: 'var(--accent-color, #0078d4)',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{action.icon}
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
81
packages/editor-app/src/gizmos/SpriteGizmo.ts
Normal file
81
packages/editor-app/src/gizmos/SpriteGizmo.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Sprite Gizmo Implementation
|
||||
* 精灵 Gizmo 实现
|
||||
*
|
||||
* Registers gizmo provider for SpriteComponent using the GizmoRegistry.
|
||||
* Rendered via Rust WebGL engine for optimal performance.
|
||||
* 使用 GizmoRegistry 为 SpriteComponent 注册 gizmo 提供者。
|
||||
* 通过 Rust WebGL 引擎渲染以获得最佳性能。
|
||||
*/
|
||||
|
||||
import type { Entity } from '@esengine/ecs-framework';
|
||||
import type { IGizmoRenderData, IRectGizmoData, GizmoColor } from '@esengine/editor-core';
|
||||
import { GizmoColors, GizmoRegistry } from '@esengine/editor-core';
|
||||
import { SpriteComponent, TransformComponent } from '@esengine/ecs-components';
|
||||
|
||||
/**
|
||||
* Gizmo provider function for SpriteComponent.
|
||||
* SpriteComponent 的 gizmo 提供者函数。
|
||||
*/
|
||||
function spriteGizmoProvider(
|
||||
sprite: SpriteComponent,
|
||||
entity: Entity,
|
||||
isSelected: boolean
|
||||
): IGizmoRenderData[] {
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
if (!transform) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Calculate world-space dimensions
|
||||
// 计算世界空间尺寸
|
||||
const width = sprite.width * transform.scale.x;
|
||||
const height = sprite.height * transform.scale.y;
|
||||
|
||||
// Get rotation (handle both number and Vector3)
|
||||
// 获取旋转(处理数字和 Vector3 两种情况)
|
||||
const rotation = typeof transform.rotation === 'number'
|
||||
? transform.rotation
|
||||
: transform.rotation.z;
|
||||
|
||||
// Use predefined colors based on selection state
|
||||
// 根据选择状态使用预定义颜色
|
||||
const color: GizmoColor = isSelected
|
||||
? GizmoColors.selected
|
||||
: GizmoColors.unselected;
|
||||
|
||||
const gizmo: IRectGizmoData = {
|
||||
type: 'rect',
|
||||
x: transform.position.x,
|
||||
y: transform.position.y,
|
||||
width,
|
||||
height,
|
||||
rotation,
|
||||
originX: sprite.anchorX,
|
||||
originY: sprite.anchorY,
|
||||
color,
|
||||
showHandles: false // Selection handles are drawn separately by EngineRenderSystem
|
||||
};
|
||||
|
||||
return [gizmo];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register gizmo provider for SpriteComponent.
|
||||
* 为 SpriteComponent 注册 gizmo 提供者。
|
||||
*
|
||||
* Uses the GizmoRegistry pattern for clean separation between
|
||||
* game components and editor functionality.
|
||||
* 使用 GizmoRegistry 模式实现游戏组件和编辑器功能的清晰分离。
|
||||
*/
|
||||
export function registerSpriteGizmo(): void {
|
||||
GizmoRegistry.register(SpriteComponent, spriteGizmoProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister gizmo provider for SpriteComponent.
|
||||
* 取消注册 SpriteComponent 的 gizmo 提供者。
|
||||
*/
|
||||
export function unregisterSpriteGizmo(): void {
|
||||
GizmoRegistry.unregister(SpriteComponent);
|
||||
}
|
||||
9
packages/editor-app/src/gizmos/index.ts
Normal file
9
packages/editor-app/src/gizmos/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Editor Gizmos
|
||||
* 编辑器 Gizmos
|
||||
*
|
||||
* Gizmo implementations for built-in components.
|
||||
* 内置组件的 Gizmo 实现。
|
||||
*/
|
||||
|
||||
export { registerSpriteGizmo } from './SpriteGizmo';
|
||||
@@ -23,6 +23,25 @@ export class AssetFieldEditor implements IFieldEditor<string | null> {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
const messageHub = Core.services.tryResolve(MessageHub);
|
||||
if (messageHub) {
|
||||
if (fileExtension === '.tilemap.json') {
|
||||
messageHub.publish('tilemap:create-asset', {
|
||||
entityId: context.metadata?.entityId,
|
||||
onChange
|
||||
});
|
||||
} else if (fileExtension === '.btree') {
|
||||
messageHub.publish('behavior-tree:create-asset', {
|
||||
entityId: context.metadata?.entityId,
|
||||
onChange
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const canCreate = ['.tilemap.json', '.btree'].includes(fileExtension);
|
||||
|
||||
return (
|
||||
<AssetField
|
||||
label={label}
|
||||
@@ -32,6 +51,7 @@ export class AssetFieldEditor implements IFieldEditor<string | null> {
|
||||
placeholder={placeholder}
|
||||
readonly={context.readonly}
|
||||
onNavigate={handleNavigate}
|
||||
onCreate={canCreate ? handleCreate : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user