ednl-liftstatus-web-components
Version:
The EDNL LiftStatus web components
651 lines (650 loc) • 22.1 kB
JavaScript
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