tweaks, comments
This commit is contained in:
parent
ce58cfaa01
commit
9ea5b6910b
|
@ -50,6 +50,7 @@ onBeforeUnmount(() => {
|
|||
.editor-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ watch(modelValue, (value, oldValue) => {
|
|||
&-label {
|
||||
padding: 8px;
|
||||
text-transform: capitalize;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -60,6 +60,7 @@ const changed = ($event: Event) => {
|
|||
&-label {
|
||||
padding: 8px;
|
||||
text-transform: capitalize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-input {
|
||||
|
|
|
@ -54,6 +54,7 @@ const changed = () => {
|
|||
&-label {
|
||||
padding: 8px;
|
||||
text-transform: capitalize;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -46,26 +46,39 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Color, Vector3 } from 'three';
|
||||
import { Color, Euler, MathUtils, Vector3 } from 'three';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
value: Vector3 | Color;
|
||||
type?: 'vector' | 'color';
|
||||
value: Vector3 | Color | Euler;
|
||||
type?: 'vector' | 'color' | 'euler';
|
||||
}>();
|
||||
|
||||
const toVector = computed(() =>
|
||||
props.value instanceof Color
|
||||
? new Vector3(props.value.r * 255, props.value.g * 255, props.value.b * 255)
|
||||
: props.value.clone()
|
||||
);
|
||||
const toVector = computed<Vector3>(() => {
|
||||
if (props.type === 'color') {
|
||||
const asColor = props.value as Color;
|
||||
return new Vector3(asColor.r * 255, asColor.g * 255, asColor.b * 255);
|
||||
}
|
||||
|
||||
if (props.type === 'euler') {
|
||||
const asEuler = props.value as Euler;
|
||||
return new Vector3(
|
||||
MathUtils.radToDeg(asEuler.x),
|
||||
MathUtils.radToDeg(asEuler.y),
|
||||
MathUtils.radToDeg(asEuler.z)
|
||||
);
|
||||
}
|
||||
|
||||
return props.value.clone() as Vector3;
|
||||
});
|
||||
|
||||
const modelValueX = ref(toVector.value.x);
|
||||
const modelValueY = ref(toVector.value.y);
|
||||
const modelValueZ = ref(toVector.value.z);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update', value: Vector3 | Color): void;
|
||||
(e: 'update', value: Vector3 | Color | Euler): void;
|
||||
}>();
|
||||
|
||||
const names = computed(() =>
|
||||
|
@ -87,6 +100,19 @@ const changed = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (props.type === 'euler') {
|
||||
emit(
|
||||
'update',
|
||||
new Euler(
|
||||
MathUtils.degToRad(x),
|
||||
MathUtils.degToRad(y),
|
||||
MathUtils.degToRad(z),
|
||||
'XYZ'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit('update', new Vector3(x, y, z));
|
||||
};
|
||||
</script>
|
||||
|
@ -99,11 +125,13 @@ const changed = () => {
|
|||
&-label {
|
||||
padding: 8px;
|
||||
text-transform: capitalize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-label-sub {
|
||||
padding: 8px 4px;
|
||||
width: 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-pieces {
|
||||
|
|
|
@ -17,7 +17,7 @@ import { GameObject } from '@freeblox/engine';
|
|||
import type { Component } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import Field from '../form/Field.vue';
|
||||
import { Color, Vector3 } from 'three';
|
||||
import { Color, Euler, Vector3 } from 'three';
|
||||
import Vector3Field from '../form/Vector3Field.vue';
|
||||
import Checkbox from '../form/Checkbox.vue';
|
||||
import ColorPicker from '../form/ColorPicker.vue';
|
||||
|
@ -60,10 +60,11 @@ const formFields = computed(() => {
|
|||
});
|
||||
}
|
||||
|
||||
if (property.type === Vector3) {
|
||||
if (property.type === Vector3 || property.type === Euler) {
|
||||
fields.push({
|
||||
name: property.name,
|
||||
value: (object as unknown as Record<string, unknown>)[property.name],
|
||||
type: property.type === Vector3 ? 'vector' : 'euler',
|
||||
component: Vector3Field,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: #5a5a5a;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-inner {
|
||||
|
|
|
@ -54,6 +54,7 @@ const filtered = (items: Object3D[]) =>
|
|||
|
||||
&-button {
|
||||
background-color: #efefef;
|
||||
user-select: none;
|
||||
appearance: none;
|
||||
padding: 8px;
|
||||
border: 0;
|
||||
|
|
|
@ -84,14 +84,25 @@ export class Editor extends GameRunner {
|
|||
this.shortcuts.cleanUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current scene tree for the level, starting with two virtual
|
||||
* objects (world and environment).
|
||||
*/
|
||||
public getSceneTree() {
|
||||
return this.level.getSceneTree();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize edited world into distributable format (JSON).
|
||||
* @param name World name
|
||||
*/
|
||||
public export(name: string) {
|
||||
return this.level.serializeLevelSave(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected objects.
|
||||
*/
|
||||
public getSelection() {
|
||||
return this.workspace.selection;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,9 @@ import { Euler, Object3D, Vector3 } from 'three';
|
|||
type Changes = Record<string, unknown>;
|
||||
type HistoryType = [Object, Changes, number, number?];
|
||||
|
||||
/**
|
||||
* Manages history (undo, redo) for editor operations.
|
||||
*/
|
||||
export class HistoryComponent extends EngineComponent {
|
||||
public cleanUpEvents?: Function;
|
||||
private history: HistoryType[] = [];
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { EngineComponent, EventEmitter, Renderer } from '@freeblox/engine';
|
||||
import { EditorEvents } from '..';
|
||||
|
||||
/**
|
||||
* Provides editing shortcuts for the editor.
|
||||
*/
|
||||
export class ShotcutsComponent extends EngineComponent {
|
||||
public cleanUpEvents?: Function;
|
||||
|
||||
|
|
|
@ -33,6 +33,12 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
|
||||
import { World } from '@freeblox/engine/dist/gameobjects/world.object';
|
||||
|
||||
/**
|
||||
* This component does most of the work related to editing the level.
|
||||
* Acts as a middle man for most events.
|
||||
*
|
||||
* Most importantly, handles selection, the clipboard and the editor scene.
|
||||
*/
|
||||
export class WorkspaceComponent extends EngineComponent {
|
||||
public background = new Object3D();
|
||||
public world = new World();
|
||||
|
@ -268,9 +274,13 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
};
|
||||
|
||||
const copyEvent = () => {
|
||||
if (!this.selection.length) return;
|
||||
const filteredSelection = this.selection.filter(
|
||||
(entry) => !(entry as GameObject).virtual
|
||||
);
|
||||
if (!filteredSelection.length) return;
|
||||
|
||||
this.clipboard = [];
|
||||
this.selection.forEach((entry) => {
|
||||
filteredSelection.forEach((entry) => {
|
||||
this.clipboard.push(entry);
|
||||
});
|
||||
};
|
||||
|
@ -278,11 +288,13 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
const pasteEvent = (target?: Object3D) => {
|
||||
if (!this.clipboard.length) return;
|
||||
this.selection.length = 0;
|
||||
|
||||
this.clipboard.forEach((entry) => {
|
||||
const newObject = this.cutOperation ? entry : entry.clone();
|
||||
(target || this.world).add(newObject);
|
||||
this.selection.push(newObject);
|
||||
});
|
||||
|
||||
this.selection.forEach((entry) =>
|
||||
this.events.emit('selected', {
|
||||
object: entry,
|
||||
|
@ -295,30 +307,48 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
};
|
||||
|
||||
const deleteEvent = () => {
|
||||
if (!this.selection.length) return;
|
||||
const toDelete = [...this.selection];
|
||||
this.selection.forEach((entry) =>
|
||||
const filteredSelection = this.selection.filter(
|
||||
(entry) => !(entry as GameObject).virtual
|
||||
);
|
||||
if (!filteredSelection.length) return;
|
||||
|
||||
const toDelete = [...filteredSelection];
|
||||
|
||||
filteredSelection.forEach((entry) =>
|
||||
this.events.emit('deselected', {
|
||||
object: entry,
|
||||
selection: [],
|
||||
})
|
||||
);
|
||||
|
||||
this.events.emit('remove', {
|
||||
object: toDelete,
|
||||
});
|
||||
|
||||
this.selection.length = 0;
|
||||
};
|
||||
|
||||
const cutEvent = () => {
|
||||
if (!this.selection.length) return;
|
||||
const filteredSelection = this.selection.filter(
|
||||
(entry) => !(entry as GameObject).virtual
|
||||
);
|
||||
|
||||
if (!filteredSelection.length) return;
|
||||
|
||||
this.clipboard = [];
|
||||
this.selection.forEach((entry) => {
|
||||
|
||||
filteredSelection.forEach((entry) => {
|
||||
this.clipboard.push(entry);
|
||||
});
|
||||
|
||||
this.cutOperation = true;
|
||||
deleteEvent();
|
||||
};
|
||||
|
||||
const undoEvent = () => {
|
||||
if (this.cutOperation) this.cutOperation = false;
|
||||
};
|
||||
|
||||
this.events.addListener('mouseDown', mouseDownEventHandler);
|
||||
this.events.addListener('mouseMove', mouseMoveEventHandler);
|
||||
this.events.addListener('mouseUp', mouseUpEventHandler);
|
||||
|
@ -333,6 +363,7 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
this.events.addListener('copy', copyEvent);
|
||||
this.events.addListener('paste', pasteEvent);
|
||||
this.events.addListener('delete', deleteEvent);
|
||||
this.events.addListener('undo', undoEvent);
|
||||
|
||||
return () => {
|
||||
this.events.removeEventListener('mouseDown', mouseDownEventHandler);
|
||||
|
@ -352,6 +383,7 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
this.events.removeEventListener('copy', copyEvent);
|
||||
this.events.removeEventListener('paste', pasteEvent);
|
||||
this.events.removeEventListener('delete', deleteEvent);
|
||||
this.events.removeEventListener('undo', undoEvent);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { EngineComponent } from '../types/engine-component';
|
|||
import {
|
||||
ChangeEvent,
|
||||
EngineEvents,
|
||||
InstanceEvent,
|
||||
RemoveEvent,
|
||||
ReparentEvent,
|
||||
} from '../types/events';
|
||||
|
@ -11,6 +12,9 @@ import { Environment } from '../gameobjects/environment.object';
|
|||
import { assetManager } from '../assets/manager';
|
||||
import { WorldFile } from '../types/world-file';
|
||||
import { World } from '../gameobjects/world.object';
|
||||
import { instancableGameObjects } from '../gameobjects';
|
||||
import { Object3D } from 'three';
|
||||
import { GameObject } from '../types/game-object';
|
||||
|
||||
/**
|
||||
* Game level management component
|
||||
|
@ -51,6 +55,15 @@ export class LevelComponent extends EngineComponent {
|
|||
};
|
||||
}
|
||||
|
||||
public createObject(object: string, setParent?: Object3D) {
|
||||
const parent = setParent || this.world;
|
||||
const ObjectType = instancableGameObjects[object];
|
||||
if (!ObjectType) return;
|
||||
const newObject = new ObjectType();
|
||||
parent.add(newObject);
|
||||
this.events.emit('sceneJoin', newObject);
|
||||
}
|
||||
|
||||
public serializeLevelSave(name: string): WorldFile {
|
||||
const world = this.world.serialize();
|
||||
const environment = this.environment.serialize();
|
||||
|
@ -73,12 +86,14 @@ export class LevelComponent extends EngineComponent {
|
|||
|
||||
const removeEvent = (event: RemoveEvent) => {
|
||||
if (event.applied || !event.object) return;
|
||||
if ((event.object as GameObject).virtual) return;
|
||||
if (Array.isArray(event.object)) {
|
||||
event.object.forEach((object) => object.removeFromParent());
|
||||
return;
|
||||
}
|
||||
|
||||
event.object.removeFromParent();
|
||||
this.events.emit('sceneLeave', event.object);
|
||||
};
|
||||
|
||||
const reparentEvent = (event: ReparentEvent) => {
|
||||
|
@ -95,14 +110,19 @@ export class LevelComponent extends EngineComponent {
|
|||
event.parent.add(event.object);
|
||||
};
|
||||
|
||||
const instanceEvent = (event: InstanceEvent) =>
|
||||
this.createObject(event.type, event.parent);
|
||||
|
||||
this.events.addListener('change', changeEvent);
|
||||
this.events.addListener('remove', removeEvent);
|
||||
this.events.addListener('reparent', reparentEvent);
|
||||
this.events.addListener('instance', instanceEvent);
|
||||
|
||||
return () => {
|
||||
this.events.removeEventListener('change', changeEvent);
|
||||
this.events.removeEventListener('remove', removeEvent);
|
||||
this.events.removeEventListener('reparent', reparentEvent);
|
||||
this.events.removeEventListener('instance', instanceEvent);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,11 @@ export interface ReparentEvent {
|
|||
applied?: boolean;
|
||||
}
|
||||
|
||||
export interface InstanceEvent {
|
||||
type: string;
|
||||
parent?: Object3D;
|
||||
}
|
||||
|
||||
export type EngineEvents = {
|
||||
error: (error: Error) => void;
|
||||
mouseDown: (event: MouseButtonEvent) => void;
|
||||
|
@ -70,5 +75,8 @@ export type EngineEvents = {
|
|||
resize: (event: Vector2) => void;
|
||||
remove: (event: RemoveEvent) => void;
|
||||
reparent: (event: ReparentEvent) => void;
|
||||
instance: (event: InstanceEvent) => void;
|
||||
sceneJoin: (event: Object3D) => void;
|
||||
sceneLeave: (event: Object3D) => void;
|
||||
initialized: () => void;
|
||||
};
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export type Instancable<T> = { new (): T } | Function;
|
||||
export interface Instancable<T> {
|
||||
new (): T;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue