editor properties as meta decorators
This commit is contained in:
parent
068f7ec2cc
commit
759e67ddc6
|
@ -60,18 +60,18 @@ const formFields = computed(() => {
|
||||||
if (!object) return [];
|
if (!object) return [];
|
||||||
const fields: FormItem[] = [];
|
const fields: FormItem[] = [];
|
||||||
|
|
||||||
Object.values(object.editorProperties)
|
object.properties
|
||||||
.filter((item) => item.definition.exposed)
|
.filter((item) => item.definition.exposed && item.definition.name)
|
||||||
.forEach((property) => {
|
.forEach((property) => {
|
||||||
if (
|
if (
|
||||||
property.definition.type === String ||
|
property.definition.type === String ||
|
||||||
property.definition.type === Number
|
property.definition.type === Number
|
||||||
) {
|
) {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: property.definition.name,
|
name: property.definition.name!,
|
||||||
label: toCapitalizedWords(property.definition.name),
|
label: toCapitalizedWords(property.definition.name!),
|
||||||
value: (object as unknown as Record<string, unknown>)[
|
value: (object as unknown as Record<string, unknown>)[
|
||||||
property.definition.name
|
property.definition.name!
|
||||||
],
|
],
|
||||||
type: property.definition.type === String ? 'string' : 'number',
|
type: property.definition.type === String ? 'string' : 'number',
|
||||||
component: Field,
|
component: Field,
|
||||||
|
@ -83,10 +83,10 @@ const formFields = computed(() => {
|
||||||
property.definition.type === Euler
|
property.definition.type === Euler
|
||||||
) {
|
) {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: property.definition.name,
|
name: property.definition.name!,
|
||||||
label: toCapitalizedWords(property.definition.name),
|
label: toCapitalizedWords(property.definition.name!),
|
||||||
value: (object as unknown as Record<string, unknown>)[
|
value: (object as unknown as Record<string, unknown>)[
|
||||||
property.definition.name
|
property.definition.name!
|
||||||
],
|
],
|
||||||
type: property.definition.type === Vector3 ? 'vector' : 'euler',
|
type: property.definition.type === Vector3 ? 'vector' : 'euler',
|
||||||
component: Vector3Field,
|
component: Vector3Field,
|
||||||
|
@ -95,10 +95,10 @@ const formFields = computed(() => {
|
||||||
|
|
||||||
if (property.definition.type === Color) {
|
if (property.definition.type === Color) {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: property.definition.name,
|
name: property.definition.name!,
|
||||||
label: toCapitalizedWords(property.definition.name),
|
label: toCapitalizedWords(property.definition.name!),
|
||||||
value: (object as unknown as Record<string, unknown>)[
|
value: (object as unknown as Record<string, unknown>)[
|
||||||
property.definition.name
|
property.definition.name!
|
||||||
],
|
],
|
||||||
component: ColorPicker,
|
component: ColorPicker,
|
||||||
});
|
});
|
||||||
|
@ -106,10 +106,10 @@ const formFields = computed(() => {
|
||||||
|
|
||||||
if (property.definition.type === Boolean) {
|
if (property.definition.type === Boolean) {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: property.definition.name,
|
name: property.definition.name!,
|
||||||
label: toCapitalizedWords(property.definition.name),
|
label: toCapitalizedWords(property.definition.name!),
|
||||||
value: (object as unknown as Record<string, unknown>)[
|
value: (object as unknown as Record<string, unknown>)[
|
||||||
property.definition.name
|
property.definition.name!
|
||||||
],
|
],
|
||||||
component: Checkbox,
|
component: Checkbox,
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.0.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
"three": "^0.153.0"
|
"three": "^0.153.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './property';
|
|
@ -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<Property> = 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<string> = Reflect.getOwnMetadata(
|
||||||
|
'excludedProperties',
|
||||||
|
target
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!excluded) {
|
||||||
|
Reflect.defineMetadata('excludedProperties', (excluded = []), target);
|
||||||
|
}
|
||||||
|
|
||||||
|
excluded.push(String(propertyKey));
|
||||||
|
};
|
||||||
|
}
|
|
@ -5,25 +5,11 @@ import {
|
||||||
Mesh,
|
Mesh,
|
||||||
MeshPhongMaterial,
|
MeshPhongMaterial,
|
||||||
} from 'three';
|
} from 'three';
|
||||||
import {
|
import { GameObject3D } from '../types/game-object';
|
||||||
EditorProperties,
|
|
||||||
GameObject3D,
|
|
||||||
gameObject3DEditorProperties,
|
|
||||||
} from '../types/game-object';
|
|
||||||
import { Property } from '../types/property';
|
|
||||||
import { gameObjectGeometries } from './geometries';
|
import { gameObjectGeometries } from './geometries';
|
||||||
import { assetManager } from '../assets/manager';
|
import { assetManager } from '../assets/manager';
|
||||||
import { AssetInfo } from '../types/asset';
|
import { AssetInfo } from '../types/asset';
|
||||||
|
import { EditorProperty } from '../decorators/property';
|
||||||
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 }),
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Brick extends GameObject3D {
|
export class Brick extends GameObject3D {
|
||||||
public objectType = Brick.name;
|
public objectType = Brick.name;
|
||||||
|
@ -31,19 +17,7 @@ export class Brick extends GameObject3D {
|
||||||
protected material = new MeshPhongMaterial();
|
protected material = new MeshPhongMaterial();
|
||||||
protected mesh: Mesh = new Mesh(this.geometry, this.material);
|
protected mesh: Mesh = new Mesh(this.geometry, this.material);
|
||||||
|
|
||||||
public canCollide = true;
|
@EditorProperty({ type: Color })
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
get color() {
|
get color() {
|
||||||
return this.material.color;
|
return this.material.color;
|
||||||
}
|
}
|
||||||
|
@ -51,6 +25,7 @@ export class Brick extends GameObject3D {
|
||||||
this.material.color = new Color(color);
|
this.material.color = new Color(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EditorProperty({ type: Number })
|
||||||
get transparency() {
|
get transparency() {
|
||||||
return 1 - this.material.opacity;
|
return 1 - this.material.opacity;
|
||||||
}
|
}
|
||||||
|
@ -60,6 +35,10 @@ export class Brick extends GameObject3D {
|
||||||
this.material.needsUpdate = true;
|
this.material.needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EditorProperty({ type: AssetInfo })
|
||||||
|
get texture() {
|
||||||
|
return this.texturePath;
|
||||||
|
}
|
||||||
set texture(path: string | undefined) {
|
set texture(path: string | undefined) {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
this.material.map = null;
|
this.material.map = null;
|
||||||
|
@ -76,7 +55,21 @@ export class Brick extends GameObject3D {
|
||||||
this.texturePath = path;
|
this.texturePath = path;
|
||||||
this.material.map = asset.texture;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,34 @@
|
||||||
import { Color, Vector3 } from 'three';
|
import { Color, Vector3 } from 'three';
|
||||||
import {
|
import { GameObject, SerializedObject } from '../types/game-object';
|
||||||
EditorProperties,
|
|
||||||
GameObject,
|
|
||||||
SerializedObject,
|
|
||||||
} from '../types/game-object';
|
|
||||||
import { Property } from '../types/property';
|
|
||||||
import { environmentDefaults } from '../defaults/environment';
|
import { environmentDefaults } from '../defaults/environment';
|
||||||
|
import { EditorProperty, EditorPropertyExclude } from '../decorators/property';
|
||||||
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 }),
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Environment extends GameObject {
|
export class Environment extends GameObject {
|
||||||
public objectType = Environment.name;
|
public objectType = Environment.name;
|
||||||
|
@EditorPropertyExclude()
|
||||||
public name = Environment.name;
|
public name = Environment.name;
|
||||||
public virtual = true;
|
public virtual = true;
|
||||||
|
|
||||||
sunColor = environmentDefaults.sunColor.clone();
|
@EditorPropertyExclude()
|
||||||
sunPosition = environmentDefaults.sunPosition.clone();
|
public override visible!: boolean;
|
||||||
sunStrength = environmentDefaults.sunStrength;
|
|
||||||
ambientColor = environmentDefaults.ambientColor.clone();
|
|
||||||
ambientStrength = environmentDefaults.ambientStrength;
|
|
||||||
clearColor = environmentDefaults.clearColor.clone();
|
|
||||||
|
|
||||||
constructor() {
|
@EditorProperty({ type: Color })
|
||||||
super(environmentEditorProperties);
|
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() {
|
override serialize() {
|
||||||
return super.serialize() as SerializedEnvironment;
|
return super.serialize() as SerializedEnvironment;
|
||||||
|
|
|
@ -5,7 +5,11 @@ export class World extends GameObject {
|
||||||
public name = 'World';
|
public name = 'World';
|
||||||
public virtual = true;
|
public virtual = true;
|
||||||
|
|
||||||
|
override get properties() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({});
|
super();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,3 +5,4 @@ export * from './components';
|
||||||
export * from './gameobjects';
|
export * from './gameobjects';
|
||||||
export * from './assets';
|
export * from './assets';
|
||||||
export * from './defaults/environment';
|
export * from './defaults/environment';
|
||||||
|
export * from './decorators';
|
||||||
|
|
|
@ -1,31 +1,35 @@
|
||||||
import { Color, Euler, Object3D, Vector3 } from 'three';
|
import { Color, Euler, Object3D, Vector3 } from 'three';
|
||||||
import { Property } from './property';
|
import { Property } from './property';
|
||||||
|
import { EditorProperty } from '../decorators/property';
|
||||||
export type EditorProperties = { [x: string]: Property };
|
import { readMetadataOf } from '../utils/read-metadata';
|
||||||
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 }),
|
|
||||||
};
|
|
||||||
|
|
||||||
export class GameObject extends Object3D {
|
export class GameObject extends Object3D {
|
||||||
public objectType = 'GameObject';
|
public objectType = 'GameObject';
|
||||||
public virtual = false;
|
public virtual = false;
|
||||||
|
|
||||||
constructor(
|
@EditorProperty({ type: String })
|
||||||
public editorProperties: EditorProperties = gameObjectEditorProperties
|
public override name: string = '';
|
||||||
) {
|
@EditorProperty({ type: Boolean })
|
||||||
|
public override visible: boolean = true;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.name = this.objectType;
|
this.name = this.objectType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get excludedProperties() {
|
||||||
|
return readMetadataOf<string>(this, 'excludedProperties');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The exposed properties for this game object, used for the editor */
|
||||||
|
get properties() {
|
||||||
|
const exclude = this.excludedProperties;
|
||||||
|
const properties = readMetadataOf<Property>(this, 'properties');
|
||||||
|
return properties.filter(
|
||||||
|
(item) => !exclude.includes(item.definition.name!)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize GameObject for exporting
|
* Serialize GameObject for exporting
|
||||||
*/
|
*/
|
||||||
|
@ -39,7 +43,7 @@ export class GameObject extends Object3D {
|
||||||
visible: this.visible,
|
visible: this.visible,
|
||||||
};
|
};
|
||||||
|
|
||||||
const keys = Object.keys(this.editorProperties);
|
const keys = this.properties.map((property) => property.definition.name!);
|
||||||
|
|
||||||
Object.assign(
|
Object.assign(
|
||||||
object,
|
object,
|
||||||
|
@ -61,7 +65,8 @@ export class GameObject extends Object3D {
|
||||||
|
|
||||||
override copy(object: Object3D, recursive = true) {
|
override copy(object: Object3D, recursive = true) {
|
||||||
super.copy(object as any, recursive);
|
super.copy(object as any, recursive);
|
||||||
Object.keys(this.editorProperties)
|
this.properties
|
||||||
|
.map((property) => property.definition.name!)
|
||||||
.filter((key) => !['position', 'rotation', 'scale'].includes(key))
|
.filter((key) => !['position', 'rotation', 'scale'].includes(key))
|
||||||
.forEach((key) => {
|
.forEach((key) => {
|
||||||
(this as any)[key] = (object as any)[key];
|
(this as any)[key] = (object as any)[key];
|
||||||
|
@ -93,10 +98,18 @@ export class GameObject extends Object3D {
|
||||||
|
|
||||||
export class GameObject3D extends GameObject {
|
export class GameObject3D extends GameObject {
|
||||||
public objectType = 'GameObject3D';
|
public objectType = 'GameObject3D';
|
||||||
|
|
||||||
|
@EditorProperty({ type: Boolean })
|
||||||
public locked = false;
|
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 {
|
export interface SerializedObject {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Material, Object3D } from 'three';
|
import { Material, Object3D } from 'three';
|
||||||
|
|
||||||
export interface PropertyDefinition {
|
export interface PropertyDefinition {
|
||||||
name: string;
|
name?: string;
|
||||||
type: any;
|
type?: any;
|
||||||
description?: string;
|
description?: string;
|
||||||
exposed?: boolean;
|
exposed?: boolean;
|
||||||
validators?: Function[];
|
validators?: Function[];
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './clamp';
|
export * from './clamp';
|
||||||
export * from './debounce';
|
export * from './debounce';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
|
export * from './read-metadata';
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export const readMetadataOf = <T>(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;
|
||||||
|
};
|
|
@ -81,6 +81,9 @@ importers:
|
||||||
|
|
||||||
packages/engine:
|
packages/engine:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
reflect-metadata:
|
||||||
|
specifier: ^0.1.13
|
||||||
|
version: 0.1.13
|
||||||
three:
|
three:
|
||||||
specifier: ^0.153.0
|
specifier: ^0.153.0
|
||||||
version: 0.153.0
|
version: 0.153.0
|
||||||
|
@ -1028,6 +1031,10 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
/reflect-metadata@0.1.13:
|
||||||
|
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/resolve@1.19.0:
|
/resolve@1.19.0:
|
||||||
resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==}
|
resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
Loading…
Reference in New Issue