working on texture picker
This commit is contained in:
parent
759e67ddc6
commit
4c44c7923f
|
@ -14,13 +14,14 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { Editor } from '../editor';
|
||||
import { Editor, SelectionEvent } from '../editor';
|
||||
import Menu from './menu/Menu.vue';
|
||||
import { WorldFile, instancableGameObjects } from '@freeblox/engine';
|
||||
import { useEditorEvents } from '../composables/use-editor-events';
|
||||
import { exportToFile } from '../utils/export-file';
|
||||
import { readFileToString } from '../utils/read-file';
|
||||
|
||||
const selection = ref(false);
|
||||
const currentlyOpen = ref<string | undefined>(undefined);
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -31,7 +32,7 @@ const emit = defineEmits<{
|
|||
(e: 'update'): void;
|
||||
}>();
|
||||
|
||||
// const { register } = useEditorEvents(props.editor);
|
||||
const { register } = useEditorEvents(props.editor);
|
||||
|
||||
const fileMenu = [
|
||||
{
|
||||
|
@ -68,12 +69,14 @@ const editMenu = computed(() => [
|
|||
id: 'cut',
|
||||
label: 'Cut',
|
||||
shortcut: 'CTRL+X',
|
||||
disabled: !selection.value,
|
||||
onClick: () => props.editor.events.emit('cut'),
|
||||
},
|
||||
{
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
shortcut: 'CTRL+C',
|
||||
disabled: !selection.value,
|
||||
onClick: () => props.editor.events.emit('copy'),
|
||||
},
|
||||
{
|
||||
|
@ -86,12 +89,14 @@ const editMenu = computed(() => [
|
|||
id: 'duplicate',
|
||||
label: 'Duplicate',
|
||||
shortcut: 'CTRL+D',
|
||||
disabled: !selection.value,
|
||||
onClick: () => props.editor.events.emit('duplicate'),
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
label: 'Delete',
|
||||
shortcut: 'Delete',
|
||||
disabled: !selection.value,
|
||||
onClick: () => props.editor.events.emit('delete'),
|
||||
},
|
||||
]);
|
||||
|
@ -138,6 +143,13 @@ const loadLevelFromFile = async () => {
|
|||
const obj = JSON.parse(data) as WorldFile;
|
||||
props.editor.load(obj);
|
||||
};
|
||||
|
||||
const setSelectionPreset = (event: SelectionEvent) => {
|
||||
selection.value = !!event.selection?.length;
|
||||
};
|
||||
|
||||
register('selected', setSelectionPreset);
|
||||
register('deselected', setSelectionPreset);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="editor">
|
||||
<Toolbar :editor="editorRef" />
|
||||
<EditorToolbar :editor="editorRef" />
|
||||
<TransformControls :editor="editorRef" />
|
||||
<div class="editor-row">
|
||||
<div class="viewport">
|
||||
|
@ -8,15 +8,17 @@
|
|||
</div>
|
||||
<EditorSidebar :editor="editorRef" />
|
||||
</div>
|
||||
<EditorAssets :editor="editorRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
||||
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
||||
import { Editor } from '../editor';
|
||||
import Toolbar from './Toolbar.vue';
|
||||
import EditorToolbar from './EditorToolbar.vue';
|
||||
import TransformControls from './TransformControls.vue';
|
||||
import EditorSidebar from './EditorSidebar.vue';
|
||||
import EditorAssets from './assets/EditorAssets.vue';
|
||||
|
||||
const wrapperRef = ref();
|
||||
const editorRef = shallowRef<Editor>(new Editor());
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
<template>
|
||||
<div class="assets">
|
||||
<ul class="toolbar">
|
||||
<Menu
|
||||
v-for="item of tabs"
|
||||
:id="item.id"
|
||||
:open="activeTab === item.id"
|
||||
@menuClick="clickTab(item.id)"
|
||||
>{{ item.label }}</Menu
|
||||
>
|
||||
</ul>
|
||||
<div class="assets-content">
|
||||
<div class="assets-content-inner">
|
||||
<template v-for="asset of assets">
|
||||
<button type="button" class="asset-btn asset-btn-texture">
|
||||
<div class="preview-wrapper">
|
||||
<img :src="asset.data" class="preview" />
|
||||
</div>
|
||||
<span>{{ asset.name }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
type="button"
|
||||
class="asset-btn asset-btn-add"
|
||||
@click="uploadTexture"
|
||||
>
|
||||
<FileUploadSvg />
|
||||
<span>New Texture</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, shallowRef } from 'vue';
|
||||
import { useEditorEvents } from '../../composables/use-editor-events';
|
||||
import { Editor } from '../../editor';
|
||||
import FileUploadSvg from '../../icons/file-upload.svg.vue';
|
||||
import Menu from '../menu/Menu.vue';
|
||||
import { readFileToString } from '../../utils/read-file';
|
||||
import { Asset, assetManager } from '@freeblox/engine';
|
||||
|
||||
const props = defineProps<{
|
||||
editor: Editor;
|
||||
}>();
|
||||
|
||||
const { register } = useEditorEvents(props.editor);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update'): void;
|
||||
}>();
|
||||
|
||||
const assets = shallowRef<Asset[]>(assetManager.assets);
|
||||
const activeTab = ref('textures');
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
id: 'textures',
|
||||
label: 'Textures',
|
||||
},
|
||||
]);
|
||||
|
||||
const clickTab = (tabId: string) => {
|
||||
activeTab.value = tabId;
|
||||
};
|
||||
|
||||
const updateRef = async () => {
|
||||
assets.value = [];
|
||||
await nextTick();
|
||||
assets.value = assetManager.assets;
|
||||
};
|
||||
|
||||
const uploadTexture = async () => {
|
||||
const read = await readFileToString('image/*', true);
|
||||
const asset = await assetManager.createAsset(read, 'Texture', 'Texture');
|
||||
updateRef();
|
||||
};
|
||||
|
||||
register('loadComplete', () => updateRef());
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.assets {
|
||||
--asset-width: 376px;
|
||||
--asset-height: 140px;
|
||||
--asset-toolbar-height: 36px;
|
||||
--asset-content-height: calc(
|
||||
var(--asset-height) - var(--asset-toolbar-height)
|
||||
);
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: calc(70% - var(--asset-width));
|
||||
margin-left: 20px;
|
||||
height: var(--asset-height);
|
||||
background-color: #f7f7f7;
|
||||
overflow: hidden;
|
||||
|
||||
&-toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: var(--asset-toolbar-height);
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
&-content {
|
||||
overflow: auto;
|
||||
height: var(--asset-content-height);
|
||||
width: 100%;
|
||||
|
||||
&-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.asset-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
height: calc(var(--asset-content-height) - 8px);
|
||||
width: calc(var(--asset-content-height) - 8px);
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
margin: 4px 0;
|
||||
|
||||
&-add {
|
||||
border: 4px dashed #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&-texture {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.preview-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #ddd;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<div class="form-field-asset">
|
||||
<label class="form-field-asset-label" :for="id">{{ label }}</label>
|
||||
<div class="form-field-asset-input">
|
||||
<Menu
|
||||
:id="id"
|
||||
class="form-field-asset-button"
|
||||
:items="pickable"
|
||||
overflowing
|
||||
position="top-right"
|
||||
>Assign Texture</Menu
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { assetManager } from '@freeblox/engine';
|
||||
import { computed, ref } from 'vue';
|
||||
import AssetPickerAsset from './AssetPickerAsset.vue';
|
||||
import Menu from '../menu/Menu.vue';
|
||||
import { MenuItem } from '../../types/menu.interface';
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
label: string;
|
||||
value?: string;
|
||||
}>();
|
||||
|
||||
const modelValueId = ref(props.value);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update', value?: string): void;
|
||||
}>();
|
||||
|
||||
const id = computed(() => `form-${props.name}`);
|
||||
|
||||
const pickable = computed<MenuItem[]>(() => [
|
||||
{
|
||||
id: 'texture-none',
|
||||
label: 'None',
|
||||
onClick: () => {
|
||||
modelValueId.value = undefined;
|
||||
emit('update', undefined);
|
||||
},
|
||||
},
|
||||
...assetManager.assets.map((item) => ({
|
||||
id: item.path || item.name,
|
||||
label: item.name,
|
||||
asset: item,
|
||||
component: AssetPickerAsset,
|
||||
onClick: () => {
|
||||
modelValueId.value = item.path;
|
||||
emit('update', item.path);
|
||||
},
|
||||
})),
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.form-field-asset {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
&-label {
|
||||
padding: 8px;
|
||||
text-transform: capitalize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
&-button {
|
||||
width: 100%;
|
||||
> .menu-button {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
background-color: #ffffff;
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
margin: 2px 0;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="asset-picker-asset">
|
||||
<img :src="asset.data" />
|
||||
{{ asset.name }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Asset } from '@freeblox/engine';
|
||||
|
||||
defineProps<{
|
||||
asset: Asset;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.asset-picker-asset {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -58,8 +58,13 @@ const props = withDefaults(
|
|||
defineProps<{
|
||||
id: string;
|
||||
items?: MenuItem[];
|
||||
onClick?: () => void;
|
||||
position?: 'bottom' | 'right' | 'left';
|
||||
position?:
|
||||
| 'bottom'
|
||||
| 'bottom-right'
|
||||
| 'top'
|
||||
| 'top-right'
|
||||
| 'right'
|
||||
| 'left';
|
||||
trigger?: 'click' | 'hover' | 'none';
|
||||
top?: number;
|
||||
left?: number;
|
||||
|
@ -67,22 +72,26 @@ const props = withDefaults(
|
|||
open?: boolean;
|
||||
disabled?: boolean;
|
||||
submenu?: boolean;
|
||||
overflowing?: boolean;
|
||||
}>(),
|
||||
{
|
||||
position: 'bottom',
|
||||
trigger: 'click',
|
||||
open: false,
|
||||
overflowing: false,
|
||||
items: () => [],
|
||||
}
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle', v: boolean, t: ToggleTrigger): void;
|
||||
(e: 'menuClick'): void;
|
||||
}>();
|
||||
|
||||
const buttonRef = ref();
|
||||
const isOpen = ref(props.open);
|
||||
|
||||
const toggle = (trigger: ToggleTrigger = 'user') => {
|
||||
if (!props.items?.length) return emit('menuClick');
|
||||
isOpen.value = !isOpen.value;
|
||||
emit('toggle', isOpen.value, trigger);
|
||||
};
|
||||
|
@ -92,6 +101,7 @@ const menuClass = computed(() => [
|
|||
isOpen.value ? 'menu-wrapper--open' : 'menu-wrapper--closed',
|
||||
`menu-wrapper--${props.position}`,
|
||||
props.submenu ? 'menu-wrapper--submenu' : '',
|
||||
props.overflowing ? 'menu-wrapper--overflowing' : '',
|
||||
]);
|
||||
|
||||
const menuStyle = computed(() =>
|
||||
|
@ -155,6 +165,20 @@ watch(
|
|||
top: 100%;
|
||||
}
|
||||
|
||||
&--top > .menu-dropdown {
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
&--bottom-right > .menu-dropdown {
|
||||
top: 100%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&--top-right > .menu-dropdown {
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&--right > .menu-dropdown {
|
||||
top: 0;
|
||||
left: 100%;
|
||||
|
@ -184,6 +208,11 @@ watch(
|
|||
& > .menu-button {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&--overflowing > .menu-dropdown {
|
||||
overflow: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
&-dropdown {
|
||||
|
@ -211,6 +240,7 @@ watch(
|
|||
&-button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
font-size: 1rem;
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GameObject } from '@freeblox/engine';
|
||||
import { AssetInfo, GameObject } from '@freeblox/engine';
|
||||
import type { Component } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import Field from '../form/Field.vue';
|
||||
|
@ -22,6 +22,7 @@ import { Color, Euler, Vector3 } from 'three';
|
|||
import Vector3Field from '../form/Vector3Field.vue';
|
||||
import Checkbox from '../form/Checkbox.vue';
|
||||
import ColorPicker from '../form/ColorPicker.vue';
|
||||
import AssetPicker from '../form/AssetPicker.vue';
|
||||
|
||||
interface FormItem {
|
||||
name: string;
|
||||
|
@ -114,6 +115,17 @@ const formFields = computed(() => {
|
|||
component: Checkbox,
|
||||
});
|
||||
}
|
||||
|
||||
if (property.definition.type === AssetInfo) {
|
||||
fields.push({
|
||||
name: property.definition.name!,
|
||||
label: toCapitalizedWords(property.definition.name!),
|
||||
value: (object as unknown as Record<string, unknown>)[
|
||||
property.definition.name!
|
||||
],
|
||||
component: AssetPicker,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return fields;
|
||||
|
|
|
@ -21,7 +21,7 @@ class CameraControls extends EventDispatcher {
|
|||
public pointerSpeed = 1.5;
|
||||
public panSpeed = 1;
|
||||
public movementSpeed = 0.025;
|
||||
public shiftMultiplier = 1.2;
|
||||
public shiftMultiplier = 2;
|
||||
public zoomScale = 100;
|
||||
public screenSpacePanning = true;
|
||||
|
||||
|
@ -42,6 +42,7 @@ class CameraControls extends EventDispatcher {
|
|||
right: 0,
|
||||
up: 0,
|
||||
down: 0,
|
||||
multiplier: 1,
|
||||
};
|
||||
|
||||
constructor(private camera: Object3D, private domElement: HTMLElement) {
|
||||
|
@ -76,27 +77,27 @@ class CameraControls extends EventDispatcher {
|
|||
|
||||
update(dt: number) {
|
||||
if (this.movement.forward !== 0) {
|
||||
this.moveForward(this.movement.forward * dt);
|
||||
this.moveForward(this.movement.forward * dt * this.movement.multiplier);
|
||||
}
|
||||
|
||||
if (this.movement.backward !== 0) {
|
||||
this.moveForward(-this.movement.backward * dt);
|
||||
this.moveForward(-this.movement.backward * dt * this.movement.multiplier);
|
||||
}
|
||||
|
||||
if (this.movement.right !== 0) {
|
||||
this.moveRight(this.movement.right * dt);
|
||||
this.moveRight(this.movement.right * dt * this.movement.multiplier);
|
||||
}
|
||||
|
||||
if (this.movement.left !== 0) {
|
||||
this.moveRight(-this.movement.left * dt);
|
||||
this.moveRight(-this.movement.left * dt * this.movement.multiplier);
|
||||
}
|
||||
|
||||
if (this.movement.up !== 0) {
|
||||
this.moveUp(this.movement.up * dt);
|
||||
this.moveUp(this.movement.up * dt * this.movement.multiplier);
|
||||
}
|
||||
|
||||
if (this.movement.down !== 0) {
|
||||
this.moveUp(-this.movement.down * dt);
|
||||
this.moveUp(-this.movement.down * dt * this.movement.multiplier);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -212,6 +213,9 @@ class CameraControls extends EventDispatcher {
|
|||
if (event.altKey || event.ctrlKey) return;
|
||||
|
||||
switch (event.code) {
|
||||
case 'ShiftLeft':
|
||||
this.movement.multiplier = this.shiftMultiplier;
|
||||
break;
|
||||
case 'KeyE':
|
||||
this.movement.up = this.movementSpeed;
|
||||
break;
|
||||
|
@ -240,6 +244,9 @@ class CameraControls extends EventDispatcher {
|
|||
if (event.altKey || event.ctrlKey) return;
|
||||
|
||||
switch (event.code) {
|
||||
case 'ShiftLeft':
|
||||
this.movement.multiplier = 1;
|
||||
break;
|
||||
case 'KeyE':
|
||||
this.movement.up = 0;
|
||||
break;
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
|
||||
<path
|
||||
d="M452-202h60v-201l82 82 42-42-156-152-154 154 42 42 84-84v201ZM220-80q-24 0-42-18t-18-42v-680q0-24 18-42t42-18h361l219 219v521q0 24-18 42t-42 18H220Zm331-554v-186H220v680h520v-494H551ZM220-820v186-186 680-680Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
|
@ -1,4 +1,5 @@
|
|||
export interface MenuItem {
|
||||
[x: string]: unknown;
|
||||
id: string;
|
||||
label?: string;
|
||||
shortcut?: string;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { h } from 'vue';
|
||||
|
||||
export const readString = (file: File, dataUrl = false) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
@ -16,7 +14,7 @@ export const readString = (file: File, dataUrl = false) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const readFileToString = (allowedTypes?: string) => {
|
||||
export const readFileToString = (allowedTypes?: string, dataUrl = false) => {
|
||||
let cleanUpTimer: ReturnType<typeof setTimeout>;
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
|
@ -31,7 +29,7 @@ export const readFileToString = (allowedTypes?: string) => {
|
|||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
input.addEventListener('change', () => {
|
||||
input.files && readString(input.files![0]).then(resolve, reject);
|
||||
input.files && readString(input.files![0], dataUrl).then(resolve, reject);
|
||||
cleanUp();
|
||||
});
|
||||
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import { CubeTexture, CubeTextureLoader, Texture, TextureLoader } from 'three';
|
||||
import { Asset } from '../types/asset';
|
||||
import { Asset, AssetsEvents } from '../types/asset';
|
||||
import { EventEmitter } from '../utils/events';
|
||||
|
||||
export class AssetManagerFactory {
|
||||
export class AssetManagerFactory extends EventEmitter<AssetsEvents> {
|
||||
public assets: Asset[] = [];
|
||||
public texureLoader = new TextureLoader();
|
||||
public cubeTextureLoader = new CubeTextureLoader();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.on('load', (input) =>
|
||||
Array.isArray(input) ? this.loadAll(input) : this.load(input)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an asset by path, this is an unique identifier
|
||||
* @param path Asset path
|
||||
|
@ -44,6 +52,7 @@ export class AssetManagerFactory {
|
|||
* @param asset Remote or local asset data
|
||||
*/
|
||||
async load(asset: Asset) {
|
||||
this.emit('loadStart', asset);
|
||||
return (
|
||||
asset.type === 'Texture'
|
||||
? this.loadTextureData(
|
||||
|
@ -54,10 +63,16 @@ export class AssetManagerFactory {
|
|||
asset.remote ? asset.path : asset.data,
|
||||
asset.name
|
||||
)
|
||||
).then((texture) => {
|
||||
asset.texture = texture;
|
||||
return asset;
|
||||
});
|
||||
)
|
||||
.then((texture) => {
|
||||
asset.texture = texture;
|
||||
this.emit('loadComplete', asset);
|
||||
return asset;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.emit('loadError', error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -103,6 +103,7 @@ export class LevelComponent extends EngineComponent {
|
|||
|
||||
// Load world
|
||||
this.deserializeObject(save.world);
|
||||
this.events.emit('loadComplete');
|
||||
}
|
||||
|
||||
private recursiveCreate(entry: SerializedObject, setParent?: Object3D) {
|
||||
|
|
|
@ -43,6 +43,7 @@ export class Brick extends GameObject3D {
|
|||
if (!path) {
|
||||
this.material.map = null;
|
||||
this.texturePath = undefined;
|
||||
this.material.needsUpdate = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -54,6 +55,7 @@ export class Brick extends GameObject3D {
|
|||
|
||||
this.texturePath = path;
|
||||
this.material.map = asset.texture;
|
||||
this.material.needsUpdate = true;
|
||||
}
|
||||
|
||||
@EditorProperty({ type: Boolean })
|
||||
|
|
|
@ -12,3 +12,10 @@ export interface Asset {
|
|||
export class AssetInfo {
|
||||
constructor(public path: string, public name: string) {}
|
||||
}
|
||||
|
||||
export type AssetsEvents = {
|
||||
load(asset: Asset | Asset[]): void;
|
||||
loadStart(asset: Asset): void;
|
||||
loadComplete(asset: Asset): void;
|
||||
loadError(asset: Asset, error?: Error): void;
|
||||
};
|
||||
|
|
|
@ -79,6 +79,7 @@ export type EngineEvents = {
|
|||
instance: (event: InstanceEvent) => void;
|
||||
sceneJoin: (event: Object3D) => void;
|
||||
sceneLeave: (event: Object3D) => void;
|
||||
loadComplete: () => void;
|
||||
initialized: () => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ export class GameObject extends Object3D {
|
|||
|
||||
@EditorProperty({ type: String })
|
||||
public override name: string = '';
|
||||
|
||||
@EditorProperty({ type: Boolean })
|
||||
public override visible: boolean = true;
|
||||
|
||||
|
@ -21,7 +22,9 @@ export class GameObject extends Object3D {
|
|||
return readMetadataOf<string>(this, 'excludedProperties');
|
||||
}
|
||||
|
||||
/** The exposed properties for this game object, used for the editor */
|
||||
/**
|
||||
* The exposed properties for this game object, used for the editor
|
||||
*/
|
||||
get properties() {
|
||||
const exclude = this.excludedProperties;
|
||||
const properties = readMetadataOf<Property>(this, 'properties');
|
||||
|
@ -85,9 +88,9 @@ export class GameObject extends Object3D {
|
|||
const indexable = this as any;
|
||||
if (indexable[key]?.fromArray && Array.isArray(input[key])) {
|
||||
indexable[key].fromArray(input[key]);
|
||||
} else if (indexable[key].isColor) {
|
||||
} else if (indexable[key]?.isColor) {
|
||||
indexable[key] = new Color(input[key] as string);
|
||||
} else if (indexable[key].copy) {
|
||||
} else if (indexable[key]?.copy) {
|
||||
indexable[key].copy(input[key]);
|
||||
} else {
|
||||
indexable[key] = input[key];
|
||||
|
|
Loading…
Reference in New Issue