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
+ }
+}
+