Initial code

This commit is contained in:
Evert Prants 2023-06-03 20:49:26 +03:00
commit 4fde9cf8f0
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
58 changed files with 2538 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
build

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"semi": true,
"singleQuote": true
}

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

20
package.json Normal file
View File

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

24
packages/client/.gitignore vendored Normal file
View File

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

18
packages/client/README.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
<template>
<h1>test</h1>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div class="game-wrapper"></div>
</template>

View File

@ -0,0 +1 @@
export * as GameWrapper from './components/GameWrapper.vue';

View File

@ -0,0 +1,8 @@
* {
margin: 0;
padding: 0;
}
*, *::before, *::after {
box-sizing: border-box;
}

View File

@ -0,0 +1,5 @@
import { createApp } from 'vue';
import App from './App.vue';
import './main.css';
createApp(App).mount('#app');

1
packages/client/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -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"
}
]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

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

24
packages/editor/.gitignore vendored Normal file
View File

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

18
packages/editor/README.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
<template>
<EditorWrapper></EditorWrapper>
</template>
<script setup>
import EditorWrapper from './components/EditorWrapper.vue';
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export * from './core/editor';

View File

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

View File

@ -0,0 +1 @@
export * as EditorWrapper from './components/EditorWrapper.vue';

View File

@ -0,0 +1,8 @@
* {
margin: 0;
padding: 0;
}
*, *::before, *::after {
box-sizing: border-box;
}

View File

@ -0,0 +1,5 @@
import { createApp } from 'vue';
import App from './App.vue';
import './main.css';
createApp(App).mount('#app');

1
packages/editor/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -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"
}
]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

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

2
packages/engine/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
dist/
node_modules/

View File

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

View File

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

View File

@ -0,0 +1 @@
export * from './renderer';

View File

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

View File

@ -0,0 +1,3 @@
export * from './core';
export * from './utils';
export * from './types';

View File

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

View File

@ -0,0 +1,3 @@
export type EngineEvents = {
error: (error: Error) => void;
};

View File

@ -0,0 +1,6 @@
export abstract class GameRunner {
abstract mount(element: HTMLElement): void;
abstract loop(now: DOMHighResTimeStamp): void;
abstract start(): void;
abstract stop(): void;
}

View File

@ -0,0 +1,3 @@
export * from './game-runner';
export * from './events';
export * from './engine-component';

View File

@ -0,0 +1,3 @@
export function clamp(x: number, min: number, max: number): number {
return Math.min(Math.max(x, min), max);
}

View File

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

View File

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

View File

@ -0,0 +1,3 @@
export * from './clamp';
export * from './debounce';
export * from './events';

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
},
"include": [
"src/**/*"
]
}

1219
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,3 @@
packages:
- 'apps/**'
- 'packages/**'

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"declaration": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
/* Linting */
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "bundler"
}
}