tweaks, comments

This commit is contained in:
Evert Prants 2023-06-04 20:23:41 +03:00
parent ce58cfaa01
commit 9ea5b6910b
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
15 changed files with 133 additions and 19 deletions

View File

@ -50,6 +50,7 @@ onBeforeUnmount(() => {
.editor-row {
display: flex;
flex-direction: row;
overflow: hidden;
width: 100%;
height: 100%;
}

View File

@ -51,6 +51,7 @@ watch(modelValue, (value, oldValue) => {
&-label {
padding: 8px;
text-transform: capitalize;
user-select: none;
}
}
</style>

View File

@ -60,6 +60,7 @@ const changed = ($event: Event) => {
&-label {
padding: 8px;
text-transform: capitalize;
user-select: none;
}
&-input {

View File

@ -54,6 +54,7 @@ const changed = () => {
&-label {
padding: 8px;
text-transform: capitalize;
user-select: none;
}
}
</style>

View File

@ -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 {

View File

@ -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,
});
}

View File

@ -17,6 +17,7 @@
text-transform: uppercase;
font-weight: bold;
color: #5a5a5a;
user-select: none;
}
&-inner {

View File

@ -54,6 +54,7 @@ const filtered = (items: Object3D[]) =>
&-button {
background-color: #efefef;
user-select: none;
appearance: none;
padding: 8px;
border: 0;

View File

@ -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;
}

View File

@ -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[] = [];

View File

@ -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;

View File

@ -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);
};
}

View File

@ -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);
};
}
}

View File

@ -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;
};

View File

@ -1 +1,3 @@
export type Instancable<T> = { new (): T } | Function;
export interface Instancable<T> {
new (): T;
}