diff --git a/src/common.ts b/src/common.ts index 02cc0f4..5086488 100644 --- a/src/common.ts +++ b/src/common.ts @@ -211,4 +211,49 @@ interface LinkedElmt export function clamp(x: number, min: number, max: number): number { return x > max ? max : x < min ? min : x; -} \ No newline at end of file +} +export function lerp(x: number, a: number, b: number): number +{ + return (1-x)*a+x*b; +} +export class Random +{ + #seed: number; + #hasher: () => number; + + constructor(seed: number) + { + this.#seed = seed; + this.#hasher = mb32(hash(seed)); + } + next(): number + { + return this.#hasher(); + } + nextInt(min?: number, max?: number): number + { + return Math.floor(this.nextFloat(min, max)); + } + nextFloat(min?: number, max?: number): number + { + if(min === undefined && max === undefined) + { + min = 0; + max = 1; + } + else if(max === undefined) + { + max = min; + min = 0; + } + else if(min === undefined) + { + min = 0; + } + + return lerp(this.#hasher() / 2**32, min!, max!); + } +} + +const hash = (n: number) => Math.imul(n,2654435761) >>> 0; +const mb32 = (a: number) => (t?: number) => (a = a + 1831565813|0, t = Math.imul(a^a>>>15,1|a), t = t + Math.imul(t^t>>>7,61|t)^t, (t^t>>>14)>>>0); \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 2d460be..d2a6f5e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,24 +4,26 @@ 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 { Random, clamp } from './common'; import Selector from './renderer/selector.class'; 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 r = new Random(0); + +const quad = new Quadtree({x1: -FRUSTUMSIZE * Renderer.aspect / 2, x2: FRUSTUMSIZE * Renderer.aspect / 2, y1: -FRUSTUMSIZE / 2, y2: FRUSTUMSIZE / 2}); const assets: Asset[] = []; -for(let i = 0; i < 10; i++) +for(let i = 0; i < 10000; 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.5 + 0.05, Math.random() * 0.5 + 0.05) + .move(r.nextFloat(-0.5 * FRUSTUMSIZE * Renderer.aspect, 0.5 * FRUSTUMSIZE * Renderer.aspect), r.nextFloat(-0.5 * FRUSTUMSIZE, 0.5 * FRUSTUMSIZE)) + .rotate(r.nextFloat(Math.PI * 2)) + .scale(r.nextFloat(0.01, 0.15), r.nextFloat(0.01, 0.15)) Asset.instance.setMatrixAt(i, assets[i].mat); quad.insert(assets[i]); @@ -36,13 +38,16 @@ Renderer.scene.add(Asset.instance); Renderer.startRendering(); -Input.onDragStart((start, button) => { if(button & 1) Selector.hide(); }); +Input.onDragStart((_, button) => { if(button & 1) Selector.hide(); }); Input.onDragEnd((start, end, button) => { if(button & 1) { - const selection = quad.query({x1: start.x, x2: end.x, y1: start.y, y2: end.y}) as Asset[]; + const s = performance.now(); + const selection = quad.query({x1: Math.min(start.x, end.x), x2: Math.max(start.x, end.x), y1: Math.min(start.y, end.y), y2: Math.max(start.y, end.y)}) as Asset[]; + console.log("Fetching %s out of %s elements in %sms", selection.length, assets.length, performance.now() - s); - if(Input.keys['Shift']) Selector.add(selection); else Selector.select(selection); + if(Input.keys['Shift']) Selector.toggle(selection); + else Selector.select(selection); } }); Input.onDrag((delta, start, end, button) => { if(button & 1) Selector.preview(start, end); else Renderer.move(-delta.x, -delta.y); }); @@ -51,8 +56,9 @@ Input.onClick((point, button) => { { const selection = quad.fetch(point.x, point.y) as Asset[]; - if(Input.keys['Shift']) Selector.add(selection); else Selector.select(selection); + if(Input.keys['Shift']) Selector.toggle(selection); + else Selector.select(selection); } }); 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 +Input.onMove(p => { if(!Input.dragging) Selector.ghost(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 518d0dc..b4848be 100644 --- a/src/physics/quadtree.class.ts +++ b/src/physics/quadtree.class.ts @@ -1,6 +1,13 @@ import { LinkedList } from "../common"; -import { AABB, Point, intersects } from "./common"; +import Renderer from "../renderer/renderer.class"; +import { AABB, intersects } from "./common"; +import * as THREE from 'three'; +/** + * A node that contains elments is called a leaf. Its count is equals to the amount of children it holds. + * A node that contains other nodes is called a branch. Its count is equals to -1. + * The AABB of each node isn't stored and is computed on the go. (Not memory friendly in JS ?) + */ export default class Quadtree { #bounds: AABB; @@ -10,6 +17,7 @@ export default class Quadtree #content: T[]; #dirty: boolean = true; + #debugRect: THREE.Box3Helper[] = []; constructor(bounds: AABB, maxDepth?: number, maxElmts?: number) { @@ -24,21 +32,17 @@ export default class Quadtree } fetch(x: number, y: number): T[] { - const results = this.query({x1: x, x2: x, y1: y, y2: y}); - - results.length > 0 && console.log(results); - - return results; + return this.query({x1: x, x2: x, y1: y, y2: y}); } query(aabb: AABB): T[] { const result: number[] = []; - const leaves = this.#find_leaves(0, 0, this.#bounds, aabb); + const leaves = this.#find_leaves(0, 0, this.#bounds, aabb); //Search every mathed leaves for(let i = 0; i < leaves.length; i++) { const node = this.#nodes[leaves[i].index]; - node.children.forEach(e => { + node.children.forEach(e => { //Test every leaves elements if(!result.includes(e) && intersects(this.#content[e], aabb)) result.push(e); }); @@ -125,30 +129,41 @@ export default class Quadtree } } } + render(): void + { + Renderer.scene.remove(...this.#debugRect); + this.#debugRect = []; + + this.traverse((prop) => { + this.#debugRect.push(new THREE.Box3Helper(new THREE.Box3(new THREE.Vector3(prop.bounds.x1, prop.bounds.y1, 0), new THREE.Vector3(prop.bounds.x2, prop.bounds.y2, 0)))); + }); + + Renderer.scene.add(...this.#debugRect); + } #find_leaves(index: number, depth: number, bounds: AABB, elmtBounds: AABB): NodeProp[] { const stack: NodeProp[] = [], result: NodeProp[] = []; stack.push({index: index, depth: depth, bounds: bounds}); - //Fetch every nodes intersecting the element bounds + //Fetch every nodes to search for leaves that match the element bounds while(stack.length > 0) { const nodeProp = stack.pop()!; - //If the node contains elements + //If this is a leaf if(this.#nodes[nodeProp.index].count !== -1) result.push(nodeProp); else { - //Check intersection on each 4 sides of the node + //Otherwise, check intersection on each 4 sides of the node const children = this.#nodes[nodeProp.index].children.toArray(); const mx = nodeProp.bounds.x1 + (nodeProp.bounds.x2 - nodeProp.bounds.x1) / 2, my = nodeProp.bounds.y1 + (nodeProp.bounds.y2 - nodeProp.bounds.y1) / 2; if(elmtBounds.y1 <= my) { - if(elmtBounds.x1 <= mx) - stack.push({ index: children[0], depth: nodeProp.depth + 1, bounds: { x1: nodeProp.bounds.x1, x2: mx, y1: nodeProp.bounds.y1, y2: my }}); + if(elmtBounds.x1 <= mx) //Add a new + stack.push({ index: children[0], depth: nodeProp.depth + 1, bounds: { x1: nodeProp.bounds.x1, x2: mx, y1: nodeProp.bounds.y1, y2: my }}); if(elmtBounds.x2 > mx) stack.push({ index: children[1], depth: nodeProp.depth + 1, bounds: { x1: mx, x2: nodeProp.bounds.x2, y1: nodeProp.bounds.y1, y2: my }}); } @@ -171,15 +186,15 @@ export default class Quadtree //Split if the max amount of element is reached and the max depth isn't if(node.count == this.#maxElmts && depth < this.#maxDepth) { - const elmts = node.children.toArray(); + const elmts = node.children.toArray(); //Get every children node.children.clear(); node.count = -1; - for(let i = 0; i < 4; i++) + for(let i = 0; i < 4; i++) //Split in 4 nodes { node.children.add(this.#nodes.push({ children: new LinkedList(), count: 0 }) - 1); } - for(let i = 0; i < elmts.length; i++) + for(let i = 0; i < elmts.length; i++) //Insert back the children { this.#node_insert(index, depth, bounds, elmts[i]); } @@ -209,5 +224,5 @@ interface NodeProp interface Node { children: LinkedList; - count: number; //The count is used to get the amount of T elements in the current node. If the Node only contains other nodes, the count is = -1. + count: number; } \ No newline at end of file diff --git a/src/renderer/selector.class.ts b/src/renderer/selector.class.ts index b2279ae..6699b8c 100644 --- a/src/renderer/selector.class.ts +++ b/src/renderer/selector.class.ts @@ -9,6 +9,7 @@ export default class Selector static #selected: boolean = false; static #previewMesh: Three.Box3Helper; + static #ghostMesh: Three.Box3Helper; static #selectionMesh: Three.Box3Helper; static get selected(): boolean @@ -18,9 +19,11 @@ export default class Selector static init(): void { Selector.#previewMesh = new Three.Box3Helper(new Three.Box3(), 0x2980B9); + Selector.#ghostMesh = new Three.Box3Helper(new Three.Box3(), 0xffffff); Selector.#selectionMesh = new Three.Box3Helper(new Three.Box3(), 0xffffff); Renderer.scene.add(Selector.#previewMesh); + Renderer.scene.add(Selector.#ghostMesh); Renderer.scene.add(Selector.#selectionMesh); Selector.hide(); @@ -34,20 +37,18 @@ export default class Selector Selector.#previewMesh.visible = true; } - static ghostSelect(asset: Asset): void + static ghost(asset: Asset): void { - Selector.#assets = [asset]; - Selector.hide(); - if(!asset) + { + Selector.#ghostMesh.visible = false; return; - - Selector.#selected = false; + } - Selector.#selectionMesh.box.setFromArray([asset.x1, asset.y1, 0, asset.x2, asset.y2, 0]); - Selector.#previewMesh.updateMatrix(); + Selector.#ghostMesh.box.setFromArray([asset.x1, asset.y1, 0, asset.x2, asset.y2, 0]); + Selector.#ghostMesh.updateMatrix(); - Selector.#selectionMesh.visible = true; + Selector.#ghostMesh.visible = true; } static select(assets: Asset[]): void { @@ -67,13 +68,22 @@ export default class Selector assets.map(e => e.y2).reduce((p, v) => Math.max(p, v), -Infinity), 0 ]); - Selector.#previewMesh.updateMatrix(); + Selector.#selectionMesh.updateMatrix(); Selector.#selectionMesh.visible = true; } - static add(assets: Asset[]): void + static toggle(assets: Asset[]): void { - Selector.select([...Selector.#assets, ...assets].filter((e, i, a) => a.indexOf(e) === i)); + for(let i = 0; i < assets.length; i++) + { + const index = Selector.#assets.indexOf(assets[i]); + + if(index === -1) + Selector.#assets.push(assets[i]); + else + Selector.#assets.splice(index, 1); + } + Selector.select(Selector.#assets); } static clear(): void { @@ -84,6 +94,7 @@ export default class Selector static hide(): void { Selector.#previewMesh.visible = false; + Selector.#ghostMesh.visible = false; Selector.#selectionMesh.visible = false; } } \ No newline at end of file