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