diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0f9c415 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,286 @@ +{ + "name": "vtt-mapper", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vtt-mapper", + "version": "0.0.0", + "dependencies": { + "@types/three": "^0.165.0", + "stats.js": "^0.17.0", + "three": "^0.165.0" + }, + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^5.2.0" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.2", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stats.js": { + "version": "0.17.3", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.165.0", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "~23.1.1", + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.16", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.20.2", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "license": "MIT" + }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.38", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stats.js": { + "version": "0.17.0", + "license": "MIT" + }, + "node_modules/three": { + "version": "0.165.0", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.4.5", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.2.12", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json index 40a65a9..83717eb 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "bunx --bun vite --port 3000 --cors", + "dev": "bunx --bun vite", "build": "tsc && vite build", "preview": "vite preview" }, diff --git a/src/assets/asset.class.ts b/src/assets/asset.class.ts index 361a832..02e2269 100644 --- a/src/assets/asset.class.ts +++ b/src/assets/asset.class.ts @@ -2,8 +2,7 @@ import * as Three from 'three'; import * as CONST from '../consts'; import { AABB } from '../physics/common'; import Quadtree from '../physics/quadtree.class'; - -const UP = new Three.Vector3(0, 0, 1); +import { FreeList } from '../common'; const _position = new Three.Vector3(); const _rotation = new Three.Quaternion(); @@ -14,6 +13,7 @@ export default class Asset { mat: Three.Matrix4; layer: number; + selected: boolean = false; //@ts-expect-error #aabb: AABB; @@ -28,7 +28,8 @@ export default class Asset #quad?: number; static instance: Three.InstancedMesh = new Three.InstancedMesh(CONST.QUAD, new Three.MeshBasicMaterial({ color: new Three.Color(0xffffff) }), 1000000); - static assets: Asset[] = []; + static quadtree: Quadtree = new Quadtree({ x1: -CONST.RESOLUTION_X / 2, x2: CONST.RESOLUTION_X / 2, y1: -CONST.RESOLUTION_Y / 2, y2: CONST.RESOLUTION_Y / 2 }, 6, 10); + static assets: FreeList = new FreeList(); constructor(mat?: Three.Matrix4, layer?: number) { @@ -44,12 +45,26 @@ export default class Asset this.#updateAABB(); this.layer = layer ?? 0; - this.#index = Asset.assets.push(this) - 1; + this.#index = Asset.assets.insert(this); } - get aabb(): AABB - { + get aabb(): AABB { return this.#aabb; } + get posX(): number { + return this.#posX; + } + get posY(): number { + return this.#posY; + } + get rot(): number { + return this.#rot; + } + get scaleX(): number { + return this.#scaleX; + } + get scaleY(): number { + return this.#scaleY; + } #updateAABB(): void { const aabb = { x1: -0.5, x2: 0.5, y1: -0.5, y2: 0.5 }; @@ -71,13 +86,32 @@ export default class Asset y2: Math.max(y1, y2, y3, y4) }; } - insert(quad: Quadtree): void + insert(): Asset { - this.#quad = quad.insert(this.#index, this.#aabb); + this.#quad = Asset.quadtree.insert(this.#index, this.#aabb); + + return this; } - remove(quad: Quadtree): void + remove(): Asset { - this.#quad !== undefined && quad.remove(this.#quad); + this.#quad !== undefined && Asset.quadtree.remove(this.#quad); + Asset.assets.erase(this.#index); + Asset.instance.setMatrixAt(this.#index, this.mat.identity()); + Asset.instance.instanceMatrix.needsUpdate = true; + + return this; + } + update(quad: boolean = true): void + { + this.#updateAABB(); + Asset.instance.setMatrixAt(this.#index, this.mat); + Asset.instance.instanceMatrix.needsUpdate = true; + + if(quad) + { + this.#quad !== undefined && Asset.quadtree.remove(this.#quad); + this.#quad = Asset.quadtree.insert(this.#index, this.#aabb); + } } moveTo(x: number, y: number): Asset { @@ -86,7 +120,6 @@ export default class Asset e[12] = this.#posX = x; e[13] = this.#posY = y; - this.#updateAABB(); return this; } rotateTo(rad: number): Asset @@ -101,7 +134,6 @@ export default class Asset e[4] = sin * this.#scaleY; e[5] = cos * this.#scaleY; - this.#updateAABB(); return this; } scaleTo(x: number, y: number): Asset @@ -117,7 +149,6 @@ export default class Asset e[4] = sin * this.#scaleY; e[5] = cos * this.#scaleY; - this.#updateAABB(); return this; } } \ No newline at end of file diff --git a/src/common.ts b/src/common.ts index f053586..52d5c35 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,4 +1,5 @@ const DEFAULT_SIZE = 128; +const MAX_BYTE_LENGTH = 1024*1024*16; export class FreeList { @@ -17,10 +18,12 @@ export class FreeList { if(this.#free.empty) { + ++this.#length; return this.#data.push(element) - 1; } else { + ++this.#length; const index = this.#free.pop()!; this.#data[index] = element; return index; @@ -30,12 +33,45 @@ export class FreeList { delete this.#data[n]; this.#free.add(n); + this.#length--; } clear(): void { this.#data.length = 0; this.#free.clear(); } + forEach(cb: (e: T, i: number) => void): void { + let i = 0, offset = 0; + for (; i + offset < this.#data.length; ++i) { + while (this.#data[i + offset] === undefined && i + offset < this.#data.length) + ++offset; + + if (i + offset >= this.#data.length) + break; + + cb(this.#data[i + offset], i + offset); + } + if (i !== this.#length) + throw new Error("Something wants wrong."); + } + toArray(): T[] + { + let i = 0, offset = 0, arr = []; + for(; i + offset < this.#data.length; ++i) + { + while (this.#data[i + offset] === undefined && i + offset < this.#data.length) + ++offset; + + if (i + offset >= this.#data.length) + break; + + arr.push(this.#data[i + offset]); + } + if(i !== this.#length) + throw new Error("Something wants wrong."); + + return arr; + } get(index: number): T { @@ -52,7 +88,7 @@ export class FreeList } export class IntList { - #data: number[]; + #data: Int32Array; #fields: number; #capacity: number = DEFAULT_SIZE; #length: number = 0; @@ -63,7 +99,8 @@ export class IntList if(fields <= 0) throw new Error("Invalid field count"); - this.#data = new Array(this.#capacity * fields); + //@ts-ignore + this.#data = new Int32Array(new ArrayBuffer(this.#capacity * fields * Int32Array.BYTES_PER_ELEMENT, { maxByteLength: MAX_BYTE_LENGTH })); this.#fields = fields; } get length(): number @@ -101,11 +138,20 @@ export class IntList { const pos = (this.#length + 1) * this.#fields; - if(pos > this.#capacity) + if(pos > this.#capacity * this.#fields) { this.#capacity *= 2; - this.#data.length = this.#capacity * this.#fields; + //@ts-ignore + if(this.#data.buffer.resizable) + { + //@ts-ignore + this.#data.buffer.resize(clamp(this.#capacity * this.#fields * Int32Array.BYTES_PER_ELEMENT, 1, MAX_BYTE_LENGTH)); + } + else + { + throw new Error("Cannot resize the buffer"); + } } return this.#length++; @@ -139,7 +185,7 @@ export class IntList toArray(): number[] { - return this.#data.slice(0, this.#length); + return [...this.#data.slice(0, this.#length)]; } //DEBUG diff --git a/src/main.ts b/src/main.ts index 846e852..328e58c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,30 +1,29 @@ import * as THREE from 'three'; import Renderer from './renderer/renderer.class'; import Asset from './assets/asset.class'; -import Quadtree from './physics/quadtree.class'; import { RESOLUTION_X, RESOLUTION_Y } from './consts'; import Input from './renderer/input.class'; import { Random, clamp } from './common'; import Selector from './renderer/selector.class'; +export enum DragMode +{ + pan, move, select, scale, rotate +} + Renderer.init(); Input.init(Renderer.canvas); Selector.init(); const r = new Random(0); -const quad = new Quadtree({ x1: -RESOLUTION_X / 2, x2: RESOLUTION_X / 2, y1: -RESOLUTION_Y / 2, y2: RESOLUTION_Y / 2 }, 6, 10); +let dragMode = DragMode.pan, dragFrom: number | undefined; -for(let i = 0; i < 2; i++) +for(let i = 0; i < 10; i++) { - const asset = new Asset(new THREE.Matrix4(), 1); - - asset - .rotateTo(r.nextFloat(Math.PI * 2)) - .scaleTo(r.nextFloat(100, 300), r.nextFloat(100, 300)) - .moveTo(r.nextInt(-0.5 * RESOLUTION_X, 0.5 * RESOLUTION_X), r.nextInt(-0.5 * RESOLUTION_Y, 0.5 * RESOLUTION_Y)); - - Asset.instance.setMatrixAt(i, asset.mat); - asset.insert(quad); + new Asset(new THREE.Matrix4(), 1).rotateTo(r.nextFloat(Math.PI * 2)) + .scaleTo(r.nextFloat(10, 30), r.nextFloat(10, 30)) + .moveTo(r.nextInt(-0.5 * RESOLUTION_X, 0.5 * RESOLUTION_X), r.nextInt(-0.5 * RESOLUTION_Y, 0.5 * RESOLUTION_Y)) + .update(); } Asset.instance.count = Asset.assets.length; @@ -34,37 +33,94 @@ Asset.instance.computeBoundingSphere(); Renderer.scene.add(Asset.instance); -//quad.debug(); Renderer.startRendering(); -Input.onDragStart((_, button) => { if(button & 1) Selector.hide(); }); -Input.onDragEnd((s, e, button) => { +Input.onDragStart((s, button) => { if(button & 1) { - const n = performance.now(); - const selection = quad.query({x1: Math.min(s.x, e.x), x2: Math.max(s.x, e.x), y1: Math.min(s.y, e.y), y2: Math.max(s.y, e.y)}).map(e => Asset.assets[e]); - console.log("Fetching %s out of %s elements in %sms", selection.length, Asset.assets.length, performance.now() - n); - - if(Input.keys['Shift']) Selector.toggle(selection); - else Selector.select(selection); + if(Selector.selected) + { + const result = Selector.mousemove(s.x, s.y); + dragMode = result.mode; + dragFrom = result.details; + } + else + { + dragMode = DragMode.select; + Selector.hide(); + } + } + else + { + dragMode = DragMode.pan; + } +}); +Input.onDragEnd((s, e, _) => { + if(dragMode === DragMode.select) + { + const n = performance.now(); + const selection = Asset.quadtree.query({x1: Math.min(s.x, e.x), x2: Math.max(s.x, e.x), y1: Math.min(s.y, e.y), y2: Math.max(s.y, e.y)}).map(e => Asset.assets.get(e)); + console.log("Fetching %s out of %s elements in %sms", selection.length, Asset.assets.length, performance.now() - n); + + if(Input.keys['shift']) Selector.toggle(selection); + else Selector.select(selection); + } + else + { + Selector.selection.forEach(e => e.update()); + } +}); +Input.onDrag((delta, start, end, _) => { + if (dragMode === DragMode.select) + { + Selector.preview(start, end); + } + else if (dragMode === DragMode.move && Selector.selected) + { + Selector.move(delta.x, delta.y); + } + else if (dragMode === DragMode.scale && Selector.selected) + { + Selector.scale(dragFrom!, delta.x, delta.y); + } + else if(dragMode === DragMode.pan) + { + Renderer.move(-delta.x, -delta.y); } }); -Input.onDrag((delta, start, end, button) => { if(button & 1) Selector.preview(start, end); else Renderer.move(-delta.x, -delta.y); }); Input.onClick((p, button) => { if(button & 1) { - const selection = quad.fetch(p.x, p.y)[0]; + const selection = Asset.quadtree.fetch(p.x, p.y)[0]; if (selection === undefined) { - if (!Input.keys['Shift']) + if (!Input.keys['shift']) Selector.clear(); + dragMode = DragMode.select; return; } - if (Input.keys['Shift']) Selector.toggle([Asset.assets[selection]]); - else Selector.select([Asset.assets[selection]]); + if (Input.keys['shift']) Selector.toggle([Asset.assets.get(selection)]); + else Selector.select([Asset.assets.get(selection)]); } }); Input.onWheel(delta => Renderer.zoom = clamp(Renderer.zoom * 1 + (delta * -0.001), 0.9, 5)); -Input.onMove(p => { if (!Input.dragging) Selector.ghost(Asset.assets[quad.fetch(p.x, p.y)[0]]); }); \ No newline at end of file +Input.onMove(p => { + if (!Input.dragging) + { + Selector.ghost(Asset.assets.get(Asset.quadtree.fetch(p.x, p.y)[0])); + Selector.mousemove(p.x, p.y); + } +}); +Input.onInput((input) => { + if(input === 'delete') + { + Selector.selection.forEach(e => e.remove()); + Selector.clear(); + } + else if(input === 'a' && Input.keys['control']) + { + Selector.select(Asset.assets.toArray()); + } +}) \ No newline at end of file diff --git a/src/physics/common.ts b/src/physics/common.ts index df49374..c4213fb 100644 --- a/src/physics/common.ts +++ b/src/physics/common.ts @@ -19,6 +19,12 @@ export function intersects(aLeft: number, aTop: number, aRight: number, aBottom: } export function intersectsAsset(left: number, top: number, right: number, bottom: number, id: number): boolean { - const aabb = Asset.assets[id].aabb; + const aabb = Asset.assets.get(id).aabb; return intersects(left, top, right, bottom, aabb.x1, aabb.y1, aabb.x2, aabb.y2); +} +export function sqrtLen(x1: number, y1: number, x2: number, y2: number) { + return Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2); +} +export function len(x1: number, y1: number, x2: number, y2: number) { + return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); } \ No newline at end of file diff --git a/src/physics/quadtree.class.ts b/src/physics/quadtree.class.ts index 20127ff..f839580 100644 --- a/src/physics/quadtree.class.ts +++ b/src/physics/quadtree.class.ts @@ -34,7 +34,6 @@ export default class Quadtree #enodes: IntList = new IntList(2); #content: IntList = new IntList(5); - #dirty: boolean = true; #debugRect: THREE.Box3Helper[] = []; constructor(bounds: AABB, maxDepth?: number, maxElmts?: number) @@ -126,7 +125,7 @@ export default class Quadtree else this.#enodes.set(prev, QuadConsts.ENodeINext, next); - this.#nodes.set(prop, QuadConsts.NodeICount, this.#nodes.get(prop, QuadConsts.NodeICount - 1)); + this.#nodes.set(prop, QuadConsts.NodeICount, this.#nodes.get(prop, QuadConsts.NodeICount) - 1); } } @@ -138,19 +137,13 @@ export default class Quadtree this.#content.clear(); this.#enodes.clear(); - this.#dirty = false; - //DEBUG this.#debugRect.forEach(e => e.parent?.remove(e)); this.#debugRect = []; } cleanup(): void { - //Only cleanup if it's dirty. - //Allows the system to call the function at each loop iteration. - if(!this.#dirty) - return; - + let updated = false; _process.clear(); if(this.#nodes.get(0, QuadConsts.NodeICount) === -1) @@ -170,7 +163,7 @@ export default class Quadtree const count = this.#nodes.get(current, QuadConsts.NodeICount); - if(count == 0) //Count the amount of empty leaves + if(count === 0) //Count the amount of empty leaves ++empty else if(count === -1) //Add this node to the check process if it's a branch _process.set(_process.push(), 0, current); @@ -178,7 +171,7 @@ export default class Quadtree if(empty === 4) { - //Because of the way the IntList is made, it's preferable to erase in the reversed order + //Because of the way the IntList is made, it's preferable to erase in the reversed order as the last erased index becomes the first available index. this.#nodes.erase(fc + 3); this.#nodes.erase(fc + 2); this.#nodes.erase(fc + 1); @@ -187,8 +180,15 @@ export default class Quadtree //The branch becomes a empty leaf this.#nodes.set(node, QuadConsts.NodeICount, 0); this.#nodes.set(node, QuadConsts.NodeIFirst, -1); + + updated = true; } } + + if(updated) + { + //this.debug(); + } } traverse(cb: (index: number, depth: number, left: number, top: number, right: number, bottom: number, leaf: boolean) => void): void { @@ -258,6 +258,11 @@ export default class Quadtree const nodeBottom = _nodes.get(last, QuadConsts.PropIBtm); const nodeIndex = _nodes.get(last, QuadConsts.PropIIdx); const nodeDepth = _nodes.get(last, QuadConsts.PropIDpt); + + if (nodeIndex === -1) { + console.log(leaves, last, nodeIndex, nodeDepth); + debugger; + } _nodes.pop(); diff --git a/src/renderer/input.class.ts b/src/renderer/input.class.ts index 2464cee..13f5e81 100644 --- a/src/renderer/input.class.ts +++ b/src/renderer/input.class.ts @@ -167,12 +167,12 @@ export default class Input } static #keydown(e: KeyboardEvent): void { - Input.#keys[e.key] = true; + Input.#keys[e.key.toLowerCase()] = true; } static #keyup(e: KeyboardEvent): void { - Input.#keys[e.key] = false; + Input.#keys[e.key.toLowerCase()] = false; - Input.#inputCb && Input.#inputCb(e.key); + Input.#inputCb && Input.#inputCb(e.key.toLowerCase()); } } \ No newline at end of file diff --git a/src/renderer/renderer.class.ts b/src/renderer/renderer.class.ts index 9f4789e..0597fb5 100644 --- a/src/renderer/renderer.class.ts +++ b/src/renderer/renderer.class.ts @@ -3,7 +3,18 @@ import { RESOLUTION_X, RESOLUTION_Y } from '../consts'; import Stats from 'stats.js'; import { Point } from '../physics/common'; import Selector from './selector.class'; +import Asset from '../assets/asset.class'; +export enum CursorType +{ + default = "default", + pointer = "pointer", + move = "move", + updown = "ns-resize", + leftright = "ew-resize", + nesw = "nesw-resize", + nwse = "nwse-resize", +} export default class Renderer { static scene: Three.Scene; @@ -87,6 +98,7 @@ export default class Renderer this.#stats.begin(); Selector.update(); this.renderer.render(this.scene, this.camera); + Asset.quadtree.cleanup(); this.#stats.end(); } static startRendering(): void @@ -97,4 +109,8 @@ export default class Renderer { this.renderer.setAnimationLoop(null); } + static cursor(type?: CursorType): void + { + Renderer.canvas.style.setProperty("cursor", type ?? CursorType.default); + } } \ No newline at end of file diff --git a/src/renderer/selector.class.ts b/src/renderer/selector.class.ts index 0234094..ad99e29 100644 --- a/src/renderer/selector.class.ts +++ b/src/renderer/selector.class.ts @@ -1,12 +1,22 @@ import * as Three from "three"; import Asset from "../assets/asset.class"; -import { Point } from "../physics/common"; -import Renderer from "./renderer.class"; +import { Point, intersects, intersectsAsset, sqrtLen } from "../physics/common"; +import Renderer, { CursorType } from "./renderer.class"; import { QUAD } from '../consts'; +import { DragMode } from "../main"; + +const _vector = new Three.Vector3(); + +const orientation = { + 0: { x: -1, y: -1 }, + 1: { x: -1, y: 1 }, + 2: { x: -1, y: 1 }, + 3: { x: -1, y: 1 }, +}; export default class Selector { - static #assets: Asset[]; + static #assets: Asset[] = []; static #selected: boolean = false; static #previousZoom: number = 1; @@ -68,6 +78,8 @@ export default class Selector } static ghost(asset: Asset): void { + Renderer.cursor(asset && !asset.selected ? CursorType.pointer : undefined); + if(!asset) { Selector.#ghostMesh.visible = false; @@ -81,11 +93,18 @@ export default class Selector } static select(assets: Asset[]): void { + for (let i = 0; i < Selector.#assets.length; i++) { + Selector.#assets[i].selected = false; + } + Selector.#assets = assets; Selector.hide(); if(assets.length <= 0) + { + Selector.#selected = false; return; + } Selector.#selected = true; @@ -96,6 +115,8 @@ export default class Selector minY = Math.min(minY, assets[i].aabb.y1); maxX = Math.max(maxX, assets[i].aabb.x2); maxY = Math.max(maxY, assets[i].aabb.y2); + + assets[i].selected = true; } const scale = Math.max((maxX - minX) / 2, (maxY - minY) / 2) @@ -125,6 +146,7 @@ export default class Selector for(let i = 0; i < assets.length; i++) { const index = Selector.#assets.indexOf(assets[i]); + assets[i].selected = false; if(index === -1) Selector.#assets.push(assets[i]); @@ -135,7 +157,11 @@ export default class Selector } static clear(): void { + for(let i = 0; i < Selector.#assets.length; i++) + Selector.#assets[i].selected = false; + Selector.#assets = []; + Selector.#selected = false; Selector.hide(); } @@ -162,4 +188,78 @@ export default class Selector Selector.gizmoScale[3].scale.set(20 / Renderer.zoom, 20 / Renderer.zoom, 1); } } + static mousemove(x: number, y: number): { mode: DragMode, details?: number } + { + if(!Selector.selected) + return { mode: DragMode.select }; + + const minX = Selector.#selectionMesh.box.min.x, + minY = Selector.#selectionMesh.box.min.y, + maxX = Selector.#selectionMesh.box.max.x, + maxY = Selector.#selectionMesh.box.max.y; + + if (sqrtLen(x, y, Selector.gizmoScale[0].position.x, Selector.gizmoScale[0].position.y) <= 500 / Renderer.zoom) + { + Renderer.cursor(CursorType.nesw); + return { mode: DragMode.scale, details: 0 }; + } + else if (sqrtLen(x, y, Selector.gizmoScale[1].position.x, Selector.gizmoScale[1].position.y) <= 500 / Renderer.zoom) + { + Renderer.cursor(CursorType.nwse); + return { mode: DragMode.scale, details: 1 }; + } + else if (sqrtLen(x, y, Selector.gizmoScale[2].position.x, Selector.gizmoScale[2].position.y) <= 500 / Renderer.zoom) + { + Renderer.cursor(CursorType.nwse); + return { mode: DragMode.scale, details: 2 }; + } + else if (sqrtLen(x, y, Selector.gizmoScale[3].position.x, Selector.gizmoScale[3].position.y) <= 500 / Renderer.zoom) + { + Renderer.cursor(CursorType.nesw); + return { mode: DragMode.scale, details: 3 }; + } + else if (intersects(x, y, x, y, minX, minY, maxX, maxY)) + { + Renderer.cursor(CursorType.move); + return { mode: DragMode.move }; + } + + return { mode: DragMode.select }; + } + static move(x: number, y: number): void + { + Selector.selection.forEach(e => e.moveTo(e.posX + x, e.posY + y).update(false)); + + Asset.instance.computeBoundingBox(); + Asset.instance.computeBoundingSphere(); + + Selector.#selectionMesh.box.translate(_vector.set(x, y, 0)); + Selector.#ghostMesh.box.translate(_vector.set(x, y, 0)); + + Selector.gizmoScale[0].position.add({ x: x, y: y, z: 0 }); + Selector.gizmoScale[1].position.add({ x: x, y: y, z: 0 }); + Selector.gizmoScale[2].position.add({ x: x, y: y, z: 0 }); + Selector.gizmoScale[3].position.add({ x: x, y: y, z: 0 }); + } + static scale(dragFrom: number, x: number, y: number): void + { + console.log(dragFrom, x, y); + + //@ts-ignore + x *= orientation[dragFrom].x; + //@ts-ignore + y *= orientation[dragFrom].y; + + Selector.selection.forEach(e => e.scaleTo(e.scaleX + x, e.scaleY + y).moveTo(e.posX - x / 2, e.posY - y / 2).update(false)); + + Selector.#selectionMesh.box.translate(_vector.set(-x / 2, -y / 2, 0)); + + Selector.gizmoScale[0].position.add({ x: -x / 2, y: -y / 2, z: 0 }); + Selector.gizmoScale[1].position.add({ x: -x / 2, y: -y / 2, z: 0 }); + Selector.gizmoScale[2].position.add({ x: -x / 2, y: -y / 2, z: 0 }); + Selector.gizmoScale[3].position.add({ x: -x / 2, y: -y / 2, z: 0 }); + + Asset.instance.computeBoundingBox(); + Asset.instance.computeBoundingSphere(); + } } \ No newline at end of file