refactor(core): 移除@Inject参数装饰器,统一使用@InjectProperty (#229)

* refactor(core): 移除@Inject参数装饰器,统一使用@InjectProperty

* refactor(core): 移除@Inject参数装饰器,统一使用@InjectProperty
This commit is contained in:
YHH
2025-11-21 11:37:55 +08:00
committed by GitHub
parent a768b890fd
commit 2621d7f659
16 changed files with 211 additions and 209 deletions

View File

@@ -213,6 +213,7 @@ export class Core {
); );
this._debugManager = this._serviceContainer.resolve(DebugManager); this._debugManager = this._serviceContainer.resolve(DebugManager);
this._debugManager.onInitialize();
} }
this.initialize(); this.initialize();
@@ -488,6 +489,7 @@ export class Core {
); );
this._instance._debugManager = this._instance._serviceContainer.resolve(DebugManager); this._instance._debugManager = this._instance._serviceContainer.resolve(DebugManager);
this._instance._debugManager.onInitialize();
} }
// 更新Core配置 // 更新Core配置

View File

@@ -1,7 +1,7 @@
/** /**
* 依赖注入装饰器 * 依赖注入装饰器
* *
* 提供 @Injectable、@Inject 和 @Updatable 装饰器,用于标记可注入的类和依赖注入点 * 提供 @Injectable、@InjectProperty 和 @Updatable 装饰器,用于标记可注入的类和依赖注入点
*/ */
import type { ServiceContainer } from '../ServiceContainer'; import type { ServiceContainer } from '../ServiceContainer';
@@ -13,7 +13,6 @@ import type { IService, ServiceType } from '../ServiceContainer';
type Constructor = abstract new (...args: unknown[]) => unknown; type Constructor = abstract new (...args: unknown[]) => unknown;
const injectableMetadata = new WeakMap<Constructor, InjectableMetadata>(); const injectableMetadata = new WeakMap<Constructor, InjectableMetadata>();
const injectMetadata = new WeakMap<Constructor, Map<number, ServiceType<IService> | string | symbol>>();
const updatableMetadata = new WeakMap<Constructor, UpdatableMetadata>(); const updatableMetadata = new WeakMap<Constructor, UpdatableMetadata>();
/** /**
@@ -67,10 +66,11 @@ export interface UpdatableMetadata {
* *
* @Injectable() * @Injectable()
* class PhysicsSystem extends EntitySystem { * class PhysicsSystem extends EntitySystem {
* constructor( * @InjectProperty(TimeService)
* @Inject(TimeService) private timeService: TimeService * private timeService!: TimeService;
* ) { *
* super(); * constructor() {
* super(Matcher.empty());
* } * }
* } * }
* ``` * ```
@@ -135,27 +135,6 @@ export function Updatable(priority: number = 0): ClassDecorator {
} as ClassDecorator; } as ClassDecorator;
} }
/**
* @Inject() 装饰器
*
* 标记构造函数参数需要注入的服务类型
*
* @param serviceType 服务类型标识符
*/
export function Inject(serviceType: ServiceType<IService> | string | symbol): ParameterDecorator {
return function (target: object, _propertyKey: string | symbol | undefined, parameterIndex: number) {
// 获取或创建注入元数据
let params = injectMetadata.get(target as Constructor);
if (!params) {
params = new Map();
injectMetadata.set(target as Constructor, params);
}
// 记录参数索引和服务类型的映射
params.set(parameterIndex, serviceType);
};
}
/** /**
* @InjectProperty() 装饰器 * @InjectProperty() 装饰器
* *
@@ -164,6 +143,27 @@ export function Inject(serviceType: ServiceType<IService> | string | symbol): Pa
* 注入时机在构造函数执行后、onInitialize() 调用前完成 * 注入时机在构造函数执行后、onInitialize() 调用前完成
* *
* @param serviceType 服务类型 * @param serviceType 服务类型
*
* @example
* ```typescript
* @Injectable()
* class PhysicsSystem extends EntitySystem {
* @InjectProperty(TimeService)
* private timeService!: TimeService;
*
* @InjectProperty(CollisionService)
* private collision!: CollisionService;
*
* constructor() {
* super(Matcher.empty());
* }
*
* public onInitialize(): void {
* // 此时属性已注入完成,可以安全使用
* console.log(this.timeService.getDeltaTime());
* }
* }
* ```
*/ */
export function InjectProperty(serviceType: ServiceType<IService>): PropertyDecorator { export function InjectProperty(serviceType: ServiceType<IService>): PropertyDecorator {
return function (target: object, propertyKey: string | symbol) { return function (target: object, propertyKey: string | symbol) {
@@ -207,13 +207,14 @@ export function getInjectableMetadata(target: Constructor): InjectableMetadata |
} }
/** /**
* 获取构造函数参数的注入元数据 * 获取属性注入元数据
* *
* @param target 目标类 * @param target 目标类
* @returns 参数索引到服务类型的映射 * @returns 属性名到服务类型的映射
*/ */
export function getInjectMetadata(target: Constructor): Map<number, ServiceType<IService> | string | symbol> { export function getPropertyInjectMetadata(target: Constructor): Map<string | symbol, ServiceType<IService>> {
return injectMetadata.get(target) || new Map(); const metadata = injectableMetadata.get(target);
return metadata?.properties || new Map();
} }
/** /**
@@ -232,38 +233,13 @@ export function createInstance<T>(
constructor: new (...args: any[]) => T, constructor: new (...args: any[]) => T,
container: ServiceContainer container: ServiceContainer
): T { ): T {
// 获取参数注入元数据 // 创建实例(无参数注入
const injectParams = getInjectMetadata(constructor as Constructor); const instance = new constructor();
// 解析依赖 // 注入属性依赖
const dependencies: unknown[] = []; injectProperties(instance as object, container);
// 获取构造函数参数数量 return instance;
const paramCount = constructor.length;
for (let i = 0; i < paramCount; i++) {
const serviceType = injectParams.get(i);
if (serviceType) {
// 如果有显式的@Inject标记使用标记的类型
if (typeof serviceType === 'string' || typeof serviceType === 'symbol') {
// 字符串或Symbol类型的服务标识
throw new Error(
'String and Symbol service identifiers are not yet supported in constructor injection. ' +
`Please use class types for ${constructor.name} parameter ${i}`
);
} else {
// 类类型
dependencies.push(container.resolve(serviceType as ServiceType<IService>));
}
} else {
// 没有@Inject标记传入undefined
dependencies.push(undefined);
}
}
// 创建实例
return new constructor(...dependencies);
} }
/** /**

View File

@@ -6,12 +6,11 @@
export { export {
Injectable, Injectable,
Inject,
InjectProperty, InjectProperty,
Updatable, Updatable,
isInjectable, isInjectable,
getInjectableMetadata, getInjectableMetadata,
getInjectMetadata, getPropertyInjectMetadata,
isUpdatable, isUpdatable,
getUpdatableMetadata, getUpdatableMetadata,
createInstance, createInstance,

View File

@@ -70,11 +70,14 @@ export interface SystemMetadata {
* } * }
* } * }
* *
* // 配置更新顺序 * // 配置更新顺序和依赖注入
* @Injectable() * @Injectable()
* @ECSSystem('Physics', { updateOrder: 10 }) * @ECSSystem('Physics', { updateOrder: 10 })
* class PhysicsSystem extends EntitySystem { * class PhysicsSystem extends EntitySystem {
* constructor(@Inject(CollisionSystem) private collision: CollisionSystem) { * @InjectProperty(CollisionSystem)
* private collision!: CollisionSystem;
*
* constructor() {
* super(Matcher.empty().all(Transform, RigidBody)); * super(Matcher.empty().all(Transform, RigidBody));
* } * }
* } * }

View File

@@ -612,7 +612,7 @@ export class Scene implements IScene {
* 在场景中添加一个EntitySystem处理器 * 在场景中添加一个EntitySystem处理器
* *
* 支持两种使用方式: * 支持两种使用方式:
* 1. 传入类型推荐自动使用DI创建实例支持@Injectable和@Inject装饰器 * 1. 传入类型推荐自动使用DI创建实例支持@Injectable和@InjectProperty装饰器
* 2. 传入实例:直接使用提供的实例 * 2. 传入实例:直接使用提供的实例
* *
* @param systemTypeOrInstance 系统类型或系统实例 * @param systemTypeOrInstance 系统类型或系统实例
@@ -623,7 +623,10 @@ export class Scene implements IScene {
* // 方式1传入类型自动DI推荐 * // 方式1传入类型自动DI推荐
* @Injectable() * @Injectable()
* class PhysicsSystem extends EntitySystem { * class PhysicsSystem extends EntitySystem {
* constructor(@Inject(CollisionSystem) private collision: CollisionSystem) { * @InjectProperty(CollisionSystem)
* private collision!: CollisionSystem;
*
* constructor() {
* super(Matcher.empty().all(Transform)); * super(Matcher.empty().all(Transform));
* } * }
* } * }
@@ -718,7 +721,10 @@ export class Scene implements IScene {
* @Injectable() * @Injectable()
* @ECSSystem('Physics', { updateOrder: 10 }) * @ECSSystem('Physics', { updateOrder: 10 })
* class PhysicsSystem extends EntitySystem implements IService { * class PhysicsSystem extends EntitySystem implements IService {
* constructor(@Inject(CollisionSystem) private collision: CollisionSystem) { * @InjectProperty(CollisionSystem)
* private collision!: CollisionSystem;
*
* constructor() {
* super(Matcher.empty().all(Transform, RigidBody)); * super(Matcher.empty().all(Transform, RigidBody));
* } * }
* dispose() {} * dispose() {}
@@ -779,7 +785,7 @@ export class Scene implements IScene {
/** /**
* 获取指定类型的EntitySystem处理器 * 获取指定类型的EntitySystem处理器
* *
* @deprecated 推荐使用依赖注入代替此方法。使用 `scene.services.resolve(SystemType)` 或在System构造函数中使用 `@Inject(SystemType)` 装饰器。 * @deprecated 推荐使用依赖注入代替此方法。使用 `scene.services.resolve(SystemType)` 或使用 `@InjectProperty(SystemType)` 装饰器。
* *
* @param type 处理器类型 * @param type 处理器类型
* @returns 处理器实例如果未找到则返回null * @returns 处理器实例如果未找到则返回null
@@ -788,8 +794,11 @@ export class Scene implements IScene {
* ```typescript * ```typescript
* @Injectable() * @Injectable()
* class MySystem extends EntitySystem { * class MySystem extends EntitySystem {
* constructor(@Inject(PhysicsSystem) private physics: PhysicsSystem) { * @InjectProperty(PhysicsSystem)
* super(); * private physics!: PhysicsSystem;
*
* constructor() {
* super(Matcher.empty());
* } * }
* } * }
* ``` * ```

View File

@@ -13,7 +13,7 @@ import type { IService } from '../../Core/ServiceContainer';
import type { IUpdatable } from '../../Types/IUpdatable'; import type { IUpdatable } from '../../Types/IUpdatable';
import { SceneManager } from '../../ECS/SceneManager'; import { SceneManager } from '../../ECS/SceneManager';
import { PerformanceMonitor } from '../PerformanceMonitor'; import { PerformanceMonitor } from '../PerformanceMonitor';
import { Injectable, Inject, Updatable } from '../../Core/DI/Decorators'; import { Injectable, InjectProperty, Updatable } from '../../Core/DI/Decorators';
import { DebugConfigService } from './DebugConfigService'; import { DebugConfigService } from './DebugConfigService';
/** /**
@@ -24,19 +24,26 @@ import { DebugConfigService } from './DebugConfigService';
@Injectable() @Injectable()
@Updatable() @Updatable()
export class DebugManager implements IService, IUpdatable { export class DebugManager implements IService, IUpdatable {
private config: IECSDebugConfig; private config!: IECSDebugConfig;
private webSocketManager: WebSocketManager; private webSocketManager!: WebSocketManager;
private entityCollector: EntityDataCollector; private entityCollector!: EntityDataCollector;
private systemCollector: SystemDataCollector; private systemCollector!: SystemDataCollector;
private performanceCollector: PerformanceDataCollector; private performanceCollector!: PerformanceDataCollector;
private componentCollector: ComponentDataCollector; private componentCollector!: ComponentDataCollector;
private sceneCollector: SceneDataCollector; private sceneCollector!: SceneDataCollector;
private sceneManager: SceneManager;
private performanceMonitor: PerformanceMonitor; @InjectProperty(SceneManager)
private sceneManager!: SceneManager;
@InjectProperty(PerformanceMonitor)
private performanceMonitor!: PerformanceMonitor;
@InjectProperty(DebugConfigService)
private configService!: DebugConfigService;
private frameCounter: number = 0; private frameCounter: number = 0;
private lastSendTime: number = 0; private lastSendTime: number = 0;
private sendInterval: number; private sendInterval: number = 0;
private isRunning: boolean = false; private isRunning: boolean = false;
private originalConsole = { private originalConsole = {
log: console.log.bind(console), log: console.log.bind(console),
@@ -46,14 +53,8 @@ export class DebugManager implements IService, IUpdatable {
error: console.error.bind(console) error: console.error.bind(console)
}; };
constructor( public onInitialize(): void {
@Inject(SceneManager) sceneManager: SceneManager, this.config = this.configService.getConfig();
@Inject(PerformanceMonitor) performanceMonitor: PerformanceMonitor,
@Inject(DebugConfigService) configService: DebugConfigService
) {
this.config = configService.getConfig();
this.sceneManager = sceneManager;
this.performanceMonitor = performanceMonitor;
// 初始化数据收集器 // 初始化数据收集器
this.entityCollector = new EntityDataCollector(); this.entityCollector = new EntityDataCollector();

View File

@@ -19,12 +19,14 @@ export * from './Plugins';
// 依赖注入 // 依赖注入
export { export {
Injectable, Injectable,
Inject, InjectProperty,
Updatable, Updatable,
registerInjectable, registerInjectable,
createInstance, createInstance,
injectProperties,
isUpdatable, isUpdatable,
getUpdatableMetadata getUpdatableMetadata,
getPropertyInjectMetadata
} from './Core/DI'; } from './Core/DI';
export type { InjectableMetadata, UpdatableMetadata } from './Core/DI'; export type { InjectableMetadata, UpdatableMetadata } from './Core/DI';

View File

@@ -1,4 +1,4 @@
import { Injectable, Inject, isInjectable, getInjectMetadata, createInstance, registerInjectable } from '../../src/Core/DI'; import { Injectable, InjectProperty, isInjectable, getPropertyInjectMetadata, createInstance, registerInjectable } from '../../src/Core/DI';
import { ServiceContainer } from '../../src/Core/ServiceContainer'; import { ServiceContainer } from '../../src/Core/ServiceContainer';
import type { IService } from '../../src/Core/ServiceContainer'; import type { IService } from '../../src/Core/ServiceContainer';
@@ -14,9 +14,8 @@ class SimpleService implements IService {
@Injectable() @Injectable()
class DependentService implements IService { class DependentService implements IService {
constructor( @InjectProperty(SimpleService)
@Inject(SimpleService) public simpleService: SimpleService public simpleService!: SimpleService;
) {}
dispose() { dispose() {
// 清理资源 // 清理资源
@@ -25,10 +24,11 @@ class DependentService implements IService {
@Injectable() @Injectable()
class MultiDependencyService implements IService { class MultiDependencyService implements IService {
constructor( @InjectProperty(SimpleService)
@Inject(SimpleService) public service1: SimpleService, public service1!: SimpleService;
@Inject(DependentService) public service2: DependentService
) {} @InjectProperty(DependentService)
public service2!: DependentService;
dispose() { dispose() {
// 清理资源 // 清理资源
@@ -58,18 +58,18 @@ describe('DI - 依赖注入装饰器测试', () => {
}); });
}); });
describe('@Inject 装饰器', () => { describe('@InjectProperty 装饰器', () => {
test('应该记录参数注入元数据', () => { test('应该记录属性注入元数据', () => {
const metadata = getInjectMetadata(DependentService as any); const metadata = getPropertyInjectMetadata(DependentService as any);
expect(metadata.size).toBe(1); expect(metadata.size).toBe(1);
expect(metadata.get(0)).toBe(SimpleService); expect(metadata.get('simpleService')).toBe(SimpleService);
}); });
test('应该记录多个参数的注入元数据', () => { test('应该记录多个属性的注入元数据', () => {
const metadata = getInjectMetadata(MultiDependencyService as any); const metadata = getPropertyInjectMetadata(MultiDependencyService as any);
expect(metadata.size).toBe(2); expect(metadata.size).toBe(2);
expect(metadata.get(0)).toBe(SimpleService); expect(metadata.get('service1')).toBe(SimpleService);
expect(metadata.get(1)).toBe(DependentService); expect(metadata.get('service2')).toBe(DependentService);
}); });
}); });

View File

@@ -3,7 +3,7 @@ import { EntitySystem } from '../../src/ECS/Systems/EntitySystem';
import { Entity } from '../../src/ECS/Entity'; import { Entity } from '../../src/ECS/Entity';
import { Component } from '../../src/ECS/Component'; import { Component } from '../../src/ECS/Component';
import { Matcher } from '../../src/ECS/Utils/Matcher'; import { Matcher } from '../../src/ECS/Utils/Matcher';
import { Injectable, Inject, InjectProperty } from '../../src/Core/DI'; import { Injectable, InjectProperty } from '../../src/Core/DI';
import { Core } from '../../src/Core'; import { Core } from '../../src/Core';
import type { IService } from '../../src/Core/ServiceContainer'; import type { IService } from '../../src/Core/ServiceContainer';
import { ECSSystem } from '../../src/ECS/Decorators'; import { ECSSystem } from '../../src/ECS/Decorators';
@@ -82,9 +82,10 @@ describe('EntitySystem - 依赖注入测试', () => {
@Injectable() @Injectable()
@ECSSystem('Physics') @ECSSystem('Physics')
class PhysicsSystem extends EntitySystem implements IService { class PhysicsSystem extends EntitySystem implements IService {
constructor( @InjectProperty(CollisionSystem)
@Inject(CollisionSystem) public collision: CollisionSystem public collision!: CollisionSystem;
) {
constructor() {
super(Matcher.empty().all(Transform, Velocity)); super(Matcher.empty().all(Transform, Velocity));
} }
@@ -124,7 +125,10 @@ describe('EntitySystem - 依赖注入测试', () => {
@Injectable() @Injectable()
@ECSSystem('B') @ECSSystem('B')
class SystemB extends EntitySystem implements IService { class SystemB extends EntitySystem implements IService {
constructor(@Inject(SystemA) public systemA: SystemA) { @InjectProperty(SystemA)
public systemA!: SystemA;
constructor() {
super(Matcher.empty()); super(Matcher.empty());
} }
override dispose() {} override dispose() {}
@@ -133,10 +137,13 @@ describe('EntitySystem - 依赖注入测试', () => {
@Injectable() @Injectable()
@ECSSystem('C') @ECSSystem('C')
class SystemC extends EntitySystem implements IService { class SystemC extends EntitySystem implements IService {
constructor( @InjectProperty(SystemA)
@Inject(SystemA) public systemA: SystemA, public systemA!: SystemA;
@Inject(SystemB) public systemB: SystemB
) { @InjectProperty(SystemB)
public systemB!: SystemB;
constructor() {
super(Matcher.empty()); super(Matcher.empty());
} }
override dispose() {} override dispose() {}
@@ -166,7 +173,10 @@ describe('EntitySystem - 依赖注入测试', () => {
@Injectable() @Injectable()
@ECSSystem('Physics', { updateOrder: 10 }) @ECSSystem('Physics', { updateOrder: 10 })
class PhysicsSystem extends EntitySystem implements IService { class PhysicsSystem extends EntitySystem implements IService {
constructor(@Inject(CollisionSystem) public collision: CollisionSystem) { @InjectProperty(CollisionSystem)
public collision!: CollisionSystem;
constructor() {
super(Matcher.empty().all(Transform, Velocity)); super(Matcher.empty().all(Transform, Velocity));
} }
override dispose() {} override dispose() {}
@@ -175,7 +185,10 @@ describe('EntitySystem - 依赖注入测试', () => {
@Injectable() @Injectable()
@ECSSystem('Render', { updateOrder: 20 }) @ECSSystem('Render', { updateOrder: 20 })
class RenderSystem extends EntitySystem implements IService { class RenderSystem extends EntitySystem implements IService {
constructor(@Inject(PhysicsSystem) public physics: PhysicsSystem) { @InjectProperty(PhysicsSystem)
public physics!: PhysicsSystem;
constructor() {
super(Matcher.empty().all(Transform)); super(Matcher.empty().all(Transform));
} }
override dispose() {} override dispose() {}
@@ -346,7 +359,7 @@ describe('EntitySystem - 依赖注入测试', () => {
}); });
describe('Issue #76 场景验证', () => { describe('Issue #76 场景验证', () => {
test('应该消除硬编码依赖,使用构造函数注入', () => { test('应该消除硬编码依赖,使用属性注入', () => {
@Injectable() @Injectable()
@ECSSystem('TimeService') @ECSSystem('TimeService')
class TimeService extends EntitySystem implements IService { class TimeService extends EntitySystem implements IService {
@@ -378,10 +391,13 @@ describe('EntitySystem - 依赖注入测试', () => {
@Injectable() @Injectable()
@ECSSystem('Physics') @ECSSystem('Physics')
class PhysicsSystem extends EntitySystem implements IService { class PhysicsSystem extends EntitySystem implements IService {
constructor( @InjectProperty(TimeService)
@Inject(TimeService) private time: TimeService, private time!: TimeService;
@Inject(CollisionService) private collision: CollisionService
) { @InjectProperty(CollisionService)
private collision!: CollisionService;
constructor() {
super(Matcher.empty().all(Transform, Velocity)); super(Matcher.empty().all(Transform, Velocity));
} }
@@ -544,49 +560,5 @@ describe('EntitySystem - 依赖注入测试', () => {
expect(consumer.getInitializeValue()).toBe(42); expect(consumer.getInitializeValue()).toBe(42);
}); });
test('属性注入可以与构造函数注入混合使用', () => {
@Injectable()
@ECSSystem('A')
class ServiceA extends EntitySystem implements IService {
public valueA = 'A';
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('B')
class ServiceB extends EntitySystem implements IService {
public valueB = 'B';
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('Mixed')
class MixedSystem extends EntitySystem implements IService {
@InjectProperty(ServiceB)
serviceB!: ServiceB;
constructor(@Inject(ServiceA) public serviceA: ServiceA) {
super(Matcher.empty());
}
protected override onInitialize(): void {
expect(this.serviceA).toBeInstanceOf(ServiceA);
expect(this.serviceB).toBeInstanceOf(ServiceB);
expect(this.serviceA.valueA).toBe('A');
expect(this.serviceB.valueB).toBe('B');
}
override dispose() {}
}
scene.registerSystems([ServiceA, ServiceB, MixedSystem]);
});
}); });
}); });

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react'; import { ChevronDown, ChevronRight, Settings } from 'lucide-react';
import { PropertyContext, PropertyRendererRegistry } from '@esengine/editor-core'; import { PropertyContext, PropertyRendererRegistry } from '@esengine/editor-core';
import { Core } from '@esengine/ecs-framework'; import { Core } from '@esengine/ecs-framework';
@@ -38,6 +38,7 @@ export function ComponentItem({ component, decimalPlaces = 4 }: ComponentItemPro
}} }}
> >
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />} {isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
<Settings size={14} style={{ marginLeft: "4px", color: "#888" }} />
<span <span
style={{ style={{
marginLeft: '6px', marginLeft: '6px',

View File

@@ -10,24 +10,17 @@ const VectorInput: React.FC<{
value: number; value: number;
onChange: (value: number) => void; onChange: (value: number) => void;
readonly?: boolean; readonly?: boolean;
}> = ({ label, value, onChange, readonly }) => ( axis: 'x' | 'y' | 'z' | 'w';
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}> }> = ({ label, value, onChange, readonly, axis }) => (
<span style={{ color: '#888', fontSize: '10px', minWidth: '12px' }}>{label}:</span> <div className="property-vector-axis-compact">
<span className={`property-vector-axis-label property-vector-axis-${axis}`}>{label}</span>
<input <input
type="number" type="number"
value={value} value={value}
onChange={(e) => onChange(parseFloat(e.target.value) || 0)} onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
disabled={readonly} disabled={readonly}
step={0.1} step={0.1}
style={{ className="property-input property-input-number property-input-number-compact"
width: '60px',
padding: '2px 4px',
backgroundColor: '#2a2a2a',
border: '1px solid #444',
borderRadius: '3px',
color: '#e0e0e0',
fontSize: '11px'
}}
/> />
</div> </div>
); );
@@ -47,18 +40,20 @@ export class Vector2FieldEditor implements IFieldEditor<Vector2> {
return ( return (
<div className="property-field"> <div className="property-field">
<label className="property-label">{label}</label> <label className="property-label">{label}</label>
<div style={{ display: 'flex', gap: '8px' }}> <div className="property-vector-compact">
<VectorInput <VectorInput
label="X" label="X"
value={v.x} value={v.x}
onChange={(x) => onChange({ ...v, x })} onChange={(x) => onChange({ ...v, x })}
readonly={context.readonly} readonly={context.readonly}
axis="x"
/> />
<VectorInput <VectorInput
label="Y" label="Y"
value={v.y} value={v.y}
onChange={(y) => onChange({ ...v, y })} onChange={(y) => onChange({ ...v, y })}
readonly={context.readonly} readonly={context.readonly}
axis="y"
/> />
</div> </div>
</div> </div>
@@ -81,24 +76,27 @@ export class Vector3FieldEditor implements IFieldEditor<Vector3> {
return ( return (
<div className="property-field"> <div className="property-field">
<label className="property-label">{label}</label> <label className="property-label">{label}</label>
<div style={{ display: 'flex', gap: '8px' }}> <div className="property-vector-compact">
<VectorInput <VectorInput
label="X" label="X"
value={v.x} value={v.x}
onChange={(x) => onChange({ ...v, x })} onChange={(x) => onChange({ ...v, x })}
readonly={context.readonly} readonly={context.readonly}
axis="x"
/> />
<VectorInput <VectorInput
label="Y" label="Y"
value={v.y} value={v.y}
onChange={(y) => onChange({ ...v, y })} onChange={(y) => onChange({ ...v, y })}
readonly={context.readonly} readonly={context.readonly}
axis="y"
/> />
<VectorInput <VectorInput
label="Z" label="Z"
value={v.z} value={v.z}
onChange={(z) => onChange({ ...v, z })} onChange={(z) => onChange({ ...v, z })}
readonly={context.readonly} readonly={context.readonly}
axis="z"
/> />
</div> </div>
</div> </div>
@@ -121,30 +119,34 @@ export class Vector4FieldEditor implements IFieldEditor<Vector4> {
return ( return (
<div className="property-field"> <div className="property-field">
<label className="property-label">{label}</label> <label className="property-label">{label}</label>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}> <div className="property-vector-compact">
<VectorInput <VectorInput
label="X" label="X"
value={v.x} value={v.x}
onChange={(x) => onChange({ ...v, x })} onChange={(x) => onChange({ ...v, x })}
readonly={context.readonly} readonly={context.readonly}
axis="x"
/> />
<VectorInput <VectorInput
label="Y" label="Y"
value={v.y} value={v.y}
onChange={(y) => onChange({ ...v, y })} onChange={(y) => onChange({ ...v, y })}
readonly={context.readonly} readonly={context.readonly}
axis="y"
/> />
<VectorInput <VectorInput
label="Z" label="Z"
value={v.z} value={v.z}
onChange={(z) => onChange({ ...v, z })} onChange={(z) => onChange({ ...v, z })}
readonly={context.readonly} readonly={context.readonly}
axis="z"
/> />
<VectorInput <VectorInput
label="W" label="W"
value={v.w} value={v.w}
onChange={(w) => onChange({ ...v, w })} onChange={(w) => onChange({ ...v, w })}
readonly={context.readonly} readonly={context.readonly}
axis="w"
/> />
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react'; import { ChevronDown, ChevronRight, Settings } from 'lucide-react';
import { IPropertyRenderer, PropertyContext, PropertyRendererRegistry } from '@esengine/editor-core'; import { IPropertyRenderer, PropertyContext, PropertyRendererRegistry } from '@esengine/editor-core';
import { Core } from '@esengine/ecs-framework'; import { Core } from '@esengine/ecs-framework';
@@ -43,6 +43,7 @@ export class ComponentRenderer implements IPropertyRenderer<ComponentData> {
}} }}
> >
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />} {isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
<Settings size={14} style={{ marginLeft: "4px", color: "#888" }} />
<span <span
style={{ style={{
marginLeft: '6px', marginLeft: '6px',

View File

@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { IPropertyRenderer, PropertyContext } from '@esengine/editor-core'; import { IPropertyRenderer, PropertyContext } from '@esengine/editor-core';
import { formatNumber } from '../../components/inspectors/utils'; import { formatNumber } from '../../components/inspectors/utils';
@@ -23,6 +22,20 @@ interface Color {
a: number; a: number;
} }
const VectorValue: React.FC<{
label: string;
value: number;
axis: 'x' | 'y' | 'z' | 'w';
decimals: number;
}> = ({ label, value, axis, decimals }) => (
<div className="property-vector-axis-compact">
<span className={`property-vector-axis-label property-vector-axis-${axis}`}>{label}</span>
<span className="property-input property-input-number property-input-number-compact" style={{ cursor: 'default' }}>
{formatNumber(value, decimals)}
</span>
</div>
);
export class Vector2Renderer implements IPropertyRenderer<Vector2> { export class Vector2Renderer implements IPropertyRenderer<Vector2> {
readonly id = 'app.vector2'; readonly id = 'app.vector2';
readonly name = 'Vector2 Renderer'; readonly name = 'Vector2 Renderer';
@@ -44,9 +57,10 @@ export class Vector2Renderer implements IPropertyRenderer<Vector2> {
return ( return (
<div className="property-field"> <div className="property-field">
<label className="property-label">{context.name}</label> <label className="property-label">{context.name}</label>
<span className="property-value-text" style={{ color: '#9cdcfe', fontFamily: 'monospace' }}> <div className="property-vector-compact">
({formatNumber(value.x, decimals)}, {formatNumber(value.y, decimals)}) <VectorValue label="X" value={value.x} axis="x" decimals={decimals} />
</span> <VectorValue label="Y" value={value.y} axis="y" decimals={decimals} />
</div>
</div> </div>
); );
} }
@@ -74,9 +88,11 @@ export class Vector3Renderer implements IPropertyRenderer<Vector3> {
return ( return (
<div className="property-field"> <div className="property-field">
<label className="property-label">{context.name}</label> <label className="property-label">{context.name}</label>
<span className="property-value-text" style={{ color: '#9cdcfe', fontFamily: 'monospace' }}> <div className="property-vector-compact">
({formatNumber(value.x, decimals)}, {formatNumber(value.y, decimals)}, {formatNumber(value.z, decimals)}) <VectorValue label="X" value={value.x} axis="x" decimals={decimals} />
</span> <VectorValue label="Y" value={value.y} axis="y" decimals={decimals} />
<VectorValue label="Z" value={value.z} axis="z" decimals={decimals} />
</div>
</div> </div>
); );
} }
@@ -108,18 +124,13 @@ export class ColorRenderer implements IPropertyRenderer<Color> {
return ( return (
<div className="property-field"> <div className="property-field">
<label className="property-label">{context.name}</label> <label className="property-label">{context.name}</label>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> <div className="property-color-wrapper">
<div <div
style={{ className="property-color-preview"
width: '20px', style={{ backgroundColor: colorHex }}
height: '20px',
backgroundColor: colorHex,
border: '1px solid #444',
borderRadius: '2px'
}}
/> />
<span className="property-value-text" style={{ fontFamily: 'monospace' }}> <span className="property-input property-input-color-text" style={{ cursor: 'default' }}>
rgba({r}, {g}, {b}, {value.a.toFixed(2)}) {colorHex.toUpperCase()}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -100,12 +100,20 @@
} }
.flexlayout__tab_button_content { .flexlayout__tab_button_content {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 120px;
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
letter-spacing: 0.3px; letter-spacing: 0.3px;
} }
.flexlayout__tab_button--selected .flexlayout__tab_button_content { .flexlayout__tab_button--selected .flexlayout__tab_button_content {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 120px;
font-weight: 500; font-weight: 500;
} }

View File

@@ -104,9 +104,9 @@
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 6px center; background-position: right 6px center;
padding-right: 24px; padding-right: 24px;
appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
appearance: none;
} }
.property-input-select:hover { .property-input-select:hover {
@@ -286,10 +286,13 @@
.property-input-number-compact { .property-input-number-compact {
flex: 1; flex: 1;
min-width: 32px; min-width: 24px;
text-align: center; max-width: 40px;
padding: 2px 4px; text-align: right;
font-size: 10px; padding: 1px 4px;
font-size: 11px;
height: 18px;
line-height: 16px;
} }
.property-vector-expanded { .property-vector-expanded {
@@ -338,6 +341,12 @@
border: 1px solid rgba(59, 130, 246, 0.3); border: 1px solid rgba(59, 130, 246, 0.3);
} }
.property-vector-axis-w {
background: rgba(168, 85, 247, 0.2);
color: #c084fc;
border: 1px solid rgba(168, 85, 247, 0.3);
}
.property-field:focus-within { .property-field:focus-within {
background: rgba(255, 255, 255, 0.04); background: rgba(255, 255, 255, 0.04);
} }
@@ -357,6 +366,7 @@
input[type="number"].property-input { input[type="number"].property-input {
-moz-appearance: textfield; -moz-appearance: textfield;
appearance: textfield;
} }
input[type="number"].property-input::-webkit-outer-spin-button, input[type="number"].property-input::-webkit-outer-spin-button,

View File

@@ -29,6 +29,11 @@
color: var(--color-text-primary); color: var(--color-text-primary);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
min-width: 0;
} }
.scene-name-container { .scene-name-container {