UNPKG

ednl-liftstatus-web-components

Version:
651 lines (650 loc) 22.1 kB
import { Host, h, getAssetPath } from "@stencil/core"; import * as THREE from "three"; import { ColladaLoader } from "three/examples/jsm/loaders/ColladaLoader"; import * as TWEEN from "@tweenjs/tween.js"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; dayjs.extend(relativeTime); import "dayjs/locale/nl-be"; import { getStore } from "../../store"; import { isEmpty } from "lodash-es"; dayjs.locale("nl-be"); export class Ls3dScene { constructor() { this.animateTheFloor = true; this.backDoorLeft = new THREE.Object3D(); this.backDoorRight = new THREE.Object3D(); this.doorWidth = 12.5; this.floorAnimationTween = null; this.floorHeight = 500; this.floorSeparatorIndicator = new THREE.Group(); this.frontDoorLeft = new THREE.Object3D(); this.frontDoorRight = new THREE.Object3D(); this.pulseInterval = null; this.speed = 3000; this.positions = { frontDoor: { left: { open: 0, closed: 0, }, right: { open: 0, closed: 0, }, }, backDoor: { left: { open: 0, closed: 0, }, right: { open: 0, closed: 0, }, }, floor: { down: 0, up: 0, }, }; this.materials = { active: new THREE.MeshStandardMaterial({ color: 0xff0000, opacity: 1 }), normal: new THREE.MeshStandardMaterial({ color: 0xa3a3a3, opacity: 1, transparent: false, }), pulseOn: new THREE.MeshStandardMaterial({ color: 0x024970, opacity: 0.5, transparent: true, }), pulseOff: new THREE.MeshStandardMaterial({ color: 0x024970, opacity: 1, transparent: false, }), transparent: new THREE.MeshStandardMaterial({ color: 0xffffff, opacity: 0, transparent: true, }), floor: new THREE.MeshStandardMaterial({ color: 0x85b19b, opacity: 0.15, transparent: true, }), door: new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0x0, roughness: 0, metalness: 1, }), }; this.initializeScene = () => { // Setup the camera this.camera = new THREE.PerspectiveCamera(45, 500 / 500, 1, 1000); this.camera.position.set(7, 4, 3.5); this.camera.zoom = 1.7; this.camera.updateProjectionMatrix(); this.camera.lookAt(0, 0, 0); // Create the scene this.scene = new THREE.Scene(); // Initialize the loading manager const loadingManager = new THREE.LoadingManager(() => { // If 3D scene file failed to load if (this.elevator === undefined) { const error = document.createElement("div"); error.innerHTML = "Failed to load 3D scene file"; error.classList.add("failed-to-load-error"); // On reload: replace the current scene, instead of creating a new one if (this.canvas.children.length > 2) { this.canvas.replaceChild(error, this.canvas.children[2]); } else { this.canvas.appendChild(error); } } else { this.scene.add(this.elevator); } }); // Load the elevator.dae file and initialize the objects const loader = new ColladaLoader(loadingManager); loader.load(getAssetPath("./assets/elevator-001.dae"), (collada) => { this.elevator = collada.scene; this.arrowUp = this.elevator.getObjectByName("arrow_up"); this.arrowDown = this.elevator.getObjectByName("arrow_down"); this.elevatorBackWall = this.elevator.getObjectByName("elevator_back_wall"); this.elevatorBackWallBeam = this.elevator.getObjectByName("achter_wand_beam"); this.frontDoorLeft = this.elevator.getObjectByName("front_door_left"); this.frontDoorRight = this.elevator.getObjectByName("front_door_right"); this.backDoor = this.elevator.getObjectByName("back_door"); this.backDoorLeft = this.elevator.getObjectByName("back_door_left"); this.backDoorRight = this.elevator.getObjectByName("back_door_right"); this.floorSeparatorIndicator = this.elevator.getObjectByName("floor_separator_indicator"); // Hide outlines this.elevator.children[0].traverse((childNode) => { if (childNode.type === "LineSegments") { childNode.visible = false; } }); // Change the material for the arrows for (let i = 0; i < this.arrowDown.children.length; i++) { this.changeMaterial(this.arrowDown.children[i], this.materials.pulseOff); this.changeMaterial(this.arrowUp.children[i], this.materials.pulseOff); } // Hide arrows this.stopMoving(); // Assign positions this.positions = { frontDoor: { left: { open: this.frontDoorLeft.position.y + this.doorWidth, closed: this.frontDoorLeft.position.y, }, right: { open: this.frontDoorRight.position.y - this.doorWidth, closed: this.frontDoorRight.position.y, }, }, backDoor: { left: { open: this.backDoorLeft.position.y + this.doorWidth, closed: this.backDoorLeft.position.y, }, right: { open: this.backDoorRight.position.y - this.doorWidth, closed: this.backDoorRight.position.y, }, }, floor: { down: this.floorSeparatorIndicator.position.z, up: this.floorSeparatorIndicator.position.z + this.floorHeight, }, }; }); // Setup lighting const ambientLight = new THREE.AmbientLight(0xcccccc, 1); this.scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xeeeeee, 2); directionalLight.position.set(1, 1, 0).normalize(); this.scene.add(directionalLight); // Setup renderer this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, }); this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setSize(500, 500); this.renderer.setClearColor(0xffffff, 0.5); // On reload: replace the current scene, instead of creating a new one if (this.canvas.children.length > 3) { this.canvas.replaceChild(this.renderer.domElement, this.canvas.children[3]); } else { this.canvas.appendChild(this.renderer.domElement); } }; /* * Functions */ this.animateScene = () => { requestAnimationFrame(this.animateScene); TWEEN.update(); this.renderScene(); }; this.renderScene = () => { this.renderer.render(this.scene, this.camera); }; this.changeMaterial = (node, material) => { if (node && typeof node.traverse === "function") { node.traverse((childNode) => { if (childNode.type === "LineSegments") { childNode.visible = false; } else { childNode.material = material; } }); } }; this.openFrontDoor = (skipAnimation = false) => { if (this.frontDoorLeft.position.y === this.positions.frontDoor.left.closed) { if (skipAnimation) { this.frontDoorLeft.position.y = this.positions.frontDoor.left.open; this.frontDoorLeft.position.y = this.positions.frontDoor.right.open; } new TWEEN.Tween({ leftDoor: this.frontDoorLeft.position.y, rightDoor: this.frontDoorRight.position.y, }) .to({ leftDoor: this.positions.frontDoor.left.open, rightDoor: this.positions.frontDoor.right.open, }, this.speed) .easing(TWEEN.Easing.Cubic.InOut) .onUpdate(({ leftDoor, rightDoor }) => { this.frontDoorLeft.position.y = leftDoor; this.frontDoorRight.position.y = rightDoor; }) .start(); } }; this.closeFrontDoor = (skipAnimation = false) => { if (this.frontDoorLeft.position.y === this.positions.frontDoor.left.open) { if (skipAnimation) { this.frontDoorLeft.position.y = this.positions.frontDoor.left.closed; this.frontDoorLeft.position.y = this.positions.frontDoor.right.closed; } new TWEEN.Tween({ leftDoor: this.frontDoorLeft.position.y, rightDoor: this.frontDoorRight.position.y, }) .to({ leftDoor: this.positions.frontDoor.left.closed, rightDoor: this.positions.frontDoor.right.closed, }, this.speed) .easing(TWEEN.Easing.Cubic.InOut) .onUpdate(({ leftDoor, rightDoor }) => { this.frontDoorLeft.position.y = leftDoor; this.frontDoorRight.position.y = rightDoor; }) .start(); } }; this.openBackDoor = (skipAnimation = false) => { if (this.backDoorLeft.position.y === this.positions.backDoor.left.closed) { if (skipAnimation) { this.backDoorLeft.position.y = this.positions.backDoor.left.open; this.backDoorLeft.position.y = this.positions.backDoor.right.open; } new TWEEN.Tween({ leftDoor: this.backDoorLeft.position.y, rightDoor: this.backDoorRight.position.y, }) .to({ leftDoor: this.positions.backDoor.left.open, rightDoor: this.positions.backDoor.right.open, }, this.speed) .easing(TWEEN.Easing.Cubic.InOut) .onUpdate(({ leftDoor, rightDoor }) => { this.backDoorLeft.position.y = leftDoor; this.backDoorRight.position.y = rightDoor; }) .start(); } }; this.closeBackDoor = (skipAnimation = false) => { if (this.backDoorLeft.position.y === this.positions.backDoor.left.open) { if (skipAnimation) { this.backDoorLeft.position.y = this.positions.backDoor.left.closed; this.backDoorLeft.position.y = this.positions.backDoor.right.closed; } new TWEEN.Tween({ leftDoor: this.backDoorLeft.position.y, rightDoor: this.backDoorRight.position.y, }) .to({ leftDoor: this.positions.backDoor.left.closed, rightDoor: this.positions.backDoor.right.closed, }, this.speed) .easing(TWEEN.Easing.Cubic.InOut) .onUpdate(({ leftDoor, rightDoor }) => { this.backDoorLeft.position.y = leftDoor; this.backDoorRight.position.y = rightDoor; }) .start(); } }; this.startPulseArrowAnimation = (arrow) => { if (arrow) { let counter = 0; this.pulseInterval = setInterval(() => { const selectedIndex = counter % 10; for (let i = 0; i <= arrow.children.length; i++) { if (i === selectedIndex) { this.changeMaterial(arrow.children[i], this.materials.pulseOn); } else { this.changeMaterial(arrow.children[i], this.materials.pulseOff); } } counter++; }, 180); } }; this.stopPulseArrowAnimation = () => { var _a, _b; if (this.pulseInterval !== null) { clearInterval(this.pulseInterval); this.pulseInterval = null; } if ((_b = (_a = this.arrowDown) === null || _a === void 0 ? void 0 : _a.children) === null || _b === void 0 ? void 0 : _b.length) { for (let i = 0; i < this.arrowDown.children.length; i++) { this.changeMaterial(this.arrowDown.children[i], this.materials.pulseOff); this.changeMaterial(this.arrowUp.children[i], this.materials.pulseOff); } } }; this.moveUp = () => { this.stopPulseArrowAnimation(); this.hideNode(this.arrowDown); this.showNode(this.arrowUp); this.startPulseArrowAnimation(this.arrowUp); this.startAnimateFloorsIndicator("down"); this.movingDirection = "down"; }; this.moveDown = () => { this.stopPulseArrowAnimation(); this.hideNode(this.arrowUp); this.showNode(this.arrowDown); this.startPulseArrowAnimation(this.arrowDown); this.startAnimateFloorsIndicator("up"); this.movingDirection = "up"; }; this.stopMoving = () => { this.hideNode(this.arrowUp); this.hideNode(this.arrowDown); this.stopPulseArrowAnimation(); this.stopAnimateFloorsIndicator(this.movingDirection); }; this.startAnimateFloorsIndicator = (direction) => { this.animateTheFloor = true; this.animateFloorsIndicator(direction); }; this.stopAnimateFloorsIndicator = (elevatorDirection) => { if (this.floorAnimationTween !== null) { this.animateTheFloor = false; this.floorAnimationTween.stop(); let easingAmount = 50; if (elevatorDirection === "down") { easingAmount = -easingAmount; } // Easy out the floor animation new TWEEN.Tween({ topPos: this.floorSeparatorIndicator.position.z, }) .to({ topPos: this.floorSeparatorIndicator.position.z + easingAmount, }) .easing(TWEEN.Easing.Cubic.Out) .onUpdate(({ topPos }) => { this.floorSeparatorIndicator.position.z = topPos; }) .start(); this.floorAnimationTween = null; } }; this.animateFloorsIndicator = (elevatorDirection) => { const continuous = this.animateTheFloor; let startPos; let endPos; let movementSpeed = this.floorHeight * 10; if (elevatorDirection === "up") { if (this.floorSeparatorIndicator.position.z >= this.positions.floor.up) { startPos = this.positions.floor.down; } // If the floor separator indicator is in view, continue from there else { startPos = this.floorSeparatorIndicator.position.z; // Adjust speed based on distance movementSpeed = Math.abs(this.floorSeparatorIndicator.position.z - this.positions.floor.up) * 10; } endPos = this.positions.floor.up; } else if (elevatorDirection === "down") { if (this.floorSeparatorIndicator.position.z <= this.positions.floor.down) { startPos = this.positions.floor.up; } // If the floor separator indicator is in view, continue from there else { startPos = this.floorSeparatorIndicator.position.z; // Adjust speed based on distance movementSpeed = (this.floorSeparatorIndicator.position.z - this.positions.floor.down) * 10; } endPos = this.positions.floor.down; } if (this.floorAnimationTween !== null) { this.floorAnimationTween.stop(); } const tween = new TWEEN.Tween({ topPos: startPos, }) .to({ topPos: endPos, }, movementSpeed) .onUpdate(({ topPos }) => { this.floorSeparatorIndicator.position.z = topPos; }) .onComplete(() => { // Restart the animation for continuous effect if (continuous === true) { this.animateFloorsIndicator(elevatorDirection); } }) .start(); this.floorAnimationTween = tween; }; this.hideNode = (node) => { if (node !== undefined) { node.traverse((childNode) => { childNode.visible = false; }); } }; this.showNode = (node) => { if (node !== undefined) { node.traverse((childNode) => { if (childNode.type === "LineSegments") { childNode.visible = false; } else { childNode.visible = true; } }); } }; this.processSensorUpdate = (sensorData) => { let sensorValues = sensorData.values; let lastUpdate = 0; let sensors = [500, 501, 502, 503, 514, 515]; sensors.forEach((value) => { lastUpdate = dayjs(sensorData.updated[value]).isAfter(dayjs(lastUpdate)) ? sensorData.updated[value] : lastUpdate; }); this.lastUpdate = lastUpdate; for (let sensorId in sensorValues) { // Elevator state (movement) switch (sensorId) { case "500": if (sensorValues[sensorId] === "D") { if (this.movementState !== "D") { this.moveDown(); } } else if (sensorValues[sensorId] === "U") { if (this.movementState !== "U") { this.moveUp(); } } else { this.stopMoving(); } this.movementState = sensorValues[sensorId]; break; // Current stop place case "515": this.currentStop = sensorValues[sensorId]; break; // Front door case "502": if (sensorValues[sensorId] === "C" || sensorValues[sensorId] === "-") { this.closeFrontDoor(false); } else if (sensorValues[sensorId] === "O" || sensorValues[sensorId] === "+") { this.openFrontDoor(false); } else { this.closeFrontDoor(false); } break; // Rear door case "503": if (sensorValues[sensorId] === "C" || sensorValues[sensorId] === "-") { this.closeBackDoor(false); } else if (sensorValues[sensorId] === "O" || sensorValues[sensorId] === "+") { this.openBackDoor(false); } else { this.closeBackDoor(false); } break; // Last passed case "514": this.lastPassedStop = sensorValues[sensorId]; break; } } }; this.hasBackDoor = (hasBackDoor) => { if (hasBackDoor) return; // Wait until the scene is loaded if (this.backDoor === undefined || this.elevatorBackWall === undefined || this.elevatorBackWallBeam === undefined) { setTimeout(() => { this.hasBackDoor(hasBackDoor); }, 100); return; } this.hideNode(this.backDoor); this.showNode(this.elevatorBackWall); this.showNode(this.elevatorBackWallBeam); }; this.handleError = (error) => { if (error.type === "Socket" || error.type === "Token") { let errorMessageElement = document.createElement("div"); if (error.type === "Socket") { errorMessageElement.append("Geen verbinding"); errorMessageElement.classList.add("error-message"); console.log(error.message); } else { errorMessageElement.append(error.message); errorMessageElement.classList.add("error-message"); } this.canvas.append(errorMessageElement); for (const key in this.canvas.children) { if (this.canvas.children[key].className !== "error-message" && this.canvas.children[key].classList !== undefined) { this.canvas.children[key].classList.add("blur"); } } this.clearTimer(); } }; this.movementState = undefined; this.currentStop = undefined; this.lastPassedStop = undefined; this.lastUpdate = undefined; this.idKey = undefined; } setTimer() { // The timer is used to update the relative timestamps this.timer = window.setInterval(() => { this.processSensorUpdate(this.store.state.currentSensorData); }, 20000); } clearTimer() { window.clearInterval(this.timer); this.timer = null; } connectedCallback() { this.clearTimer(); this.setTimer(); } disconnectedCallback() { this.scene.dispose(); this.clearTimer(); } async componentWillLoad() { this.store = await getStore(this.idKey); } componentDidLoad() { this.initializeScene(); this.animateScene(); this.hasBackDoor(this.store.state.hasBackDoor); if (!isEmpty(this.store.state.error)) { this.handleError(this.store.state.error); } this.store.onChange("currentSensorData", (sensorData) => { this.processSensorUpdate(sensorData); }); } componentDidUpdate() { this.animateScene(); } render() { return (h(Host, null, h("div", { class: "canvas", ref: (el) => { this.canvas = el; } }, h("ls-indicator", { class: "current-floor", "sensor-id": "515", "id-key": this.idKey }), h("ls-indicator", { class: "last-passed-floor", "sensor-id": "514", "id-key": this.idKey }), h("div", { class: "last-update" }, dayjs(this.lastUpdate).fromNow())), h("slot", null))); } static get is() { return "ls-3d-scene"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["ls-3d-scene.css"] }; } static get styleUrls() { return { "$": ["ls-3d-scene.css"] }; } static get assetsDirs() { return ["assets"]; } static get properties() { return { "idKey": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The unique key that is used to identify store data." }, "attribute": "id-key", "reflect": false } }; } static get states() { return { "movementState": {}, "currentStop": {}, "lastPassedStop": {}, "lastUpdate": {} }; } } //# sourceMappingURL=ls-3d-scene.js.map