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