diff --git a/bun.lockb b/bun.lockb index a63096b..a6ce7f5 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src/main.ts b/src/main.ts index b4edf5e..2d460be 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,106 +3,56 @@ import Renderer from './renderer/renderer.class'; import Asset from './assets/asset.class'; import Quadtree from './physics/quadtree.class'; import { FRUSTUMSIZE } from './consts'; +import Input from './renderer/input.class'; import { clamp } from './common'; +import Selector from './renderer/selector.class'; -performance.mark("start"); Renderer.init(); +Input.init(Renderer.canvas); +Selector.init(); + const quad = new Quadtree({x1: -FRUSTUMSIZE * Renderer.aspect, x2: FRUSTUMSIZE * Renderer.aspect, y1: -FRUSTUMSIZE, y2: FRUSTUMSIZE}); -performance.mark("init"); 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] .move((Math.random() - 0.5) * FRUSTUMSIZE * Renderer.aspect, (Math.random() - 0.5) * FRUSTUMSIZE) .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); 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.computeBoundingBox(); 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(highlightHelper); -Renderer.scene.add(sphereMesh); Renderer.startRendering(); -window.addEventListener('mousedown', drag); -window.addEventListener('mouseup', select); -window.addEventListener('mousemove', hover); - -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) +Input.onDragStart((start, button) => { if(button & 1) Selector.hide(); }); +Input.onDragEnd((start, end, button) => { + if(button & 1) { - const cursor = Renderer.screenSpaceToCameraSpace(e.clientX, e.clientY, false); - Renderer.move(dragCursor.x - cursor.x, dragCursor.y - cursor.y); + const selection = quad.query({x1: start.x, x2: end.x, y1: start.y, y2: end.y}) as Asset[]; - dragCursor = cursor; - return; + if(Input.keys['Shift']) Selector.add(selection); else Selector.select(selection); } - 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); - - 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; - } + if(Input.keys['Shift']) Selector.add(selection); else Selector.select(selection); } -} -function zoom(e: WheelEvent): void -{ - Renderer.zoom = clamp(Renderer.zoom * 1 + (e.deltaY * -0.001), 1, 5); -} - -setInterval(() => { - console.log("Average query time: %s µs", overallTimer / overallSamples * 1000); -}, 1000); \ No newline at end of file +}); +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); }); \ No newline at end of file diff --git a/src/physics/quadtree.class.ts b/src/physics/quadtree.class.ts index 31d1013..518d0dc 100644 --- a/src/physics/quadtree.class.ts +++ b/src/physics/quadtree.class.ts @@ -24,7 +24,11 @@ export default class Quadtree } 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[] { diff --git a/src/renderer/input.class.ts b/src/renderer/input.class.ts new file mode 100644 index 0000000..bf9ab1e --- /dev/null +++ b/src/renderer/input.class.ts @@ -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 = {}; + + 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(); + } +} \ No newline at end of file diff --git a/src/renderer/renderer.class.ts b/src/renderer/renderer.class.ts index d470876..2632462 100644 --- a/src/renderer/renderer.class.ts +++ b/src/renderer/renderer.class.ts @@ -43,6 +43,10 @@ export default class Renderer return false; } } + static get canvas(): HTMLCanvasElement + { + return this.renderer.domElement; + } static get zoom(): number { return this.#zoom; @@ -60,8 +64,8 @@ export default class Renderer this.camera.position.x += x; this.camera.position.y += y; } - static screenSpaceToCameraSpace(x: number, y: number, offset: boolean): 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 }; + static screenSpaceToCameraSpace(x: number, y: number, omit: boolean = false): Point { + 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 { diff --git a/src/renderer/selector.class.ts b/src/renderer/selector.class.ts new file mode 100644 index 0000000..b2279ae --- /dev/null +++ b/src/renderer/selector.class.ts @@ -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; + } +} \ No newline at end of file