diff --git a/packages/editor/src/components/sidebar/SidebarForm.vue b/packages/editor/src/components/sidebar/SidebarForm.vue index a8ea8d4..0065f82 100644 --- a/packages/editor/src/components/sidebar/SidebarForm.vue +++ b/packages/editor/src/components/sidebar/SidebarForm.vue @@ -60,18 +60,18 @@ const formFields = computed(() => { if (!object) return []; const fields: FormItem[] = []; - Object.values(object.editorProperties) - .filter((item) => item.definition.exposed) + object.properties + .filter((item) => item.definition.exposed && item.definition.name) .forEach((property) => { if ( property.definition.type === String || property.definition.type === Number ) { fields.push({ - name: property.definition.name, - label: toCapitalizedWords(property.definition.name), + name: property.definition.name!, + label: toCapitalizedWords(property.definition.name!), value: (object as unknown as Record)[ - property.definition.name + property.definition.name! ], type: property.definition.type === String ? 'string' : 'number', component: Field, @@ -83,10 +83,10 @@ const formFields = computed(() => { property.definition.type === Euler ) { fields.push({ - name: property.definition.name, - label: toCapitalizedWords(property.definition.name), + name: property.definition.name!, + label: toCapitalizedWords(property.definition.name!), value: (object as unknown as Record)[ - property.definition.name + property.definition.name! ], type: property.definition.type === Vector3 ? 'vector' : 'euler', component: Vector3Field, @@ -95,10 +95,10 @@ const formFields = computed(() => { if (property.definition.type === Color) { fields.push({ - name: property.definition.name, - label: toCapitalizedWords(property.definition.name), + name: property.definition.name!, + label: toCapitalizedWords(property.definition.name!), value: (object as unknown as Record)[ - property.definition.name + property.definition.name! ], component: ColorPicker, }); @@ -106,10 +106,10 @@ const formFields = computed(() => { if (property.definition.type === Boolean) { fields.push({ - name: property.definition.name, - label: toCapitalizedWords(property.definition.name), + name: property.definition.name!, + label: toCapitalizedWords(property.definition.name!), value: (object as unknown as Record)[ - property.definition.name + property.definition.name! ], component: Checkbox, }); diff --git a/packages/engine/package.json b/packages/engine/package.json index a3f6b53..2c4584b 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -22,6 +22,7 @@ "typescript": "^5.0.4" }, "dependencies": { + "reflect-metadata": "^0.1.13", "three": "^0.153.0" } } diff --git a/packages/engine/src/decorators/index.ts b/packages/engine/src/decorators/index.ts new file mode 100644 index 0000000..363875b --- /dev/null +++ b/packages/engine/src/decorators/index.ts @@ -0,0 +1 @@ +export * from './property'; diff --git a/packages/engine/src/decorators/property.ts b/packages/engine/src/decorators/property.ts new file mode 100644 index 0000000..ee34b2b --- /dev/null +++ b/packages/engine/src/decorators/property.ts @@ -0,0 +1,36 @@ +import 'reflect-metadata'; +import { Property, PropertyDefinition } from '../types/property'; + +export function EditorProperty( + editorPropertyDefinition?: PropertyDefinition +): PropertyDecorator { + return (target, propertyKey): void => { + let properties: Array = Reflect.getOwnMetadata( + 'properties', + target + ); + + if (!properties) { + Reflect.defineMetadata('properties', (properties = []), target); + } + + properties.push( + new Property({ ...editorPropertyDefinition, name: String(propertyKey) }) + ); + }; +} + +export function EditorPropertyExclude(): PropertyDecorator { + return (target, propertyKey): void => { + let excluded: Array = Reflect.getOwnMetadata( + 'excludedProperties', + target + ); + + if (!excluded) { + Reflect.defineMetadata('excludedProperties', (excluded = []), target); + } + + excluded.push(String(propertyKey)); + }; +} diff --git a/packages/engine/src/gameobjects/brick.object.ts b/packages/engine/src/gameobjects/brick.object.ts index 8d20bf7..c162edf 100644 --- a/packages/engine/src/gameobjects/brick.object.ts +++ b/packages/engine/src/gameobjects/brick.object.ts @@ -5,25 +5,11 @@ import { Mesh, MeshPhongMaterial, } from 'three'; -import { - EditorProperties, - GameObject3D, - gameObject3DEditorProperties, -} from '../types/game-object'; -import { Property } from '../types/property'; +import { GameObject3D } from '../types/game-object'; import { gameObjectGeometries } from './geometries'; import { assetManager } from '../assets/manager'; import { AssetInfo } from '../types/asset'; - -export const brickEditorProperties: EditorProperties = { - ...gameObject3DEditorProperties, - color: new Property({ name: 'color', type: Color }), - transparency: new Property({ name: 'transparency', type: Number }), - texture: new Property({ name: 'texture', type: AssetInfo }), - canCollide: new Property({ name: 'canCollide', type: Boolean }), - anchored: new Property({ name: 'anchored', type: Boolean }), - mass: new Property({ name: 'mass', type: Number }), -}; +import { EditorProperty } from '../decorators/property'; export class Brick extends GameObject3D { public objectType = Brick.name; @@ -31,19 +17,7 @@ export class Brick extends GameObject3D { protected material = new MeshPhongMaterial(); protected mesh: Mesh = new Mesh(this.geometry, this.material); - public canCollide = true; - public anchored = true; - public mass = 1; - - constructor( - protected geometry: BufferGeometry = gameObjectGeometries.boxGeometry, - public editorProperties: EditorProperties = brickEditorProperties - ) { - super(editorProperties); - this.name = this.objectType; - this.add(this.mesh); - } - + @EditorProperty({ type: Color }) get color() { return this.material.color; } @@ -51,6 +25,7 @@ export class Brick extends GameObject3D { this.material.color = new Color(color); } + @EditorProperty({ type: Number }) get transparency() { return 1 - this.material.opacity; } @@ -60,6 +35,10 @@ export class Brick extends GameObject3D { this.material.needsUpdate = true; } + @EditorProperty({ type: AssetInfo }) + get texture() { + return this.texturePath; + } set texture(path: string | undefined) { if (!path) { this.material.map = null; @@ -76,7 +55,21 @@ export class Brick extends GameObject3D { this.texturePath = path; this.material.map = asset.texture; } - get texture() { - return this.texturePath; + + @EditorProperty({ type: Boolean }) + public canCollide = true; + + @EditorProperty({ type: Boolean }) + public anchored = true; + + @EditorProperty({ type: Number }) + public mass = 1; + + constructor( + protected geometry: BufferGeometry = gameObjectGeometries.boxGeometry + ) { + super(); + this.name = this.objectType; + this.add(this.mesh); } } diff --git a/packages/engine/src/gameobjects/environment.object.ts b/packages/engine/src/gameobjects/environment.object.ts index 5eb9db7..0c7f0be 100644 --- a/packages/engine/src/gameobjects/environment.object.ts +++ b/packages/engine/src/gameobjects/environment.object.ts @@ -1,36 +1,34 @@ import { Color, Vector3 } from 'three'; -import { - EditorProperties, - GameObject, - SerializedObject, -} from '../types/game-object'; -import { Property } from '../types/property'; +import { GameObject, SerializedObject } from '../types/game-object'; import { environmentDefaults } from '../defaults/environment'; - -export const environmentEditorProperties: EditorProperties = { - sunColor: new Property({ name: 'sunColor', type: Color }), - sunPosition: new Property({ name: 'sunPosition', type: Vector3 }), - sunStrength: new Property({ name: 'sunStrength', type: Number }), - ambientColor: new Property({ name: 'ambientColor', type: Color }), - ambientStrength: new Property({ name: 'ambientStrength', type: Number }), - clearColor: new Property({ name: 'clearColor', type: Color }), -}; +import { EditorProperty, EditorPropertyExclude } from '../decorators/property'; export class Environment extends GameObject { public objectType = Environment.name; + @EditorPropertyExclude() public name = Environment.name; public virtual = true; - sunColor = environmentDefaults.sunColor.clone(); - sunPosition = environmentDefaults.sunPosition.clone(); - sunStrength = environmentDefaults.sunStrength; - ambientColor = environmentDefaults.ambientColor.clone(); - ambientStrength = environmentDefaults.ambientStrength; - clearColor = environmentDefaults.clearColor.clone(); + @EditorPropertyExclude() + public override visible!: boolean; - constructor() { - super(environmentEditorProperties); - } + @EditorProperty({ type: Color }) + sunColor = environmentDefaults.sunColor.clone(); + + @EditorProperty({ type: Vector3 }) + sunPosition = environmentDefaults.sunPosition.clone(); + + @EditorProperty({ type: Number }) + sunStrength = environmentDefaults.sunStrength; + + @EditorProperty({ type: Color }) + ambientColor = environmentDefaults.ambientColor.clone(); + + @EditorProperty({ type: Number }) + ambientStrength = environmentDefaults.ambientStrength; + + @EditorProperty({ type: Color }) + clearColor = environmentDefaults.clearColor.clone(); override serialize() { return super.serialize() as SerializedEnvironment; diff --git a/packages/engine/src/gameobjects/world.object.ts b/packages/engine/src/gameobjects/world.object.ts index a71246c..f5c71a6 100644 --- a/packages/engine/src/gameobjects/world.object.ts +++ b/packages/engine/src/gameobjects/world.object.ts @@ -5,7 +5,11 @@ export class World extends GameObject { public name = 'World'; public virtual = true; + override get properties() { + return []; + } + constructor() { - super({}); + super(); } } diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 198d8d4..c810313 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -5,3 +5,4 @@ export * from './components'; export * from './gameobjects'; export * from './assets'; export * from './defaults/environment'; +export * from './decorators'; diff --git a/packages/engine/src/types/game-object.ts b/packages/engine/src/types/game-object.ts index b78aaf5..0727409 100644 --- a/packages/engine/src/types/game-object.ts +++ b/packages/engine/src/types/game-object.ts @@ -1,31 +1,35 @@ import { Color, Euler, Object3D, Vector3 } from 'three'; import { Property } from './property'; - -export type EditorProperties = { [x: string]: Property }; -export const gameObjectEditorProperties: EditorProperties = { - name: new Property({ name: 'name', type: String, exposed: true }), - visible: new Property({ name: 'visible', type: Boolean, exposed: true }), -}; - -export const gameObject3DEditorProperties: EditorProperties = { - ...gameObjectEditorProperties, - locked: new Property({ name: 'locked', type: Boolean, exposed: true }), - position: new Property({ name: 'position', type: Vector3, exposed: true }), - scale: new Property({ name: 'scale', type: Vector3, exposed: true }), - rotation: new Property({ name: 'rotation', type: Euler, exposed: true }), -}; +import { EditorProperty } from '../decorators/property'; +import { readMetadataOf } from '../utils/read-metadata'; export class GameObject extends Object3D { public objectType = 'GameObject'; public virtual = false; - constructor( - public editorProperties: EditorProperties = gameObjectEditorProperties - ) { + @EditorProperty({ type: String }) + public override name: string = ''; + @EditorProperty({ type: Boolean }) + public override visible: boolean = true; + + constructor() { super(); this.name = this.objectType; } + private get excludedProperties() { + return readMetadataOf(this, 'excludedProperties'); + } + + /** The exposed properties for this game object, used for the editor */ + get properties() { + const exclude = this.excludedProperties; + const properties = readMetadataOf(this, 'properties'); + return properties.filter( + (item) => !exclude.includes(item.definition.name!) + ); + } + /** * Serialize GameObject for exporting */ @@ -39,7 +43,7 @@ export class GameObject extends Object3D { visible: this.visible, }; - const keys = Object.keys(this.editorProperties); + const keys = this.properties.map((property) => property.definition.name!); Object.assign( object, @@ -61,7 +65,8 @@ export class GameObject extends Object3D { override copy(object: Object3D, recursive = true) { super.copy(object as any, recursive); - Object.keys(this.editorProperties) + this.properties + .map((property) => property.definition.name!) .filter((key) => !['position', 'rotation', 'scale'].includes(key)) .forEach((key) => { (this as any)[key] = (object as any)[key]; @@ -93,10 +98,18 @@ export class GameObject extends Object3D { export class GameObject3D extends GameObject { public objectType = 'GameObject3D'; + + @EditorProperty({ type: Boolean }) public locked = false; - constructor(public editorProperties = gameObject3DEditorProperties) { - super(editorProperties); - } + + @EditorProperty({ type: Vector3 }) + public override position!: Vector3; + + @EditorProperty({ type: Vector3 }) + public override scale!: Vector3; + + @EditorProperty({ type: Euler }) + public override rotation!: Euler; } export interface SerializedObject { diff --git a/packages/engine/src/types/property.ts b/packages/engine/src/types/property.ts index af76aa3..75cd4c4 100644 --- a/packages/engine/src/types/property.ts +++ b/packages/engine/src/types/property.ts @@ -1,8 +1,8 @@ import { Material, Object3D } from 'three'; export interface PropertyDefinition { - name: string; - type: any; + name?: string; + type?: any; description?: string; exposed?: boolean; validators?: Function[]; diff --git a/packages/engine/src/utils/index.ts b/packages/engine/src/utils/index.ts index 82ee93f..e8a56d7 100644 --- a/packages/engine/src/utils/index.ts +++ b/packages/engine/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './clamp'; export * from './debounce'; export * from './events'; +export * from './read-metadata'; diff --git a/packages/engine/src/utils/read-metadata.ts b/packages/engine/src/utils/read-metadata.ts new file mode 100644 index 0000000..28f5f16 --- /dev/null +++ b/packages/engine/src/utils/read-metadata.ts @@ -0,0 +1,10 @@ +export const readMetadataOf = (object: any, key: string) => { + let metadata: T[] = []; + let target = Object.getPrototypeOf(object); + while (target != Object.prototype) { + let childFields = Reflect.getOwnMetadata(key, target) || []; + metadata.unshift(...childFields); + target = Object.getPrototypeOf(target); + } + return metadata; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7713637..6971263 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,9 @@ importers: packages/engine: dependencies: + reflect-metadata: + specifier: ^0.1.13 + version: 0.1.13 three: specifier: ^0.153.0 version: 0.153.0 @@ -1028,6 +1031,10 @@ packages: dependencies: picomatch: 2.3.1 + /reflect-metadata@0.1.13: + resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + dev: false + /resolve@1.19.0: resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} dependencies: diff --git a/tsconfig.json b/tsconfig.json index e2451ff..ca7f3fb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,8 @@ "declaration": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, /* Linting */ "strict": true, "skipLibCheck": true,