New Input and Selection manager

This commit is contained in:
Peaceultime 2024-06-11 14:14:22 +02:00
parent ca7b1d1956
commit 99ed6d14f7
6 changed files with 299 additions and 74 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -3,106 +3,56 @@ import Renderer from './renderer/renderer.class';
import Asset from './assets/asset.class'; import Asset from './assets/asset.class';
import Quadtree from './physics/quadtree.class'; import Quadtree from './physics/quadtree.class';
import { FRUSTUMSIZE } from './consts'; import { FRUSTUMSIZE } from './consts';
import Input from './renderer/input.class';
import { clamp } from './common'; import { clamp } from './common';
import Selector from './renderer/selector.class';
performance.mark("start");
Renderer.init(); Renderer.init();
Input.init(Renderer.canvas);
Selector.init();
const quad = new Quadtree({x1: -FRUSTUMSIZE * Renderer.aspect, x2: FRUSTUMSIZE * Renderer.aspect, y1: -FRUSTUMSIZE, y2: FRUSTUMSIZE}); const quad = new Quadtree({x1: -FRUSTUMSIZE * Renderer.aspect, x2: FRUSTUMSIZE * Renderer.aspect, y1: -FRUSTUMSIZE, y2: FRUSTUMSIZE});
performance.mark("init");
const assets: Asset[] = []; const assets: Asset[] = [];
for(let i = 0; i < 10000; i++) for(let i = 0; i < 10; i++)
{ {
assets[i] = new Asset(new THREE.Matrix4(), 1); assets[i] = new Asset(new THREE.Matrix4(), 1);
assets[i] assets[i]
.move((Math.random() - 0.5) * FRUSTUMSIZE * Renderer.aspect, (Math.random() - 0.5) * FRUSTUMSIZE) .move((Math.random() - 0.5) * FRUSTUMSIZE * Renderer.aspect, (Math.random() - 0.5) * FRUSTUMSIZE)
.rotate(Math.random() * Math.PI * 2) .rotate(Math.random() * Math.PI * 2)
.scale(Math.random() * 0.05 + 0.005, Math.random() * 0.05 + 0.005) .scale(Math.random() * 0.5 + 0.05, Math.random() * 0.5 + 0.05)
Asset.instance.setMatrixAt(i, assets[i].mat); Asset.instance.setMatrixAt(i, assets[i].mat);
quad.insert(assets[i]); quad.insert(assets[i]);
} }
performance.mark("ready");
const highlightBox = new THREE.Box3();
const highlightHelper = new THREE.Box3Helper(highlightBox, 0xffffff);
highlightHelper.visible = false;
let overallTimer = 0, overallSamples = 0;
Asset.instance.count = assets.length; Asset.instance.count = assets.length;
Asset.instance.computeBoundingBox(); Asset.instance.computeBoundingBox();
Asset.instance.computeBoundingSphere(); Asset.instance.computeBoundingSphere();
const sphere = new THREE.SphereGeometry(0.05);
const sphereMesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({ wireframe: true, }));
Renderer.scene.add(Asset.instance); Renderer.scene.add(Asset.instance);
Renderer.scene.add(highlightHelper);
Renderer.scene.add(sphereMesh);
Renderer.startRendering(); Renderer.startRendering();
window.addEventListener('mousedown', drag); Input.onDragStart((start, button) => { if(button & 1) Selector.hide(); });
window.addEventListener('mouseup', select); Input.onDragEnd((start, end, button) => {
window.addEventListener('mousemove', hover); if(button & 1)
window.addEventListener('wheel', zoom);
console.log("Start time: %sms", performance.measure("Start time", "start", "init").duration);
console.log("Init time: %sms", performance.measure("Init time", "init", "ready").duration);
let cursor = { x: 0, y: 0 };
let dragCursor = {x: 0, y: 0 }, dragging = false;
function drag(e: MouseEvent): void
{
dragCursor = Renderer.screenSpaceToCameraSpace(e.clientX, e.clientY, false);
dragging = true;
}
function select(e: MouseEvent): void
{
dragging = false;
}
function hover(e: MouseEvent): void
{
if(dragging === true)
{ {
const cursor = Renderer.screenSpaceToCameraSpace(e.clientX, e.clientY, false); const selection = quad.query({x1: start.x, x2: end.x, y1: start.y, y2: end.y}) as Asset[];
Renderer.move(dragCursor.x - cursor.x, dragCursor.y - cursor.y);
dragCursor = cursor; if(Input.keys['Shift']) Selector.add(selection); else Selector.select(selection);
return;
} }
else });
Input.onDrag((delta, start, end, button) => { if(button & 1) Selector.preview(start, end); else Renderer.move(-delta.x, -delta.y); });
Input.onClick((point, button) => {
if(button & 1)
{ {
const cursor = Renderer.screenSpaceToCameraSpace(e.clientX, e.clientY, true); const selection = quad.fetch(point.x, point.y) as Asset[];
sphereMesh.position.set(cursor.x, cursor.y, 0); if(Input.keys['Shift']) Selector.add(selection); else Selector.select(selection);
overallTimer -= performance.now();
const assets = quad.fetch(cursor.x, cursor.y) as Asset[];
overallTimer += performance.now();
overallSamples++;
if (assets.length > 0) {
highlightHelper.box.setFromArray([assets[0].x1, assets[0].y1, 0, assets[0].x2, assets[0].y2, 0]);
highlightHelper.updateMatrixWorld();
highlightHelper.visible = true;
}
else {
highlightHelper.visible = false;
}
} }
} });
function zoom(e: WheelEvent): void Input.onWheel(delta => Renderer.zoom = clamp(Renderer.zoom * 1 + (delta * -0.001), 1, 5));
{ Input.onMove(p => { if(!Input.dragging && !Selector.selected) Selector.ghostSelect(quad.fetch(p.x, p.y)[0] as Asset); });
Renderer.zoom = clamp(Renderer.zoom * 1 + (e.deltaY * -0.001), 1, 5);
}
setInterval(() => {
console.log("Average query time: %s µs", overallTimer / overallSamples * 1000);
}, 1000);

View File

@ -24,7 +24,11 @@ export default class Quadtree<T extends AABB>
} }
fetch(x: number, y: number): T[] fetch(x: number, y: number): T[]
{ {
return this.query({x1: x, x2: x, y1: y, y2: y}); const results = this.query({x1: x, x2: x, y1: y, y2: y});
results.length > 0 && console.log(results);
return results;
} }
query(aabb: AABB): T[] query(aabb: AABB): T[]
{ {

178
src/renderer/input.class.ts Normal file
View File

@ -0,0 +1,178 @@
import { Point } from "../physics/common";
import Renderer from "./renderer.class";
const dblClickTiming = 1000;
export default class Input
{
static #cursor: Point = {x: 0, y: 0};
static #delta: Point = {x: 0, y: 0};
static #holdButtons: number;
static #previousClickTime: number;
//Drag util fields
static #dragging: boolean = false;
static #dragStarted: boolean = false;
static #dragInitPos: Point = {x: 0, y: 0};
static #dragPrevPos: Point = {x: 0, y: 0};
static #scroll: number = 0;
static #keys: Record<string, boolean> = {};
static #canvas: HTMLCanvasElement;
static #clickCb?: (point: Point, button: number) => void;
static #dblClickCb?: (point: Point, button: number) => void;
static #moveCb?: (point: Point) => void;
static #dragStartCb?: (start: Point, button: number) => void;
static #dragCb?: (delta: Point, start: Point, end: Point, button: number) => void;
static #dragEndCb?: (start: Point, end: Point, button: number) => void;
static #wheelCb?: (delta: number) => void;
static #inputCb?: () => void;
static init(canvas: HTMLCanvasElement)
{
Input.#canvas = canvas;
canvas.addEventListener('mousedown', Input.#mousedown.bind(Input), false);
canvas.addEventListener('mouseup', Input.#mouseup.bind(Input), false);
canvas.addEventListener('mousemove', Input.#mousemove.bind(Input), false);
canvas.addEventListener('wheel', Input.#wheel.bind(Input), false);
window.addEventListener('keydown', Input.#keydown.bind(Input), false);
window.addEventListener('keyup', Input.#keyup.bind(Input), false);
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
}
static get dragging()
{
return Input.#dragging && Input.#dragStarted;
}
static get cursor()
{
return Input.#cursor;
}
static get delta()
{
return Input.#delta;
}
static get scroll()
{
return Input.#scroll;
}
static get keys()
{
return Input.#keys;
}
static onClick(cb: (point: Point, button: number) => void): void
{
Input.#clickCb = cb;
}
static onMove(cb: (point: Point) => void): void
{
Input.#moveCb = cb;
}
static onDblClick(cb: (point: Point, button: number) => void): void
{
Input.#dblClickCb = cb;
}
static onDragStart(cb: (start: Point, button: number) => void): void
{
Input.#dragStartCb = cb;
}
static onDrag(cb: (delta: Point, start: Point, end: Point, button: number) => void): void
{
Input.#dragCb = cb;
}
static onDragEnd(cb: (start: Point, end: Point, button: number) => void): void
{
Input.#dragEndCb = cb;
}
static onWheel(cb: (delta: number) => void): void
{
Input.#wheelCb = cb;
}
static onInput(cb: () => void): void
{
Input.#inputCb = cb;
}
static #mousedown(e: MouseEvent): void
{
e.preventDefault();
Input.#holdButtons = e.buttons;
Input.#dragging = true;
Input.#dragStarted = false;
Input.#dragInitPos = Renderer.screenSpaceToCameraSpace(e.clientX, e.clientY);
Input.#dragPrevPos = Renderer.screenSpaceToCameraSpace(e.clientX, e.clientY, true);
}
static #mousemove(e: MouseEvent): void
{
e.preventDefault();
if(Input.#dragging && !Input.#dragStarted)
{
Input.#dragStartCb && Input.#dragStartCb(Input.#dragInitPos, e.buttons);
Input.#dragStarted = true;
}
const cursor = Renderer.screenSpaceToCameraSpace(e.clientX, e.clientY);
const cursorOmitted = Renderer.screenSpaceToCameraSpace(e.clientX, e.clientY, true);
Input.#cursor = cursor;
if(Input.dragging)
{
Input.#delta.x = cursorOmitted.x - this.#dragPrevPos.x;
Input.#delta.y = cursorOmitted.y - this.#dragPrevPos.y;
Input.#dragPrevPos = cursorOmitted;
Input.#dragCb && Input.#dragCb(Input.#delta, Input.#dragInitPos, Input.#cursor, e.buttons);
}
Input.#moveCb && Input.#moveCb(Input.#cursor);
}
static #mouseup(e: MouseEvent): void
{
e.preventDefault();
if(Input.dragging)
Input.#dragEndCb && Input.#dragEndCb(Input.#dragInitPos, Input.#cursor, Input.#holdButtons);
else
{
Input.#clickCb && Input.#clickCb(Input.#cursor, Input.#holdButtons);
const timing = performance.now();
if(timing - Input.#previousClickTime <= dblClickTiming)
Input.#dblClickCb && Input.#dblClickCb(Input.#cursor, Input.#holdButtons);
Input.#previousClickTime = timing;
}
Input.#holdButtons = e.buttons;
Input.#dragging = false;
}
static #wheel(e: WheelEvent): void
{
e.preventDefault();
Input.#scroll = e.deltaY;
Input.#wheelCb && Input.#wheelCb(Input.scroll);
}
static #keydown(e: KeyboardEvent): void
{
Input.#keys[e.key] = true;
}
static #keyup(e: KeyboardEvent): void
{
Input.#keys[e.key] = false;
Input.#inputCb && Input.#inputCb();
}
}

View File

@ -43,6 +43,10 @@ export default class Renderer
return false; return false;
} }
} }
static get canvas(): HTMLCanvasElement
{
return this.renderer.domElement;
}
static get zoom(): number static get zoom(): number
{ {
return this.#zoom; return this.#zoom;
@ -60,8 +64,8 @@ export default class Renderer
this.camera.position.x += x; this.camera.position.x += x;
this.camera.position.y += y; this.camera.position.y += y;
} }
static screenSpaceToCameraSpace(x: number, y: number, offset: boolean): Point { static screenSpaceToCameraSpace(x: number, y: number, omit: boolean = false): Point {
return { x: ((x / window.innerWidth - 0.5) * FRUSTUMSIZE * this.aspect - (offset ? this.#pos.x : 0)) / this.#zoom, y: (- (y / window.innerHeight - 0.5) * FRUSTUMSIZE - (offset ? this.#pos.x : 0)) / this.zoom }; return { x: ((x / window.innerWidth - 0.5) * FRUSTUMSIZE * this.aspect) / this.#zoom + (omit ? 0 : this.#pos.x), y: (- (y / window.innerHeight - 0.5) * FRUSTUMSIZE) / this.zoom + (omit ? 0 : this.#pos.y) };
} }
static #resize(): void static #resize(): void
{ {

View File

@ -0,0 +1,89 @@
import * as Three from "three";
import Asset from "../assets/asset.class";
import { Point } from "../physics/common";
import Renderer from "./renderer.class";
export default class Selector
{
static #assets: Asset[];
static #selected: boolean = false;
static #previewMesh: Three.Box3Helper;
static #selectionMesh: Three.Box3Helper;
static get selected(): boolean
{
return Selector.#selected;
}
static init(): void
{
Selector.#previewMesh = new Three.Box3Helper(new Three.Box3(), 0x2980B9);
Selector.#selectionMesh = new Three.Box3Helper(new Three.Box3(), 0xffffff);
Renderer.scene.add(Selector.#previewMesh);
Renderer.scene.add(Selector.#selectionMesh);
Selector.hide();
}
static preview(start: Point, end: Point): void
{
Selector.hide();
Selector.#previewMesh.box.setFromArray([start.x, start.y, 0, end.x, end.y, 0]);
Selector.#previewMesh.updateMatrix();
Selector.#previewMesh.visible = true;
}
static ghostSelect(asset: Asset): void
{
Selector.#assets = [asset];
Selector.hide();
if(!asset)
return;
Selector.#selected = false;
Selector.#selectionMesh.box.setFromArray([asset.x1, asset.y1, 0, asset.x2, asset.y2, 0]);
Selector.#previewMesh.updateMatrix();
Selector.#selectionMesh.visible = true;
}
static select(assets: Asset[]): void
{
Selector.#assets = assets;
Selector.hide();
if(assets.length <= 0)
return;
Selector.#selected = true;
Selector.#selectionMesh.box.setFromArray([
assets.map(e => e.x1).reduce((p, v) => Math.min(p, v), Infinity),
assets.map(e => e.y1).reduce((p, v) => Math.min(p, v), Infinity),
0,
assets.map(e => e.x2).reduce((p, v) => Math.max(p, v), -Infinity),
assets.map(e => e.y2).reduce((p, v) => Math.max(p, v), -Infinity),
0
]);
Selector.#previewMesh.updateMatrix();
Selector.#selectionMesh.visible = true;
}
static add(assets: Asset[]): void
{
Selector.select([...Selector.#assets, ...assets].filter((e, i, a) => a.indexOf(e) === i));
}
static clear(): void
{
Selector.#assets = [];
Selector.hide();
}
static hide(): void
{
Selector.#previewMesh.visible = false;
Selector.#selectionMesh.visible = false;
}
}