From 15caad7fad464f8603ea87ad0d11fbe60148751e Mon Sep 17 00:00:00 2001 From: Thomas Forgione Date: Sat, 22 Jun 2024 15:45:44 +0200 Subject: [PATCH] Typescript --- .gitignore | 2 + Makefile | 3 + package-lock.json | 66 +++++ package.json | 15 + static/calibration-visualiser.js | 454 ------------------------------- templates/calibration.html | 10 +- ts/Animation.ts | 48 ++++ ts/Calibration.ts | 29 ++ ts/CameraObject.ts | 132 +++++++++ ts/Engine.ts | 314 +++++++++++++++++++++ ts/Led.ts | 248 +++++++++++++++++ ts/main.ts | 2 + tsconfig.json | 14 + 13 files changed, 874 insertions(+), 463 deletions(-) create mode 100644 Makefile create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 static/calibration-visualiser.js create mode 100644 ts/Animation.ts create mode 100644 ts/Calibration.ts create mode 100644 ts/CameraObject.ts create mode 100644 ts/Engine.ts create mode 100644 ts/Led.ts create mode 100644 ts/main.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index bae087f..f71342e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ data +node_modules +static/calibration-visualiser.* # Created by https://www.toptal.com/developers/gitignore/api/python # Edit at https://www.toptal.com/developers/gitignore?templates=python diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..750fac4 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +all: ts/*ts + tsc + esbuild ts/main.ts --bundle --minify --sourcemap --target=firefox57 --outfile=static/calibration-visualiser.js diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..aadbc8a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,66 @@ +{ + "name": "nenu-scanner", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nenu-scanner", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/three": "^0.165.0", + "three": "^0.165.0" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.2", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.2.tgz", + "integrity": "sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==", + "license": "MIT" + }, + "node_modules/@types/stats.js": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", + "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.165.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.165.0.tgz", + "integrity": "sha512-AJK8JZAFNBF0kBXiAIl5pggYlzAGGA8geVYQXAcPCEDRbyA+oEjkpUBcJJrtNz6IiALwzGexFJGZG2yV3WsYBw==", + "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.17", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.17.tgz", + "integrity": "sha512-JYcclaQIlisHRXM9dMF7SeVvQ54kcYc7QK1eKCExCTLKWnZDxP4cp/rXH4Uoa1j5+5oQJ0Cc2sZC/PWiiG4q2g==", + "license": "MIT" + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "license": "MIT" + }, + "node_modules/three": { + "version": "0.165.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.165.0.tgz", + "integrity": "sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..37df8da --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "nenu-scanner", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@types/three": "^0.165.0", + "three": "^0.165.0" + } +} diff --git a/static/calibration-visualiser.js b/static/calibration-visualiser.js deleted file mode 100644 index 49757ac..0000000 --- a/static/calibration-visualiser.js +++ /dev/null @@ -1,454 +0,0 @@ -import * as THREE from 'three'; -import { OrbitControls } from 'orbit-controls'; - -class Pyramid extends THREE.Object3D { - constructor() { - super(); - - let width = 1; - let height = 1; - let length = 2; - - let vertices = [ - new THREE.Vector3( 0, 0, 0), - new THREE.Vector3( width, height, length), - new THREE.Vector3(-width, height, length), - new THREE.Vector3(-width, -height, length), - new THREE.Vector3( width, -height, length), - ] - - // Faces - { - let material = new THREE.MeshPhongMaterial({color: 0xffffff}); - material.transparent = true; - material.opacity = 0.8; - - let geometry = new THREE.BufferGeometry(); - let faces = [ - // Sides of the pyramid - [0, 2, 1], - [0, 3, 2], - [0, 4, 3], - [0, 1, 4], - - // Base of the pyramid - [1, 2, 3], - [1, 3, 4], - ] - - let buffer = new Float32Array(3 * 3 * faces.length); - - for (let faceIndex in faces) { - let face = faces[faceIndex]; - buffer[faceIndex * 3 * 3 + 0] = vertices[face[0]].x; - buffer[faceIndex * 3 * 3 + 1] = vertices[face[0]].y; - buffer[faceIndex * 3 * 3 + 2] = vertices[face[0]].z; - - buffer[faceIndex * 3 * 3 + 3] = vertices[face[1]].x; - buffer[faceIndex * 3 * 3 + 4] = vertices[face[1]].y; - buffer[faceIndex * 3 * 3 + 5] = vertices[face[1]].z; - - buffer[faceIndex * 3 * 3 + 6] = vertices[face[2]].x; - buffer[faceIndex * 3 * 3 + 7] = vertices[face[2]].y; - buffer[faceIndex * 3 * 3 + 8] = vertices[face[2]].z; - } - - - geometry.setAttribute('position', new THREE.BufferAttribute(buffer, 3)); - const mesh = new THREE.Mesh(geometry, material); - mesh.layers.enable(1); - this.mesh = mesh; - this.add(mesh); - } - - // Lines - { - let material = new THREE.LineBasicMaterial({color: 0x990000}); - let geometry = new THREE.BufferGeometry(); - - let width = 1; - let height = 1; - let length = 2; - - let lines = [ - [0, 1], - [0, 2], - [0, 3], - [0, 4], - [1, 2], - [2, 3], - [3, 4], - [4, 1], - ] - - let buffer = new Float32Array(2 * 3 * lines.length); - - for (let lineIndex in lines) { - let line = lines[lineIndex]; - buffer[lineIndex * 2 * 3 + 0] = vertices[line[0]].x; - buffer[lineIndex * 2 * 3 + 1] = vertices[line[0]].y; - buffer[lineIndex * 2 * 3 + 2] = vertices[line[0]].z; - - buffer[lineIndex * 2 * 3 + 3] = vertices[line[1]].x; - buffer[lineIndex * 2 * 3 + 4] = vertices[line[1]].y; - buffer[lineIndex * 2 * 3 + 5] = vertices[line[1]].z; - } - - - geometry.setAttribute('position', new THREE.BufferAttribute(buffer, 3)); - const mesh = new THREE.Line(geometry, material); - mesh.layers.enable(1); - this.lines = mesh; - - this.add(mesh); - } - } -} - -class Led extends THREE.Mesh { - constructor() { - super( - new THREE.SphereGeometry(0.7, 32, 16), - new THREE.MeshBasicMaterial({ color: 0x555500 }), - ); - - this.on = false; - this.isHovering = false; - } - - setLines(lines, spheres) { - const material = new THREE.LineBasicMaterial({ - color: 0x0000ff - }); - this.lines = new THREE.Object3D(); - for (let index = 0; index < lines.length; index++) { - let line = lines[index]; - let sphere = spheres[index]; - let vertices = new Float32Array([ - -this.position.x - sphere[1], - -this.position.y - sphere[0], - -this.position.z + sphere[2], - -line[1] * 100, -line[0] * 100, line[2] * 100, - ]); - let geometry = new THREE.BufferGeometry(); - geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) ); - let mesh = new THREE.Line(geometry, material); - this.lines.add(mesh); - } - this.lines.visible = false; - this.add(this.lines); - } - - hover() { - if (this.on) { - return; - } - - this.isHovering = true; - this.refreshColor(); - } - - unhover() { - if (this.on) { - return; - } - - this.isHovering = false; - this.refreshColor(); - } - - toggle() { - this.on = !this.on; - this.refreshColor(); - } - - turnOn(showLines) { - this.on = true; - for (let child of this.children) { - child.visible = true; - } - this.refreshColor(); - this.lines.visible = showLines; - }; - - - turnOff(showLines) { - this.on = false; - for (let child of this.children) { - child.visible = false; - } - this.refreshColor(); - this.lines.visible = false; - } - - refreshColor() { - this.material.color.setHex(this.getColor()); - } - - showLines(showLines) { - if (this.on) { - this.lines.visible = showLines; - } - } - - getColor() { - if (this.on) { - return 0xffff00; - } else if (this.isHovering) { - return 0x888800; - } else { - return 0x555500; - } - } -} - -let center, renderer, scene, camera, controls, pointLight, leds, spheres, cameraObject, domElement, raycaster, pointer, selectedObject, ledView, animationStep, beforeAnimationPosition, beforeAnimationTarget, showLinesCheckbox; - -async function init(dataPath, domElementArg = document.body) { - - showLinesCheckbox = document.getElementById('show-lines'); - showLinesCheckbox.addEventListener('change', () => { - for (let led of leds.children) { - led.showLines(showLinesCheckbox.checked); - } - }); - - center = new THREE.Vector3(0, 0, 10); - animationStep = NaN; - beforeAnimationPosition = new THREE.Vector3(); - beforeAnimationTarget = new THREE.Vector3(); - - let request = await fetch('/data/calibration.json'); - let data = await request.json(); - - domElement = domElementArg; - let w = domElement === document.body ? window.innerWidth : domElement.offsetWidth; - let h = domElement === document.body ? window.innerHeight : domElement.offsetHeight; - - raycaster = new THREE.Raycaster(); - raycaster.layers.set(1); - - camera = new THREE.PerspectiveCamera(45, w / h, 0.001, 1000); - camera.position.set(0, 0, -30); - - scene = new THREE.Scene(); - - leds = new THREE.Object3D(); - - for (let ledInfo of data.leds) { - let row = ledInfo.position; - let sphere = new Led(); - sphere.position.x = -row[1]; - sphere.position.y = -row[0]; - sphere.position.z = row[2]; - sphere.name = ledInfo.name; - sphere.layers.enable(1); - sphere.setLines(ledInfo.directions, data.spheres); - leds.add(sphere); - } - - scene.add(leds); - - let spheres = new THREE.Object3D(); - - for (let row of data.spheres) { - const geometry = new THREE.SphereGeometry(1, 32, 16); - const material = new THREE.MeshPhongMaterial({ color: 0xffffff }); - let sphere = new THREE.Mesh(geometry, material); - sphere.position.x = -row[1]; - sphere.position.y = -row[0]; - sphere.position.z = row[2]; - sphere.layers.enable(1); - spheres.add(sphere); - } - - scene.add(spheres); - - cameraObject = new Pyramid(); - scene.add(cameraObject); - - const axesHelper = new THREE.AxesHelper(10); - scene.add(axesHelper); - - pointLight = new THREE.PointLight(0xffffff, 0); - scene.add(pointLight); - - const ambientLight = new THREE.AmbientLight(0xffffff, 0.15); - scene.add(ambientLight); - - renderer = new THREE.WebGLRenderer({antialias: true, alpha: true}); - renderer.setAnimationLoop(animate); - - selectedObject = document.getElementById('selected-object'); - ledView = document.getElementById('led-view'); - - // Add listeners - controls = new OrbitControls(camera, renderer.domElement); - controls.zoomSpeed = 5; - controls.target.copy(center); - controls.update(); - - window.addEventListener('pointermove', onPointerMove); - window.addEventListener('pointerup', onPointerUp); - window.addEventListener('resize', onWindowResize, false); - document.addEventListener('keyup', function(e) { - switch (e.code) { - case "ArrowDown": - case "ArrowRight": - nextLed(); - break; - - case "ArrowUp": - case "ArrowLeft": - previousLed(); - break; - } - - }); - onWindowResize(); - - domElement.appendChild(renderer.domElement); -} - -function animate(time) { - controls.update(); - - if (!isNaN(animationStep)) { - animationStep += 0.025; - if (animationStep > 1) { - controls.enabled = true; - camera.position.set(0, 0, 0); - controls.target.copy(center); - animationStep = NaN; - controls.update(); - } else { - camera.position.set(0, 0, 0); - camera.position.addScaledVector(beforeAnimationPosition, 1 - animationStep); - controls.target.set(0, 0, 0); - controls.target.addScaledVector(beforeAnimationTarget, 1 - animationStep); - controls.target.addScaledVector(center, animationStep); - controls.update(); - } - } - - if (pointer !== undefined) { - raycaster.setFromCamera(pointer, camera); - const intersects = raycaster.intersectObjects(scene.children); - const intersection = intersects.length > 0 ? intersects[0].object : undefined; - - if (intersection && intersection.parent instanceof Pyramid) { - cameraObject.mesh.material.opacity = 1; - cameraObject.lines.material.color.setHex(0xff0000); - } else { - cameraObject.mesh.material.opacity = 0.8; - cameraObject.lines.material.color.setHex(0x990000); - } - - if (intersection && intersection instanceof Led) { - intersection.hover(); - } - - for (let led of leds.children) { - if (led === intersection) { - led.hover(); - } else { - led.unhover(); - } - } - } - - renderer.render(scene, camera); -} - -function onWindowResize() { - let w = domElement === document.body ? window.innerWidth : domElement.offsetWidth; - let h = domElement === document.body ? window.innerHeight : domElement.offsetHeight; - - camera.aspect = w / h; - camera.updateProjectionMatrix(); - - renderer.setSize(w, h); -} - -function onPointerMove(event) { - // calculate pointer position in normalized device coordinates - // (-1 to +1) for both components - - if (pointer === undefined) { - pointer = new THREE.Vector2(); - } - let w = domElement === document.body ? window.innerWidth : domElement.offsetWidth; - let h = domElement === document.body ? window.innerHeight : domElement.offsetHeight; - pointer.x = (event.offsetX / w) * 2 - 1; - pointer.y = - (event.offsetY / h) * 2 + 1; -} - -function onPointerUp(event) { - raycaster.setFromCamera(pointer, camera); - const intersects = raycaster.intersectObjects(scene.children); - if (intersects.length > 0) { - - if (intersects[0].object.parent instanceof Pyramid && intersects[0].distance > 1) { - triggerAnimation(); - return; - } - - if (intersects[0].object instanceof Led) { - selectLed(intersects[0].object); - return; - } - } -} - -function triggerAnimation() { - beforeAnimationPosition.copy(camera.position); - beforeAnimationTarget.copy(controls.target); - animationStep = 0; - controls.enabled = false; -} - -function nextLed() { - for (let index = 0; index < leds.children.length; index++) { - if (leds.children[index].on) { - selectLed(leds.children[(index + 1) % leds.children.length]); - return; - } - } - - selectLed(leds.children[0]); -} - -function previousLed() { - for (let index = 0; index < leds.children.length; index++) { - if (leds.children[index].on) { - selectLed(leds.children[(index - 1 + leds.children.length) % leds.children.length]); - return; - } - } - - selectLed(leds.children[0]); -} - -function selectLed(led) { - for (let child of leds.children) { - if (led === child) { - if (led.on) { - led.turnOff(showLinesCheckbox.checked); - pointLight.intensity = 0; - ledView.style.display = "none"; - selectedObject.innerText = 'aucune'; - } else { - led.turnOn(showLinesCheckbox.checked); - pointLight.intensity = 100; - pointLight.position.copy(led.position); - ledView.src = 'data/small/' + led.name; - ledView.style.display = "block"; - selectedObject.innerText = led.name; - } - } else { - child.turnOff(showLinesCheckbox.checked); - } - } -} - -export { init }; diff --git a/templates/calibration.html b/templates/calibration.html index aeaed64..b785b26 100644 --- a/templates/calibration.html +++ b/templates/calibration.html @@ -55,13 +55,5 @@ {% endblock extracss %} {% block extrajs %} - - + {% endblock extrajs %} diff --git a/ts/Animation.ts b/ts/Animation.ts new file mode 100644 index 0000000..712aa04 --- /dev/null +++ b/ts/Animation.ts @@ -0,0 +1,48 @@ +import * as THREE from 'three'; + +/** + * A camera pose. + */ +export interface Pose { + /** The position of the camera. */ + position: THREE.Vector3; + + /** The point where the camera is looking. */ + target: THREE.Vector3; +} + +/** + * A class to easily manage a camera animation. + */ +export default class Animation { + + /** The beginning of the animation. */ + start: Pose; + + /** The end of the animation. */ + end: Pose; + + /** Moment in the animation, between 0 and 1. */ + t: number; + + /** Initialises a new animation. */ + constructor(start: Pose, end: Pose) { + this.start = { position: start.position, target: start.target }; + this.end = { position: end.position, target: end.target }; + this.t = 0; + } + + /** Updates the animation. */ + update(delay: number): Pose { + this.t += delay; + return { + position: new THREE.Vector3() + .addScaledVector(this.start.position, 1 - this.t) + .addScaledVector(this.end.position, this.t), + target: new THREE.Vector3() + .addScaledVector(this.start.target, 1 - this.t) + .addScaledVector(this.end.target, this.t), + }; + + } +} diff --git a/ts/Calibration.ts b/ts/Calibration.ts new file mode 100644 index 0000000..e7d418c --- /dev/null +++ b/ts/Calibration.ts @@ -0,0 +1,29 @@ +/** + * Alias type for an array of three numbers. + */ +export type Vector3 = [number, number, number]; + +/** + * A led, with its name, its estimated position and the directions of the lights. + */ +export interface Led { + /** The name of the led. */ + name: string; + + /** The estimated position of the led. */ + position: Vector3; + + /** The estimated directions of the light that allowed the estimation of the position of the led. */ + directions: Vector3[]; +} + +/** + * Type for the calibration data. + */ +export interface Calibration { + /** Information about the leds. */ + leds: Led[]; + + /** Position of the spheres. */ + spheres: Vector3[]; +} diff --git a/ts/CameraObject.ts b/ts/CameraObject.ts new file mode 100644 index 0000000..3ab834c --- /dev/null +++ b/ts/CameraObject.ts @@ -0,0 +1,132 @@ +import * as THREE from 'three'; + +/** + * Small pyramid object to represent the camera. + */ +export default class CameraObject extends THREE.Object3D { + + /** Faces of the pyramid. */ + mesh: THREE.Mesh; + + /** Wireframe of the pyramid. */ + lines: THREE.Line; + + /** + * Builds the full pyramid, with mesh and lines. + */ + constructor() { + super(); + + let width = 1; + let height = 1; + let length = 2; + + let vertices = [ + new THREE.Vector3( 0, 0, 0), + new THREE.Vector3( width, height, length), + new THREE.Vector3(-width, height, length), + new THREE.Vector3(-width, -height, length), + new THREE.Vector3( width, -height, length), + ]; + + // Faces + { + let material = new THREE.MeshPhongMaterial({color: 0xffffff}); + material.transparent = true; + material.opacity = 0.8; + + let geometry = new THREE.BufferGeometry(); + let faces = [ + // Sides of the pyramid + [0, 2, 1], + [0, 3, 2], + [0, 4, 3], + [0, 1, 4], + + // Base of the pyramid + [1, 2, 3], + [1, 3, 4], + ]; + + let buffer = new Float32Array(3 * 3 * faces.length); + + for (let faceIndex = 0; faceIndex < faces.length; faceIndex++) { + let face = faces[faceIndex]; + buffer[faceIndex * 3 * 3 + 0] = vertices[face[0]].x; + buffer[faceIndex * 3 * 3 + 1] = vertices[face[0]].y; + buffer[faceIndex * 3 * 3 + 2] = vertices[face[0]].z; + + buffer[faceIndex * 3 * 3 + 3] = vertices[face[1]].x; + buffer[faceIndex * 3 * 3 + 4] = vertices[face[1]].y; + buffer[faceIndex * 3 * 3 + 5] = vertices[face[1]].z; + + buffer[faceIndex * 3 * 3 + 6] = vertices[face[2]].x; + buffer[faceIndex * 3 * 3 + 7] = vertices[face[2]].y; + buffer[faceIndex * 3 * 3 + 8] = vertices[face[2]].z; + } + + + geometry.setAttribute('position', new THREE.BufferAttribute(buffer, 3)); + const mesh = new THREE.Mesh(geometry, material); + mesh.layers.enable(1); + this.mesh = mesh; + this.add(mesh); + } + + // Lines + { + let material = new THREE.LineBasicMaterial({color: 0x990000}); + let geometry = new THREE.BufferGeometry(); + + let lines = [ + [0, 1], + [0, 2], + [0, 3], + [0, 4], + [1, 2], + [2, 3], + [3, 4], + [4, 1], + ]; + + let buffer = new Float32Array(2 * 3 * lines.length); + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + let line = lines[lineIndex]; + buffer[lineIndex * 2 * 3 + 0] = vertices[line[0]].x; + buffer[lineIndex * 2 * 3 + 1] = vertices[line[0]].y; + buffer[lineIndex * 2 * 3 + 2] = vertices[line[0]].z; + + buffer[lineIndex * 2 * 3 + 3] = vertices[line[1]].x; + buffer[lineIndex * 2 * 3 + 4] = vertices[line[1]].y; + buffer[lineIndex * 2 * 3 + 5] = vertices[line[1]].z; + } + + + geometry.setAttribute('position', new THREE.BufferAttribute(buffer, 3)); + const mesh = new THREE.Line(geometry, material); + mesh.layers.enable(1); + this.lines = mesh; + + this.add(mesh); + } + } + + /** + * Changes the style of the model to a nice hovered style. + */ + hover(): void { + this.mesh.material.opacity = 1; + this.lines.material.color.setHex(0xff0000); + } + + /** + * Restores original style. + */ + unHover(): void { + this.mesh.material.opacity = 0.8; + this.lines.material.color.setHex(0x990000); + } + +} + diff --git a/ts/Engine.ts b/ts/Engine.ts new file mode 100644 index 0000000..e7402cf --- /dev/null +++ b/ts/Engine.ts @@ -0,0 +1,314 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; + +import Animation from './Animation'; +import { Calibration } from './Calibration'; +import { Led, Leds } from './Led'; +import CameraObject from './CameraObject'; + +/** + * Retrieves an HTML element from its id, and throw an error if it doens't exist. + */ +function getElementById(id: string): HTMLElement { + let element = document.getElementById(id); + if (element === null) { + throw new Error('No element with id ' + id); + } + return element; +} + +/** + * Retrieves an HTML input element from its id, and throw an error if it doens't exist. + */ +function getInputElementById(id: string): HTMLInputElement { + let element = getElementById(id); + if (! (element instanceof HTMLInputElement)) { + throw new Error('Element with id ' + id + ' is not an input element'); + } + return element; +} + +/** + * Retrieves an HTML image element from its id, and throw an error if it doens't exist. + */ +function getImageElementById(id: string): HTMLImageElement { + let element = getElementById(id); + if (! (element instanceof HTMLImageElement)) { + throw new Error('Element with id ' + id + ' is not an input element'); + } + return element; +} + +/** + * The class that manages the interface for the calibration visualisation. + */ +export class Engine { + /** HTML element on which the renderer will be added. */ + domElement: HTMLElement; + + /** Checkbox indicating whether wants to show the lines from the spheres to the lights. */ + showLinesCheckbox: HTMLInputElement; + + /** HTML span where we will show the name of the current selected led. */ + selectedObject: HTMLElement; + + /** HTML image where we will show the real photo corresponding to the selected led. */ + ledView: HTMLImageElement; + + /** Target point of the camera. */ + center: THREE.Vector3; + + /** Scene containing all the elements to be rendered. */ + scene: THREE.Scene; + + /** Camera from which the scene will be rendered. */ + camera: THREE.PerspectiveCamera; + + /** Object containing the representation of the camera (grey pyramid). */ + cameraObject: CameraObject; + + /** Object containing all the representations of the leds (yellow spheres). */ + leds: Leds; + + /** Object containing all the representations of the spheres (white). */ + spheres: THREE.Object3D; + + /** Axes that will be shown to help the visualisation of the scene. */ + axes: THREE.AxesHelper; + + /** Ambient light to be able to see stuff in the scene. */ + ambientLight: THREE.AmbientLight; + + /** Renderer that will be used to render the scene. */ + renderer: THREE.WebGLRenderer; + + /** Controls to let the user move the camera. */ + controls: OrbitControls; + + /** 2D Vector representing the position of the mouse on the renderer. */ + pointer: THREE.Vector2; + + /** Object that will help us when users will point or click 3D objects. */ + raycaster: THREE.Raycaster; + + /** Object to manage the animation when the user clicks on the camera. */ + animation: Animation | null; + + /** Initialises the engine. */ + static async create(domId: string) { + let domElement = getElementById(domId); + let engine = new Engine(); + engine.domElement = domElement; + engine.initHtml(); + + let request = await fetch('/data/calibration.json'); + let calibration = await request.json(); + engine.initScene(calibration); + engine.initListeners(); + return engine; + } + + /** Returns the available width to perform the redering. */ + get width(): number { + return this.domElement === document.body ? window.innerWidth : this.domElement.offsetWidth; + } + + /** Returns the available height to perform the redering. */ + get height(): number { + return this.domElement === document.body ? window.innerHeight : this.domElement.offsetHeight; + } + + /** + * Initialises the HTML components of the engine. + */ + initHtml(): void { + this.showLinesCheckbox = getInputElementById('show-lines'); + this.selectedObject = getElementById('selected-object'); + this.ledView = getImageElementById('led-view'); + } + + /** + * Initialises the 3D components of the engine. + */ + initScene(calibration: Calibration): void { + this.center = new THREE.Vector3(0, 0, 10); + this.scene = new THREE.Scene(); + + this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 0.001, 1000); + this.camera.position.set(0, 0, -30); + + this.cameraObject = new CameraObject(); + this.scene.add(this.cameraObject); + + this.leds = new Leds(calibration, this.showLinesCheckbox.checked); + this.scene.add(this.leds); + + this.spheres = new THREE.Object3D(); + for (let row of calibration.spheres) { + let sphere = new THREE.Mesh(new THREE.SphereGeometry(1, 32, 16), new THREE.MeshPhongMaterial({ color: 0xffffff })); + sphere.position.set(-row[1], -row[0], row[2]); + sphere.layers.enable(1); + this.spheres.add(sphere); + } + this.scene.add(this.spheres); + + this.axes = new THREE.AxesHelper(10); + this.scene.add(this.axes); + + this.ambientLight = new THREE.AmbientLight(0xffffff, 0.15); + this.scene.add(this.ambientLight); + + this.renderer = new THREE.WebGLRenderer({antialias: true, alpha: true}); + this.renderer.setSize(this.width, this.height); + this.renderer.setAnimationLoop(() => this.animate()); + + this.controls = new OrbitControls(this.camera, this.renderer.domElement); + this.controls.zoomSpeed = 5; + this.controls.target.copy(this.center); + this.controls.update(); + + this.pointer = new THREE.Vector2(); + + this.raycaster = new THREE.Raycaster(); + this.raycaster.layers.set(1); + + this.animation = null; + + this.onWindowResize(); + this.domElement.appendChild(this.renderer.domElement); + } + + /** + * Initialises the event listeners of the engine. + */ + initListeners(): void { + window.addEventListener('resize', () => this.onWindowResize(), false); + window.addEventListener('pointermove', (e) => this.onPointerMove(e)); + window.addEventListener('pointerup', () => this.onPointerUp()); + this.showLinesCheckbox.addEventListener('change', () => this.leds.setShowLines(this.showLinesCheckbox.checked)); + document.addEventListener('keyup', (e) => { + switch (e.code) { + case "ArrowDown": + case "ArrowRight": + this.showImage(this.leds.next()); + break; + + case "ArrowUp": + case "ArrowLeft": + this.showImage(this.leds.previous()); + break; + } + }); + } + + /** Triggers the animation. */ + startAnimation(): void { + this.animation = new Animation({ + position: this.camera.position, + target: this.controls.target, + }, { + position: new THREE.Vector3(), + target: this.center, + }) + } + + /** + * Content of the render loop. + */ + animate(): void { + // Update user controls + this.controls.update(); + + // Manage animation + if (this.animation !== null) { + if (this.animation.t > 1) { + this.animation = null; + this.camera.position.set(0, 0, 0); + this.controls.target.copy(this.center); + } else { + let current = this.animation.update(0.01); + this.camera.position.copy(current.position); + this.controls.target.copy(current.target); + } + + this.controls.update(); + } + + // Manage mouse interaction + this.raycaster.setFromCamera(this.pointer, this.camera); + let intersects = this.raycaster.intersectObjects(this.scene.children); + let firstIntersection = intersects[0]; + + // If the pointer points at the camera, make it hover + if (firstIntersection && firstIntersection.object.parent instanceof CameraObject && firstIntersection.distance > 1) { + this.cameraObject.hover(); + } else { + this.cameraObject.unHover(); + } + + // If the pointer points at a led, make it hover, but unhover other leds first + for (let led of this.leds.children) { + if (led instanceof Led) { + led.unHover(); + } + } + if (firstIntersection && firstIntersection.object instanceof Led) { + firstIntersection.object.hover(); + } + + // Perform the rendering + this.renderer.render(this.scene, this.camera); + } + + /** + * When the pointer moves on the screen. + */ + onPointerMove(e: PointerEvent): void { + // Normalize pointer position in [-1, 1]² + this.pointer.x = (e.offsetX / this.width) * 2 - 1; + this.pointer.y = - (e.offsetY / this.height) * 2 + 1; + } + + /** + * Shows the photo associated to a led. + */ + showImage(led: Led): void { + if (led.on) { + this.selectedObject.innerText = led.name; + this.ledView.src = '/data/small/' + led.name; + this.ledView.style.display = 'block'; + } else { + this.selectedObject.innerText = 'aucune'; + this.ledView.style.display = 'none'; + } + } + + /** + * When the pointer moves is released (i.e. click). + */ + onPointerUp(): void { + this.raycaster.setFromCamera(this.pointer, this.camera); + let intersects = this.raycaster.intersectObjects(this.scene.children); + let firstIntersection = intersects[0]; + + if (firstIntersection && firstIntersection.object instanceof Led) { + this.leds.toggle(firstIntersection.object); + this.showImage(firstIntersection.object); + } + + if (firstIntersection && firstIntersection.object.parent instanceof CameraObject && firstIntersection.distance > 1) { + this.startAnimation(); + return; + } + } + + /** + * When the pointer window is resized. + */ + onWindowResize(): void { + this.camera.aspect = this.width / this.height; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(this.width, this.height); + } +} + diff --git a/ts/Led.ts b/ts/Led.ts new file mode 100644 index 0000000..a857bb9 --- /dev/null +++ b/ts/Led.ts @@ -0,0 +1,248 @@ +import * as THREE from 'three'; +import * as Calibration from './Calibration'; + +/** + * Helper to render leds as spheres. + */ +export class Led extends THREE.Mesh { + + /** Whether the led is on or off. */ + on: boolean; + + /** Whether the mouse is hovering the led or not. */ + isHovered: boolean; + + /** Lines going from the sphere to the led. */ + lines: THREE.LineSegments; + + /** Point light to produce a nice lighting effect when the light is on. */ + light: THREE.PointLight; + + /** + * Creates a new led from its information and the spheres. + */ + constructor(ledInfo: Calibration.Led, spheres: Calibration.Vector3[]) { + super( + new THREE.SphereGeometry(0.7, 32, 16), + new THREE.MeshBasicMaterial({ color: 0x555500 }), + ); + + this.position.set(-ledInfo.position[1], -ledInfo.position[0], ledInfo.position[2]); + this.name = ledInfo.name; + this.layers.enable(1); + this.on = false; + this.isHovered = false; + + const material = new THREE.LineBasicMaterial({ + color: 0x0000ff + }); + + let vertices = new Float32Array(3 * 2 * spheres.length); + + for (let index = 0; index < ledInfo.directions.length; index++) { + let line = ledInfo.directions[index]; + let sphere = spheres[index]; + vertices[3 * 2 * index ] = -this.position.x - sphere[1]; + vertices[3 * 2 * index + 1] = -this.position.y - sphere[0]; + vertices[3 * 2 * index + 2] = -this.position.z + sphere[2]; + vertices[3 * 2 * index + 3] = -line[1] * 100; + vertices[3 * 2 * index + 4] = -line[0] * 100; + vertices[3 * 2 * index + 5] = line[2] * 100; + } + + let geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3)); + this.lines = new THREE.LineSegments(geometry, material); + this.lines.visible = false; + this.add(this.lines); + + this.light = new THREE.PointLight(0xffffff, 100); + this.light.visible = false; + this.add(this.light); + } + + /** + * Changes the style of the model to a nice hovered style. + */ + hover() { + if (this.on) { + return; + } + + this.isHovered = true; + this.refreshColor(); + } + + /** + * Restores original style. + */ + unHover() { + if (this.on) { + return; + } + + this.isHovered = false; + this.refreshColor(); + } + + /** + * Turn on the led if its off, turn it off if its on. + */ + toggle() { + this.on = !this.on; + this.refreshColor(); + } + + /** + * Changes the style of the model to make it visible that the led is on. + */ + turnOn(showLines: boolean) { + this.on = true; + this.light.visible = true; + for (let child of this.children) { + child.visible = true; + } + this.refreshColor(); + this.lines.visible = showLines; + } + + /** + * Changes the style of the model to make it visible that the led is off. + */ + turnOff() { + this.on = false; + this.light.visible = false; + for (let child of this.children) { + child.visible = false; + } + this.refreshColor(); + this.lines.visible = false; + } + + /** + * Refreshes the color according to the led state. + */ + refreshColor() { + this.material.color.setHex(this.getColor()); + } + + /** + * Shows or hides the lines from the spheres following the light direction. + */ + showLines(showLines: boolean) { + if (this.on) { + this.lines.visible = showLines; + } + } + + /** + * Returns the hexadecimal value of the color of the led depending on the state. + */ + getColor() { + if (this.on) { + return 0xffff00; + } else if (this.isHovered) { + return 0x888800; + } else { + return 0x555500; + } + } +} + +/** + * Container for all the leds that will help managing which led is on. + * Only one led can be on at a time. + */ +export class Leds extends THREE.Object3D { + + /** Index of the led that is currently on, null if all leds are off. */ + currentLedIndex: number | null; + + /** Whether we need to show the lines of the leds. */ + showLines: boolean; + + /** + * Create a set of leds from their configuration. + */ + constructor(calibration: Calibration.Calibration, showLines: boolean) { + super(); + + this.showLines = showLines; + this.currentLedIndex = null; + + for (let ledInfo of calibration.leds) { + this.add(new Led(ledInfo, calibration.spheres)); + } + } + + /** + * Turns of the current led if any, and turns on the led given in argument. + * If the led given in argument is the one on, it will be turned off. + */ + toggle(led: Led): void { + // If the specified led is the one on. + if (this.currentLedIndex !== null && led === this.children[this.currentLedIndex]) { + this.currentLedIndex = null; + led.turnOff(); + return; + } + + for (let index = 0; index < this.children.length; index++) { + let child = this.children[index]; + if (led === this.children[index]) { + child.turnOn(this.showLines); + this.currentLedIndex = index; + } else { + child.turnOff(); + } + } + } + + /** + * Changes whether we should show or not show the led lines. + */ + setShowLines(showLines: boolean): void { + this.showLines = showLines; + for (let child of this.children) { + if (child instanceof Led && child.on) { + child.lines.visible = showLines; + } + } + } + + /** + * Turn off the current led and goes to the next one. + */ + next(): Led { + if (this.currentLedIndex === null) { + this.currentLedIndex = 0; + let led = this.children[0]; + led.turnOn(this.showLines); + return led; + } + + ( this.children[this.currentLedIndex]).turnOff(); + this.currentLedIndex = (this.currentLedIndex + 1) % this.children.length; + ( this.children[this.currentLedIndex]).turnOn(this.showLines); + return this.children[this.currentLedIndex]; + } + + /** + * Turn off the current led and goes to the previous one. + */ + previous(): Led { + if (this.currentLedIndex === null) { + this.currentLedIndex = 0; + ( this.children[0]).turnOn(this.showLines); + return this.children[0];; + } + + ( this.children[this.currentLedIndex]).turnOff(); + this.currentLedIndex = (this.currentLedIndex + this.children.length - 1) % this.children.length; + let led = ( this.children[this.currentLedIndex]); + led.turnOn(this.showLines); + return led; + } + + +} diff --git a/ts/main.ts b/ts/main.ts new file mode 100644 index 0000000..e4711a0 --- /dev/null +++ b/ts/main.ts @@ -0,0 +1,2 @@ +import { Engine } from './Engine'; +Engine.create('visualiser'); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8aa57fb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "module": "amd", + "lib": ["dom", "ESnext"], + "removeComments": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noEmit": true + } +} +