Initial code
This commit is contained in:
commit
4fde9cf8f0
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
build
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "@freeblox/monorepo",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"workspaces": [
|
||||||
|
"src/apps/**",
|
||||||
|
"src/packages/**"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">16.0.0",
|
||||||
|
"pnpm": ">=6"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||||
|
|
||||||
|
## Type Support For `.vue` Imports in TS
|
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||||
|
|
||||||
|
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||||
|
|
||||||
|
1. Disable the built-in TypeScript Extension
|
||||||
|
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||||
|
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||||
|
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Freeblox Client Dev</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "@freeblox/client",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"main": "./dist/client.umd.cjs",
|
||||||
|
"module": "./dist/client.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/client.js",
|
||||||
|
"require": "./dist/client.umd.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@freeblox/engine": "workspace:^",
|
||||||
|
"three": "^0.153.0",
|
||||||
|
"vite-plugin-dts": "^2.3.0",
|
||||||
|
"vue": "^3.2.47"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/three": "^0.152.1",
|
||||||
|
"@vitejs/plugin-vue": "^4.1.0",
|
||||||
|
"tslib": "^2.5.3",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.3.9",
|
||||||
|
"vue-tsc": "^1.4.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<h1>test</h1>
|
||||||
|
</template>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<div class="game-wrapper"></div>
|
||||||
|
</template>
|
|
@ -0,0 +1 @@
|
||||||
|
export * as GameWrapper from './components/GameWrapper.vue';
|
|
@ -0,0 +1,8 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import './main.css';
|
||||||
|
|
||||||
|
createApp(App).mount('#app');
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2020",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import dts from 'vite-plugin-dts'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue(), dts()],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: 'src/index.ts',
|
||||||
|
name: 'Client',
|
||||||
|
fileName: 'client',
|
||||||
|
formats: ['es', 'cjs', 'umd']
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['vue'],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
vue: 'Vue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||||
|
|
||||||
|
## Type Support For `.vue` Imports in TS
|
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||||
|
|
||||||
|
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||||
|
|
||||||
|
1. Disable the built-in TypeScript Extension
|
||||||
|
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||||
|
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||||
|
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Freeblox Editor Dev</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "@freeblox/editor",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"main": "./dist/editor.umd.cjs",
|
||||||
|
"module": "./dist/editor.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/editor.js",
|
||||||
|
"require": "./dist/editor.umd.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@freeblox/engine": "workspace:^",
|
||||||
|
"three": "^0.153.0",
|
||||||
|
"vite-plugin-dts": "^2.3.0",
|
||||||
|
"vue": "^3.2.47"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/three": "^0.152.1",
|
||||||
|
"@vitejs/plugin-vue": "^4.1.0",
|
||||||
|
"tslib": "^2.5.3",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.3.9",
|
||||||
|
"vue-tsc": "^1.4.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,6 @@
|
||||||
|
<template>
|
||||||
|
<EditorWrapper></EditorWrapper>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import EditorWrapper from './components/EditorWrapper.vue';
|
||||||
|
</script>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div class="editor-wrapper" ref="wrapperRef"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, shallowRef } from 'vue';
|
||||||
|
import { Editor } from '../editor';
|
||||||
|
|
||||||
|
const wrapperRef = ref();
|
||||||
|
const editorRef = shallowRef<Editor>(new Editor());
|
||||||
|
|
||||||
|
const resize = () => editorRef.value.viewport.setSizeFromWindow();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
editorRef.value.mount(wrapperRef.value);
|
||||||
|
window.addEventListener('resize', resize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', resize);
|
||||||
|
editorRef.value?.stop();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { EventEmitter, GameRunner, Renderer } from '@freeblox/engine';
|
||||||
|
import { EditorViewport } from './viewport';
|
||||||
|
import { EditorEvents } from '../types/events';
|
||||||
|
import { EditorWorkspace } from './workspace';
|
||||||
|
import { EditorMouse } from './mouse';
|
||||||
|
import { EditorEnvironment } from './environment';
|
||||||
|
|
||||||
|
export class Editor extends GameRunner {
|
||||||
|
public lastTick = performance.now();
|
||||||
|
public events = new EventEmitter<EditorEvents>();
|
||||||
|
public render!: Renderer;
|
||||||
|
public element!: HTMLElement;
|
||||||
|
public viewport!: EditorViewport;
|
||||||
|
public workspace!: EditorWorkspace;
|
||||||
|
public mouse!: EditorMouse;
|
||||||
|
public environment!: EditorEnvironment;
|
||||||
|
public running = false;
|
||||||
|
|
||||||
|
override mount(element: HTMLElement) {
|
||||||
|
this.element = element;
|
||||||
|
this.render = new Renderer(element);
|
||||||
|
|
||||||
|
this.viewport = new EditorViewport(this.render, this.events);
|
||||||
|
this.viewport.initialize();
|
||||||
|
|
||||||
|
this.workspace = new EditorWorkspace(this.render, this.events);
|
||||||
|
this.workspace.initialize();
|
||||||
|
|
||||||
|
this.mouse = new EditorMouse(this.render, this.events);
|
||||||
|
this.mouse.initialize();
|
||||||
|
|
||||||
|
this.environment = new EditorEnvironment(this.render, this.events);
|
||||||
|
this.environment.initialize();
|
||||||
|
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
override loop(now: DOMHighResTimeStamp) {
|
||||||
|
const delta = now - this.lastTick;
|
||||||
|
this.lastTick = now;
|
||||||
|
|
||||||
|
this.running && requestAnimationFrame((ts) => this.loop(ts));
|
||||||
|
|
||||||
|
this.viewport.update(delta);
|
||||||
|
this.workspace.update(delta);
|
||||||
|
this.mouse.update(delta);
|
||||||
|
|
||||||
|
this.render.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
override start() {
|
||||||
|
this.running = true;
|
||||||
|
this.loop(this.lastTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
override stop() {
|
||||||
|
this.running = false;
|
||||||
|
this.viewport.cleanUp();
|
||||||
|
this.workspace.cleanUp();
|
||||||
|
this.mouse.cleanUp();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { EngineComponent, EventEmitter, Renderer } from '@freeblox/engine';
|
||||||
|
import { EditorEvents } from '../types/events';
|
||||||
|
import { AmbientLight, DirectionalLight } from 'three';
|
||||||
|
|
||||||
|
export class EditorEnvironment extends EngineComponent {
|
||||||
|
public ambient!: AmbientLight;
|
||||||
|
public directional!: DirectionalLight;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected renderer: Renderer,
|
||||||
|
protected events: EventEmitter<EditorEvents>
|
||||||
|
) {
|
||||||
|
super(renderer, events);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(): void {
|
||||||
|
this.ambient = new AmbientLight(0x8a8a8a, 1.0);
|
||||||
|
this.directional = new DirectionalLight(0xffffff, 1);
|
||||||
|
this.directional.position.set(1, 1, 1);
|
||||||
|
|
||||||
|
this.renderer.scene.add(this.ambient);
|
||||||
|
this.renderer.scene.add(this.directional);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta: number): void {}
|
||||||
|
|
||||||
|
cleanUp(): void {}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { EngineComponent, EventEmitter, Renderer } from '@freeblox/engine';
|
||||||
|
import { EditorEvents, SelectEvent } from '../types/events';
|
||||||
|
import { Object3D, Raycaster, Vector2 } from 'three';
|
||||||
|
|
||||||
|
type MouseMap = [boolean, boolean, boolean];
|
||||||
|
type EventMap = [string, Function];
|
||||||
|
|
||||||
|
export class EditorMouse extends EngineComponent {
|
||||||
|
private helpers!: Object3D;
|
||||||
|
private world!: Object3D;
|
||||||
|
|
||||||
|
private mouseButtons: MouseMap = [false, false, false];
|
||||||
|
private mouseButtonsLast: MouseMap = [false, false, false];
|
||||||
|
|
||||||
|
private mousePosition = new Vector2(0, 0);
|
||||||
|
private mousePositionGL = new Vector2(0, 0);
|
||||||
|
private mousePositionLast = new Vector2(0, 0);
|
||||||
|
private mousePositionGLLast = new Vector2(0, 0);
|
||||||
|
|
||||||
|
private boundEvents: EventMap[] = [];
|
||||||
|
private ray = new Raycaster();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected renderer: Renderer,
|
||||||
|
protected events: EventEmitter<EditorEvents>
|
||||||
|
) {
|
||||||
|
super(renderer, events);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canvas() {
|
||||||
|
return this.renderer.renderer.domElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(): void {
|
||||||
|
this.helpers = this.renderer.scene.getObjectByName('_helpers')!;
|
||||||
|
this.world = this.renderer.scene.getObjectByName('_world')!;
|
||||||
|
|
||||||
|
const mouseDown = (ev: MouseEvent) => {
|
||||||
|
const [object] = this.ray.intersectObjects(this.world.children, true);
|
||||||
|
this.mouseButtons[ev.button] = true;
|
||||||
|
this.events.emit('mouseDown', {
|
||||||
|
position: this.mousePosition,
|
||||||
|
positionGL: this.mousePositionGL,
|
||||||
|
button: ev.button,
|
||||||
|
target: object,
|
||||||
|
shift: ev.shiftKey,
|
||||||
|
control: ev.ctrlKey,
|
||||||
|
alt: ev.altKey,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mouseUp = (ev: MouseEvent) => {
|
||||||
|
const [object] = this.ray.intersectObjects(this.world.children, true);
|
||||||
|
this.mouseButtons[ev.button] = false;
|
||||||
|
this.events.emit('mouseUp', {
|
||||||
|
position: this.mousePosition,
|
||||||
|
positionGL: this.mousePositionGL,
|
||||||
|
button: ev.button,
|
||||||
|
target: object,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mouseMove = (ev: MouseEvent) => {
|
||||||
|
this.mousePositionLast = this.mousePosition.clone();
|
||||||
|
this.mousePositionGLLast = this.mousePositionGL.clone();
|
||||||
|
|
||||||
|
this.mousePosition.set(ev.clientX, ev.clientY);
|
||||||
|
this.mousePositionGL.set(
|
||||||
|
(this.mousePosition.x / this.renderer.resolution.x) * 2 - 1,
|
||||||
|
-(this.mousePosition.y / this.renderer.resolution.y) * 2 + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
this.events.emit('mouseMove', {
|
||||||
|
position: this.mousePosition,
|
||||||
|
positionGL: this.mousePositionGL,
|
||||||
|
offset: this.mousePositionLast.clone().sub(this.mousePosition),
|
||||||
|
offsetGL: this.mousePositionGLLast.clone().sub(this.mousePositionGL),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextMenu = (ev: MouseEvent) => ev.preventDefault();
|
||||||
|
|
||||||
|
this.canvas.addEventListener('mousedown', mouseDown);
|
||||||
|
this.canvas.addEventListener('mousemove', mouseMove);
|
||||||
|
this.canvas.addEventListener('mouseup', mouseUp);
|
||||||
|
this.canvas.addEventListener('contextmenu', contextMenu);
|
||||||
|
|
||||||
|
this.boundEvents.push(
|
||||||
|
['mousedown', mouseDown],
|
||||||
|
['mousemove', mouseMove],
|
||||||
|
['mouseup', mouseUp],
|
||||||
|
['contextmenu', contextMenu]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta: number): void {
|
||||||
|
this.ray.setFromCamera(this.mousePositionGL, this.renderer.camera);
|
||||||
|
this.mouseButtonsLast = [...this.mouseButtons];
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanUp(): void {
|
||||||
|
for (const [event, handler] of this.boundEvents) {
|
||||||
|
this.canvas.removeEventListener(event, handler as EventListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// private realMousePos(vec: Vector2, x: number, y: number) {
|
||||||
|
// const rect = this.renderer.renderer.domElement.getBoundingClientRect();
|
||||||
|
// const scaleX = this.renderer.renderer.domElement.width / rect.width;
|
||||||
|
// const scaleY = this.renderer.renderer.domElement.height / rect.height;
|
||||||
|
// vec.set((x - rect.left) * scaleX, (y - rect.top) * scaleY);
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { EngineComponent, EventEmitter, Renderer } from '@freeblox/engine';
|
||||||
|
import { Object3D } from 'three';
|
||||||
|
import { EditorEvents } from '../types/events';
|
||||||
|
|
||||||
|
export class EditorViewport extends EngineComponent {
|
||||||
|
constructor(
|
||||||
|
protected render: Renderer,
|
||||||
|
protected events: EventEmitter<EditorEvents>
|
||||||
|
) {
|
||||||
|
super(render, events);
|
||||||
|
}
|
||||||
|
|
||||||
|
get scene() {
|
||||||
|
return this.render.scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
get camera() {
|
||||||
|
return this.render.camera;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.setSizeFromWindow();
|
||||||
|
this.camera.position.set(16, 8, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(dt: number) {}
|
||||||
|
|
||||||
|
cleanUp(): void {}
|
||||||
|
|
||||||
|
setSize(width: number, height: number) {
|
||||||
|
this.render.viewport.style.width = `${width}px`;
|
||||||
|
this.render.viewport.style.height = `${height}px`;
|
||||||
|
this.render.setSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSizeFromWindow() {
|
||||||
|
this.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,353 @@
|
||||||
|
import { EngineComponent, EventEmitter, Renderer } from '@freeblox/engine';
|
||||||
|
import {
|
||||||
|
AxesHelper,
|
||||||
|
BoxGeometry,
|
||||||
|
BoxHelper,
|
||||||
|
Color,
|
||||||
|
Euler,
|
||||||
|
GridHelper,
|
||||||
|
Material,
|
||||||
|
Mesh,
|
||||||
|
MeshPhongMaterial,
|
||||||
|
Object3D,
|
||||||
|
Vector3,
|
||||||
|
} from 'three';
|
||||||
|
import {
|
||||||
|
EditorEvents,
|
||||||
|
MouseButtonEvent,
|
||||||
|
MouseMoveEvent,
|
||||||
|
SelectEvent,
|
||||||
|
TransformModeEvent,
|
||||||
|
} from '../types/events';
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
|
||||||
|
|
||||||
|
export class EditorWorkspace extends EngineComponent {
|
||||||
|
public background = new Object3D();
|
||||||
|
public world = new Object3D();
|
||||||
|
public helpers = new Object3D();
|
||||||
|
|
||||||
|
public orbitControls!: OrbitControls;
|
||||||
|
public transformControls!: TransformControls;
|
||||||
|
private grid!: GridHelper;
|
||||||
|
private box!: BoxHelper;
|
||||||
|
private axes!: AxesHelper;
|
||||||
|
|
||||||
|
public transformPosition?: Vector3;
|
||||||
|
public transformRotation?: Euler;
|
||||||
|
public transformScale?: Vector3;
|
||||||
|
|
||||||
|
public selection: Object3D[] = [];
|
||||||
|
public eventCleanUp?: Function;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected renderer: Renderer,
|
||||||
|
protected events: EventEmitter<EditorEvents>
|
||||||
|
) {
|
||||||
|
super(renderer, events);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.addToScene(this.renderer.scene);
|
||||||
|
this.initializeHelpers();
|
||||||
|
this.initializeControls();
|
||||||
|
this.eventCleanUp = this.initializeSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(dt: number) {
|
||||||
|
this.orbitControls?.update();
|
||||||
|
this.box?.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanUp(): void {
|
||||||
|
this.removeFromScene(this.renderer.scene);
|
||||||
|
this.eventCleanUp?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addToScene(scene: Object3D) {
|
||||||
|
this.background.name = '_background';
|
||||||
|
this.world.name = '_world';
|
||||||
|
this.helpers.name = '_helper';
|
||||||
|
scene.add(this.background, this.world, this.helpers);
|
||||||
|
|
||||||
|
const test = new Mesh(new BoxGeometry(), new MeshPhongMaterial());
|
||||||
|
test.position.set(2, 2, 2);
|
||||||
|
this.world.add(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeFromScene(scene: Object3D) {
|
||||||
|
scene.remove(this.background, this.world, this.helpers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeSelector() {
|
||||||
|
let moved = false;
|
||||||
|
|
||||||
|
const mouseDownEventHandler = () => {
|
||||||
|
moved = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mouseMoveEventHandler = () => {
|
||||||
|
moved = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mouseUpEventHandler = (event: MouseButtonEvent) => {
|
||||||
|
if (moved) return;
|
||||||
|
if (!event.target?.object) {
|
||||||
|
if (!this.selection.length) return;
|
||||||
|
const oldSelection = this.selection;
|
||||||
|
this.selection = [];
|
||||||
|
oldSelection.forEach((selection) =>
|
||||||
|
this.events.emit('deselect', {
|
||||||
|
object: selection,
|
||||||
|
selection: [],
|
||||||
|
picker: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const object = event.target!.object;
|
||||||
|
if (this.selection.includes(object)) {
|
||||||
|
if (event.control) {
|
||||||
|
const index = this.selection.indexOf(object);
|
||||||
|
this.selection.splice(index, 1);
|
||||||
|
this.events.emit('deselect', {
|
||||||
|
object,
|
||||||
|
selection: this.selection,
|
||||||
|
picker: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.control) {
|
||||||
|
this.selection.push(object);
|
||||||
|
this.events.emit('select', {
|
||||||
|
object,
|
||||||
|
selection: this.selection,
|
||||||
|
multi: true,
|
||||||
|
picker: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notObject = this.selection.filter(
|
||||||
|
(entry) => entry.id !== object.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const wasEmpty = !this.selection.length;
|
||||||
|
this.selection = [object];
|
||||||
|
if (wasEmpty) {
|
||||||
|
this.events.emit('select', {
|
||||||
|
object: object,
|
||||||
|
selection: [object],
|
||||||
|
picker: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
notObject.forEach((entry) =>
|
||||||
|
this.events.emit('deselect', {
|
||||||
|
object: entry,
|
||||||
|
selection: this.selection,
|
||||||
|
picker: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectHandler = (select: SelectEvent) => {
|
||||||
|
if (!select.picker) {
|
||||||
|
if (select.multi) {
|
||||||
|
this.selection.push(select.object);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selection = [select.object];
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachTo = this.selection[this.selection.length - 1];
|
||||||
|
this.transformControls?.attach(attachTo);
|
||||||
|
this.box.setFromObject(attachTo);
|
||||||
|
this.box.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deselectHandler = (select: SelectEvent) => {
|
||||||
|
if (!select.picker) {
|
||||||
|
const index = this.selection.indexOf(select.object);
|
||||||
|
this.selection.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selection.length) {
|
||||||
|
const attachTo = this.selection[this.selection.length - 1];
|
||||||
|
this.transformControls?.attach(attachTo);
|
||||||
|
this.box.setFromObject(attachTo);
|
||||||
|
this.box.visible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.transformControls?.detach();
|
||||||
|
this.box.visible = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformMode = (mode: TransformModeEvent) => {
|
||||||
|
if (!this.transformControls) return;
|
||||||
|
if (!mode) {
|
||||||
|
this.transformControls.enabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.transformControls.setMode(mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformSnap = (value: number) => {
|
||||||
|
if (!this.transformControls) return;
|
||||||
|
this.transformControls.setTranslationSnap(value);
|
||||||
|
this.transformControls.setScaleSnap(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformRotationSnap = (value: number) => {
|
||||||
|
if (!this.transformControls) return;
|
||||||
|
this.transformControls.setRotationSnap(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.events.addListener('mouseDown', mouseDownEventHandler);
|
||||||
|
this.events.addListener('mouseMove', mouseMoveEventHandler);
|
||||||
|
this.events.addListener('mouseUp', mouseUpEventHandler);
|
||||||
|
this.events.addListener('select', selectHandler);
|
||||||
|
this.events.addListener('deselect', deselectHandler);
|
||||||
|
this.events.addListener('transformMode', transformMode);
|
||||||
|
this.events.addListener('transformSnap', transformSnap);
|
||||||
|
this.events.addListener('transformRotationSnap', transformRotationSnap);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.events.removeEventListener('mouseDown', mouseDownEventHandler);
|
||||||
|
this.events.removeEventListener('mouseMove', mouseMoveEventHandler);
|
||||||
|
this.events.removeEventListener('mouseUp', mouseUpEventHandler);
|
||||||
|
this.events.removeEventListener('select', selectHandler);
|
||||||
|
this.events.removeEventListener('deselect', deselectHandler);
|
||||||
|
this.events.removeEventListener('transformMode', transformMode);
|
||||||
|
this.events.removeEventListener('transformSnap', transformSnap);
|
||||||
|
this.events.removeEventListener(
|
||||||
|
'transformRotationSnap',
|
||||||
|
transformRotationSnap
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeHelpers() {
|
||||||
|
this.grid = new GridHelper(100, 100);
|
||||||
|
this.background.add(this.grid);
|
||||||
|
|
||||||
|
this.box = new BoxHelper(this.world, 0x00a2ff);
|
||||||
|
(this.box.material as Material).depthTest = false;
|
||||||
|
(this.box.material as Material).transparent = true;
|
||||||
|
this.box.visible = false;
|
||||||
|
this.helpers.add(this.box);
|
||||||
|
|
||||||
|
this.axes = new AxesHelper(50);
|
||||||
|
this.helpers.add(this.axes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeControls() {
|
||||||
|
let translationSnap: number | null = 0.5;
|
||||||
|
let scaleSnap: number | null = 0.5;
|
||||||
|
let rotationSnap: number | null = null;
|
||||||
|
let mode: 'translate' | 'rotate' | 'scale' = 'translate';
|
||||||
|
|
||||||
|
if (this.orbitControls) this.orbitControls.dispose();
|
||||||
|
if (this.transformControls) {
|
||||||
|
translationSnap = this.transformControls.translationSnap;
|
||||||
|
scaleSnap = (this.transformControls as any).scaleSnap; // FIXME: typedef bug
|
||||||
|
rotationSnap = this.transformControls.rotationSnap;
|
||||||
|
mode = this.transformControls.getMode();
|
||||||
|
this.helpers.remove(this.transformControls);
|
||||||
|
this.transformControls.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.orbitControls = new OrbitControls(
|
||||||
|
this.renderer.camera,
|
||||||
|
this.renderer.renderer.domElement
|
||||||
|
);
|
||||||
|
this.transformControls = new TransformControls(
|
||||||
|
this.renderer.camera,
|
||||||
|
this.renderer.renderer.domElement
|
||||||
|
);
|
||||||
|
this.transformControls.translationSnap = translationSnap;
|
||||||
|
(this.transformControls as any).scaleSnap = scaleSnap;
|
||||||
|
this.transformControls.rotationSnap = rotationSnap;
|
||||||
|
this.transformControls.setMode(mode);
|
||||||
|
this.helpers.add(this.transformControls);
|
||||||
|
|
||||||
|
this.transformControls.addEventListener('mouseDown', () => {
|
||||||
|
this.orbitControls.enabled = false;
|
||||||
|
const target = this.transformControls.object;
|
||||||
|
if (!target) return;
|
||||||
|
this.transformPosition = target.position.clone();
|
||||||
|
this.transformRotation = target.rotation.clone();
|
||||||
|
this.transformScale = target.scale.clone();
|
||||||
|
this.events.emit('transformStart', {
|
||||||
|
position: this.transformPosition!,
|
||||||
|
rotation: this.transformRotation!,
|
||||||
|
scale: this.transformScale!,
|
||||||
|
object: target,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.transformControls.addEventListener('mouseUp', () => {
|
||||||
|
this.orbitControls.enabled = true;
|
||||||
|
const target = this.transformControls.object;
|
||||||
|
if (!target) return;
|
||||||
|
this.events.emit('transformEnd', {
|
||||||
|
position: target.position.clone(),
|
||||||
|
rotation: target.rotation.clone(),
|
||||||
|
scale: target.scale.clone(),
|
||||||
|
object: target,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.transformControls.addEventListener('change', () => {
|
||||||
|
const target = this.transformControls.object;
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
this.events.emit('transformChange', {
|
||||||
|
lastPosition: this.transformPosition!,
|
||||||
|
lastRotation: this.transformRotation!,
|
||||||
|
lastScale: this.transformScale!,
|
||||||
|
position: target.position.clone(),
|
||||||
|
rotation: target.rotation.clone(),
|
||||||
|
scale: target.scale.clone(),
|
||||||
|
object: target,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.transformPosition &&
|
||||||
|
!this.transformPosition.equals(target.position)
|
||||||
|
) {
|
||||||
|
this.events.emit('change', {
|
||||||
|
object: target,
|
||||||
|
property: 'position',
|
||||||
|
value: target.position.clone(),
|
||||||
|
transformed: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.transformRotation &&
|
||||||
|
!this.transformRotation.equals(target.rotation)
|
||||||
|
) {
|
||||||
|
this.events.emit('change', {
|
||||||
|
object: target,
|
||||||
|
property: 'rotation',
|
||||||
|
value: target.rotation.clone(),
|
||||||
|
transformed: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.transformScale && !this.transformScale.equals(target.scale)) {
|
||||||
|
this.events.emit('change', {
|
||||||
|
object: target,
|
||||||
|
property: 'scale',
|
||||||
|
value: target.scale.clone(),
|
||||||
|
transformed: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './core/editor';
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { EngineEvents } from '@freeblox/engine';
|
||||||
|
import { Euler, Intersection, Object3D, Vector2, Vector3 } from 'three';
|
||||||
|
|
||||||
|
export interface MousePositionEvent {
|
||||||
|
position: Vector2;
|
||||||
|
positionGL: Vector2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MouseMoveEvent extends MousePositionEvent {
|
||||||
|
offset: Vector2;
|
||||||
|
offsetGL: Vector2;
|
||||||
|
helper?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MouseButtonEvent extends MousePositionEvent {
|
||||||
|
button: number;
|
||||||
|
helper?: boolean;
|
||||||
|
target?: Intersection<Object3D>;
|
||||||
|
shift?: boolean;
|
||||||
|
control?: boolean;
|
||||||
|
alt?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransformEvent {
|
||||||
|
object: Object3D;
|
||||||
|
position: Vector3;
|
||||||
|
rotation: Euler;
|
||||||
|
scale: Vector3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TransformModeEvent = 'translate' | 'rotate' | 'scale' | null;
|
||||||
|
|
||||||
|
export interface TransformCompleteEvent extends TransformEvent {
|
||||||
|
lastPosition: Vector3;
|
||||||
|
lastRotation: Euler;
|
||||||
|
lastScale: Vector3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeEvent {
|
||||||
|
object: Object3D;
|
||||||
|
property: string;
|
||||||
|
value: any;
|
||||||
|
edited?: boolean;
|
||||||
|
transformed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectEvent {
|
||||||
|
object: Object3D;
|
||||||
|
selection: Object3D[];
|
||||||
|
multi?: boolean;
|
||||||
|
picker?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Events = {
|
||||||
|
error: (error: Error) => void;
|
||||||
|
mouseDown: (event: MouseButtonEvent) => void;
|
||||||
|
mouseUp: (event: MouseButtonEvent) => void;
|
||||||
|
mouseMove: (event: MouseMoveEvent) => void;
|
||||||
|
transformStart: (event: TransformEvent) => void;
|
||||||
|
transformChange: (event: TransformCompleteEvent) => void;
|
||||||
|
transformEnd: (event: TransformEvent) => void;
|
||||||
|
transformMode: (event: TransformModeEvent) => void;
|
||||||
|
transformSnap: (event: number) => void;
|
||||||
|
transformRotationSnap: (event: number) => void;
|
||||||
|
change: (event: ChangeEvent) => void;
|
||||||
|
select: (event: SelectEvent) => void;
|
||||||
|
deselect: (event: SelectEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EditorEvents = Events & EngineEvents;
|
|
@ -0,0 +1 @@
|
||||||
|
export * as EditorWrapper from './components/EditorWrapper.vue';
|
|
@ -0,0 +1,8 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import './main.css';
|
||||||
|
|
||||||
|
createApp(App).mount('#app');
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2020",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import dts from 'vite-plugin-dts'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue(), dts()],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: 'src/index.ts',
|
||||||
|
name: 'Editor',
|
||||||
|
fileName: 'editor',
|
||||||
|
formats: ['es', 'cjs', 'umd']
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['vue'],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
vue: 'Vue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,2 @@
|
||||||
|
dist/
|
||||||
|
node_modules/
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "@freeblox/engine",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Freeblox Engine",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"prepare": "npm run build"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"game",
|
||||||
|
"engine",
|
||||||
|
"three"
|
||||||
|
],
|
||||||
|
"author": "Evert",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/three": "^0.152.1",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"three": "^0.153.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { EngineComponent } from '..';
|
||||||
|
import { EngineEvents } from '../types/events';
|
||||||
|
import { GameRunner } from '../types/game-runner';
|
||||||
|
import { EventEmitter, EventMap } from '../utils/events';
|
||||||
|
import { Renderer } from './renderer';
|
||||||
|
|
||||||
|
export class Engine<
|
||||||
|
TEvents extends EventMap = EngineEvents
|
||||||
|
> extends GameRunner {
|
||||||
|
public lastTick = performance.now();
|
||||||
|
public running = false;
|
||||||
|
public events = new EventEmitter<TEvents>();
|
||||||
|
public render!: Renderer;
|
||||||
|
public element!: HTMLElement;
|
||||||
|
public components: EngineComponent[] = [];
|
||||||
|
|
||||||
|
mount(element: HTMLElement): void {
|
||||||
|
this.element = element;
|
||||||
|
this.render = new Renderer(element);
|
||||||
|
|
||||||
|
for (const component of this.components) {
|
||||||
|
component.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
loop(now: number): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
start(): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './renderer';
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { PerspectiveCamera, Scene, Vector2, WebGLRenderer } from 'three';
|
||||||
|
|
||||||
|
export class Renderer {
|
||||||
|
public renderer = new WebGLRenderer();
|
||||||
|
public camera = new PerspectiveCamera(
|
||||||
|
75,
|
||||||
|
this.resolution.x / this.resolution.y,
|
||||||
|
0.1,
|
||||||
|
10000
|
||||||
|
);
|
||||||
|
public scene = new Scene();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public viewport: HTMLElement,
|
||||||
|
public resolution = new Vector2(1080, 720)
|
||||||
|
) {
|
||||||
|
this.renderer.setSize(resolution.x, resolution.y);
|
||||||
|
viewport.appendChild(this.renderer.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSize(width: number, height: number) {
|
||||||
|
this.resolution.set(width, height);
|
||||||
|
this.camera.aspect = this.resolution.x / this.resolution.y;
|
||||||
|
this.renderer.setSize(this.resolution.x, this.resolution.y);
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './core';
|
||||||
|
export * from './utils';
|
||||||
|
export * from './types';
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Renderer } from '../core/renderer';
|
||||||
|
import { EventEmitter } from '../utils/events';
|
||||||
|
import { EngineEvents } from './events';
|
||||||
|
|
||||||
|
export abstract class EngineComponent {
|
||||||
|
constructor(
|
||||||
|
protected renderer: Renderer,
|
||||||
|
protected events: EventEmitter<EngineEvents>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize this component
|
||||||
|
*/
|
||||||
|
abstract initialize(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update this component. Called on every frame.
|
||||||
|
* @param delta Delta time
|
||||||
|
*/
|
||||||
|
abstract update(delta: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up the component
|
||||||
|
*/
|
||||||
|
abstract cleanUp(): void;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export type EngineEvents = {
|
||||||
|
error: (error: Error) => void;
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
export abstract class GameRunner {
|
||||||
|
abstract mount(element: HTMLElement): void;
|
||||||
|
abstract loop(now: DOMHighResTimeStamp): void;
|
||||||
|
abstract start(): void;
|
||||||
|
abstract stop(): void;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './game-runner';
|
||||||
|
export * from './events';
|
||||||
|
export * from './engine-component';
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function clamp(x: number, min: number, max: number): number {
|
||||||
|
return Math.min(Math.max(x, min), max);
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
export function debounce(func: Function, timeout = 300) {
|
||||||
|
let timer: any;
|
||||||
|
return (...args: any[]) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
func.apply(null, args);
|
||||||
|
}, timeout);
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
export type EventMap = {
|
||||||
|
[key: string]: (...args: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class EventEmitter<Events extends EventMap> {
|
||||||
|
private _handlers: {
|
||||||
|
[x: string]: { fn: (...args: any) => void; once: boolean }[];
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
addListener<E extends keyof Events>(event: E, fn: Events[E], once = false) {
|
||||||
|
if (typeof fn !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._handlers[event as string]) {
|
||||||
|
this._handlers[event as string] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._handlers[event as string].push({ fn, once });
|
||||||
|
}
|
||||||
|
|
||||||
|
on<E extends keyof Events>(event: E, fn: Events[E]) {
|
||||||
|
this.addListener<E>(event, fn, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
once<E extends keyof Events>(event: E, fn: Events[E]) {
|
||||||
|
this.addListener<E>(event, fn, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<E extends keyof Events>(event: E, ...args: Parameters<Events[E]>): void {
|
||||||
|
if (!this._handlers[event as string]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._handlers[event as string]
|
||||||
|
.filter((handler) => handler && typeof handler.fn === 'function')
|
||||||
|
.forEach((handler) => {
|
||||||
|
handler.fn(...(args as []));
|
||||||
|
if (handler.once) {
|
||||||
|
this.removeEventListener(event, handler.fn as Events[E]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEventListener<E extends keyof Events>(event: E, fn: Events[E]): void {
|
||||||
|
if (!this._handlers[event as string] || typeof fn !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexOf = this._handlers[event as string].findIndex(
|
||||||
|
(entry) => entry.fn === fn
|
||||||
|
);
|
||||||
|
if (indexOf > -1) {
|
||||||
|
this._handlers[event as string].splice(indexOf, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllListeners<E extends keyof Events>(event: E): void {
|
||||||
|
if (!this._handlers[event as string]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this._handlers[event as string];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './clamp';
|
||||||
|
export * from './debounce';
|
||||||
|
export * from './events';
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
||||||
|
packages:
|
||||||
|
- 'apps/**'
|
||||||
|
- 'packages/**'
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"declaration": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue