File

projects/components/src/lib/model/model.component.ts

Description

ModelData interface to follow

Index

Properties

Properties

backgroundColor
backgroundColor: any
Type : any

Background of the scene

file
file: string
Type : string

URL of the 3d model

font
font: string
Type : string

URL of the font

handleText
handleText: string
Type : string

Handle description

licensePlateText
licensePlateText: string
Type : string

If we load model and has license plate, this gets used

promoteModel
promoteModel: boolean
Type : boolean

Whether to show promotion for custom model

promoteText
promoteText: string
Type : string

Promotion description for custom model

rotation
rotation: number
Type : number

Rotation of the model

showHandle
showHandle: boolean
Type : boolean

Whether to show handle or not

import {
  AfterViewInit,
  Component,
  ElementRef,
  HostListener,
  Input,
  OnInit,
  ViewChild,
  EventEmitter,
  SimpleChanges,
  Output,
} from '@angular/core';
import {
  Scene,
  Color,
  Fog,
  PerspectiveCamera,
  WebGLRenderer,
  LinearToneMapping,
  PCFShadowMap,
  HemisphereLight,
  DirectionalLight,
  MeshPhongMaterial,
  PlaneBufferGeometry,
  MeshLambertMaterial,
  Mesh,
  DoubleSide,
  sRGBEncoding,
  Raycaster,
  Vector2,
  MeshBasicMaterial,
  SphereGeometry,
  BackSide,
} from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { Projector } from 'three/examples/jsm/renderers/Projector.js';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry';
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader';
import { AssetStatus } from '../Interfaces/AssetStatus';

/**
 * ModelData interface to follow
 */
export interface ModelData {
  /**
   * URL of the 3d model
   */
  file: string;
  /**
   * URL of the font
   */
  font: string;

  /**
   * Rotation of the model
   */
  rotation: number;

  /**
   * Background of the scene
   */
  backgroundColor: any;

  /**
   * Whether to show handle or not
   */
  showHandle: boolean;

  /**
   * Handle description
   */
  handleText: string;

  /**
   * Whether to show promotion for custom model
   */
  promoteModel: boolean;

  /**
   * Promotion description for custom model
   */
  promoteText: string;

  /**
   * If we load model and has license plate, this gets used
   */
  licensePlateText: string;
}

/**
 * Model component
 *
 * Visualizes any model we pass in
 *
 * tag: no-model
 */
@Component({
  selector: 'no-model',
  templateUrl: './model.component.html',
  styleUrls: ['./model.component.scss'],
})
export class ModelComponent implements OnInit, AfterViewInit {
  /**
   * Three scene
   */
  scene!: Scene;
  /**
   * Three camera
   */
  camera!: PerspectiveCamera;
  /**
   * Three renderer
   */
  renderer!: WebGLRenderer;
  /**
   * Three orbit controls
   */
  controls!: OrbitControls;
  /**
   * Three GLTFLoader
   */
  loader!: GLTFLoader;
  /**
   * Floor mesh
   */
  floorMesh!: Mesh<PlaneBufferGeometry, MeshPhongMaterial>;

  /**
   * Adds the model to be used
   * @example
   * ```
   * var modelData = {
   *   file: 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Duck/glTF-Binary/Duck.glb',
   *   rotation: -89,
   *   showHandle:true,
   * }
   * const model = document.querySelector('no-model');
   * model.model = modelData;
   * ```
   */
  @Input() model!: ModelData | any;

  /**
   * Adds annotations to the model (ONLY WHEN MODEL LOADED)
   * heavily relies on placement definition from the model itself
   * @example
   * ```
   * var modelAnnotations = [
   *  {
   *    placement: 'anno_wheel_rear_left',
   *    type: 'Error',
   *    content: [
   *      {
   *        title: 'Wheel just bad',
   *        text: 'bla bla bla',
   *      },
   *    ],
   *  },
   *  {
   *    placement: 'anno_engine_battery',
   *    type: 'Error',
   *    content: [
   *      {
   *        title: 'BatteryVoltage under 12v',
   *        text: 'bla bla bla',
   *      },
   *      {
   *        title: 'BatteryVoltage under 12v',
   *        text: 'bla bla bla',
   *      },
   *      {
   *        title: 'BatteryVoltage under 12v',
   *        text: 'bla bla bla',
   *      },
   *    ],
   *  },
   * ];
   * const model = document.querySelector('no-model');
   * model.annotations = modelAnnotations;
   * ```
   */
  @Input() annotations!: any;

  /**
   * Trigger to enable rotating wheels on model WIP
   */
  @Input() isInUse: string = 'not_active';

  /**
   * Model container reference
   */
  @ViewChild('modelContainer') modelContainer!: ElementRef;

  /**
   * Holder for canvas element
   */
  canvas: any;

  /**
   * Floor geometry
   */
  floorGeom = new PlaneBufferGeometry(1000, 1000, 30);
  /**
   * Floor mesh
   */
  floorMat!: MeshPhongMaterial;
  /**
   * Holder for render animations
   */
  renderCalls: any[] = [];

  /**
   * THREE projector
   */
  projector!: Projector;
  /**
   * mouse holder for hovering
   */
  mouse = new Vector2(0, 0);
  /**
   * THREE Raycaster object
   */
  intersectedObject: any;
  /**
   * THREE Raycaster object
   */
  intersectedObjectHover: any;
  /**
   * THREE Raycaster
   */
  ray = new Raycaster();
  /**
   * Current active annotation holder
   */
  annotation: any = null;
  /**
   * Current model holder
   */
  object: any;

  /**
   * Listener if model is loaded
   *
   * @example
   * in js
   * ```
   * const model = document.querySelector('no-model');
   * model.addEventListener('modelLoaded', (res) => {
      //could be res.detail, since its event
   *   console.log(res); //bool
   * })
   * ```
   *
   */
  @Output() modelLoaded: EventEmitter<boolean> = new EventEmitter<boolean>();

  /**
   * Listener if model is loaded
   *
   * @example
   * in js
   * ```
   * const model = document.querySelector('no-model');
   * model.addEventListener('modelDragged', (res) => {
      //could be res.detail, since its event
   *   console.log(res); //bool -> save to the cookies to never show it again
   * })
   * ```
   *
   */
  @Output() modelDragged: EventEmitter<boolean> = new EventEmitter<boolean>();

  /**
   * internal holder if model is loaded
   */
  modelIsLoaded: boolean = false;
  /**
   * Holder for current annotations
   */
  sprites: any[] = [];

  /**
   * Variable to hold progress percentage in component
   */
  progress: number = 0;

  /**
   * Holder for all wheels
   */
  wheels: any[] = [];
  /**
   * Holder for all annotation backgrounds
   */
  annoRears: any[] = [];

  /**
   * Listener to resize canvas based on parent
   * @param event
   */
  @HostListener('window:resize', ['$event'])
  onResize(event: any) {
    this.camera.aspect =
      this.canvas.parentNode.clientWidth / this.canvas.parentNode.clientHeight;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(
      this.canvas.parentNode.clientWidth,
      this.canvas.parentNode.clientHeight
    );
  }

  /**
   * @ignore
   */
  constructor() {}

  /**
   * @ignore
   */
  ngOnInit(): void {}

  /**
   * Listener for changes when model or annotations are updated
   * @param changes
   */
  ngOnChanges(changes: SimpleChanges) {
    if (changes.model) {
      this.ngAfterViewInit();
    }

    if (changes.annotations) {
      if (changes.annotations.currentValue?.length > 0) {
        if (this.sprites.length > 0) {
          this.sprites.forEach((spriteId) => {
            let object: any = this.scene.getObjectById(spriteId);
            let parent: any = object?.parent;
            if (parent) {
              parent!.remove(object);
            }
          });
          this.sprites = [];
          this.annoRears = [];
        }
        //annotations
        if (this.object) {
          changes.annotations.currentValue.forEach((annotation: any) => {
            const annotationMesh: any = this.scene.getObjectByName(
              annotation.placement
            );
            if (annotationMesh) {
              annotationMesh.userData.annotation = annotation;
              switch (annotation.type) {
                case AssetStatus.Error:
                  annotationMesh!.material.color = new Color(0xd30000);
                  annotationMesh!.material.visible = true;
                  annotationMesh!.material.opacity = 1;
                  annotationMesh!.material.alphaTest = 1;
                  break;
                case AssetStatus.Warning:
                  annotationMesh!.material.color = new Color(0xff6b02);
                  annotationMesh!.material.visible = true;
                  annotationMesh!.material.opacity = 1;
                  annotationMesh!.material.alphaTest = 1;
                  break;
              }

              const annoRearMaterial = new MeshBasicMaterial({
                name: 'depth_' + annotationMesh.name,
                color: annotationMesh.material.color,
                //wireframe: true,
                opacity: 0.3,
                transparent: true,
                alphaTest: 0.03,
                depthTest: false,
                side: BackSide,
              });
              const geometry = new SphereGeometry(
                annotationMesh.geometry.boundingSphere.radius + 1,
                18,
                16
              );
              const annoRear = new Mesh(geometry, annoRearMaterial);

              annoRear.position.copy(annotationMesh.position);
              annoRear.scale.set(
                annotationMesh.scale.x / 10,
                annotationMesh.scale.y / 10,
                annotationMesh.scale.z / 10
              );
              this.object.add(annoRear);
              this.annoRears.push(annoRear);
            }
          });
        }
      } else {
        this.sprites.forEach((spriteId) => {
          let object: any = this.scene.getObjectById(spriteId);
          let parent: any = object?.parent;
          if (parent) {
            parent!.remove(object);
          }
        });
        this.sprites = [];
        this.annoRears.forEach((aR) => {
          this.object.remove(aR);
        });
        this.annoRears = [];
        if (this.object) {
          this.object.traverse((e: any) => {
            if (e.userData.hasOwnProperty('annotation')) {
              const basicMaterial = new MeshBasicMaterial({
                name: e.name,
                color: 0xffffff,
                opacity: 0.0,
                transparent: true,
                alphaTest: 0.0,
                side: DoubleSide,
              });
              e.material = basicMaterial;
              e.originalScale = e.scale;
            }
          });
        }
      }
    }

    if (changes.isInUse) {
      if (changes.isInUse.currentValue === 'not_active') {
        this.angleFrontWheels();
      }
    }
  }

  /**
   * Toggle to rotate wheels to the side when vehicle not in use
   */
  angleFrontWheels() {
    const wheel_left = this.wheels[0];
    const wheel_right = this.wheels[1];
    if (wheel_left) {
      wheel_left.rotation.x = 0;
      wheel_left.rotation.y = -25 * (Math.PI / 180);
    }
    if (wheel_right) {
      wheel_right.rotation.x = 0;
      wheel_right.rotation.y = -25 * (Math.PI / 180);
    }
  }

  /**
   * Initiation of the whole model
   */
  ngAfterViewInit(): void {
    if (!this.modelIsLoaded && this.model && this.modelContainer) {
      this.floorMat = new MeshPhongMaterial({
        color: this.model?.backgroundColor,
        shininess: 0,
        side: DoubleSide,
      });
      this.setupScene();
      this.canvas = this.modelContainer.nativeElement;
      this.setupCamera();
      this.setupRenderer();
      this.setupControls();
      this.setupLights();
      this.setupModel();

      this.projector = new Projector();

      this.canvas.addEventListener(
        'click',
        this.onModelMouseClick.bind(this),
        false
      );

      this.canvas.addEventListener(
        'mousedown',
        this.mouseDrag.bind(this),
        false
      );

      this.canvas.addEventListener(
        'mousemove',
        this.onModelMouseMove.bind(this),
        false
      );

      this.setupFloor();

      this.renderCalls.push(() => {
        if (this.isInUse == 'active') {
          const time = -performance.now() / 1000;

          for (let i = 0; i < this.wheels.length; i++) {
            this.wheels[i].rotation.y = 0;
            this.wheels[i].rotation.x = -(time * Math.PI * 2);
          }
        }
      });
      let counttx = 0,
        countup = true;
      //good luck to the one, who modifies this

      this.renderCalls.push(() => {
        if (countup) {
          counttx += 0.001;

          for (let i = 0; i < this.annoRears.length; i++) {
            this.annoRears[i].scale.x =
              counttx >= 0.2
                ? 0.052
                : this.annoRears[i].scale.x + Math.sin(counttx) / 100;
            this.annoRears[i].scale.y =
              counttx >= 0.2
                ? 0.052
                : this.annoRears[i].scale.y + Math.sin(counttx) / 100;
            this.annoRears[i].scale.z =
              counttx >= 0.2
                ? 0.052
                : this.annoRears[i].scale.z + Math.sin(counttx) / 100;
          }
          if (counttx >= 0.06) countup = false;
        } else {
          counttx -= 0.001;

          for (let i = 0; i < this.annoRears.length; i++) {
            this.annoRears[i].scale.x =
              counttx <= 0
                ? 0.05
                : this.annoRears[i].scale.x - Math.sin(counttx) / 100;
            this.annoRears[i].scale.y =
              counttx <= 0
                ? 0.05
                : this.annoRears[i].scale.y - Math.sin(counttx) / 100;
            this.annoRears[i].scale.z =
              counttx <= 0
                ? 0.05
                : this.annoRears[i].scale.z - Math.sin(counttx) / 100;
          }
          if (counttx <= 0) countup = true;
        }
      });
      this.render();
    }

    /* this.renderCalls.push(() => {
      this.updateMouse();
    }); */
  }

  /**
   * Setting up scene
   */
  setupScene(): void {
    this.scene = new Scene();
    this.scene.background = new Color(this.model?.backgroundColor);
    this.scene.fog = new Fog(this.model?.backgroundColor, 20, 100);
  }

  /**
   * Setting up camera
   */
  setupCamera(): void {
    this.camera = new PerspectiveCamera(
      55,
      this.canvas.parentNode.clientWidth / this.canvas.parentNode.clientHeight,
      0.1,
      800
    );

    this.camera.position.set(0, 5, 5);
  }

  /**
   * Setting up render
   */
  setupRenderer() {
    this.renderer = new WebGLRenderer({
      canvas: this.canvas,
      antialias: true,
    });
    this.renderer.setPixelRatio(window.devicePixelRatio);
    this.renderer.setSize(
      this.canvas.parentNode.clientWidth,
      this.canvas.parentNode.clientHeight
    );
    this.renderer.setClearColor(this.model?.backgroundColor);

    this.renderer.toneMapping = LinearToneMapping;
    this.renderer.toneMappingExposure = Math.pow(0.94, 5.0);
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = PCFShadowMap;
    this.renderer.outputEncoding = sRGBEncoding;

    this.canvas.insertAdjacentHTML('beforeend', this.renderer.domElement);

    this.renderCalls.push(() => {
      this.renderer.render(this.scene, this.camera);
      if (this.scene.getObjectByName('car') && !this.modelIsLoaded) {
        this.modelIsLoaded = true;
        this.modelLoaded.next(this.modelIsLoaded);
        this.angleFrontWheels();
      }
    });
  }

  /**
   * Setting up controls
   */
  setupControls() {
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);

    this.controls.rotateSpeed = 0.3;
    this.controls.zoomSpeed = 0.9;

    this.controls.minDistance = 3;
    this.controls.maxDistance = 5;
    this.controls.enableZoom = false;

    this.controls.minPolarAngle = Math.PI / 2; // radians
    this.controls.maxPolarAngle = Math.PI / 3; // radians

    this.controls.enableDamping = true;
    this.controls.dampingFactor = 0.05;
    this.controls.enablePan = false;

    this.renderCalls.push(() => {
      this.controls.update();
    });
  }

  /**
   * Setting up lights
   */
  setupLights() {
    let lightDirect = new DirectionalLight(0xffffff, 2.4);
    lightDirect.position.set(-3, 5, 8);
    lightDirect.castShadow = true;
    lightDirect.shadow.camera.near = 3;
    lightDirect.shadow.camera.far = 30;
    lightDirect.shadow.mapSize.width = 1024;
    lightDirect.shadow.mapSize.height = 1024;
    this.scene.add(lightDirect);

    let light = new HemisphereLight(0xffffff, 0xbebec5, 1);
    light.position.set(0, 5, 0);
    this.scene.add(light);
  }

  /**
   * Setting up the model
   */
  setupModel() {
    this.loader = new GLTFLoader();
    this.loader.load(
      this.model?.file,
      (gltf: any) => {
        let object = gltf.scene;
        object.traverse((e: any) => {
          if (e.isMesh === true && !e.userData.hasOwnProperty('annotation')) {
            e.castShadow = true;
            e.recieveShadow = true;
          }

          if (e.userData.hasOwnProperty('annotation')) {
            const basicMaterial = new MeshBasicMaterial({
              name: e.name,
              color: 0xffffff,
              opacity: 0.0,
              transparent: true,
              alphaTest: 0.0,
              side: DoubleSide,
            });
            e.material = basicMaterial;
            e.originalScale = e.scale;
          }

          if (
            e.userData.name == 'license_plate_rear' &&
            this.model?.licensePlateText
          ) {
            e.visible = false;

            const loader = new FontLoader();
            loader.load(this.model?.font, (font: any) => {
              var material = new MeshLambertMaterial({
                color: 0x000000,
              });

              var geometry = new TextGeometry(this.model?.licensePlateText, {
                font: font,
                size: 70,
                height: 5,
                bevelEnabled: true,
                bevelThickness: 4,
                bevelSize: 4,
                bevelSegments: 5,
                curveSegments: 12,
              });

              var licensePlate = new Mesh(geometry, material);

              licensePlate.position.set(
                e.position.x,
                e.position.y,
                e.position.z
              );
              licensePlate.scale.set(
                e.scale.x / 100,
                e.scale.y / 100,
                e.scale.z / 100
              );
              licensePlate.rotation.set(
                e.rotation.x + Math.PI / 2,
                e.rotation.y,
                e.rotation.z
              );
              object.children[0].add(licensePlate);
            });
          }
        });
        object.scale.set(1, 1, 1);
        object.position.set(0, -1, 0);
        object.rotation.y = this.model?.rotation; //-Math.PI / 2;
        this.object = object;
        this.wheels = [];

        this.wheels.push(
          object.getObjectByName('wheel_front_left'),
          object.getObjectByName('wheel_front_right'),
          object.getObjectByName('wheel_rear_left'),
          object.getObjectByName('wheel_rear_right')
        );

        this.scene.add(object);
      },
      (xhr) => {
        this.progress = Math.trunc((xhr.loaded / xhr.total) * 100);
      }
    );
  }

  /**
   * Event to know if user dragged the model
   * @param e
   */
  mouseDrag(e: MouseEvent) {
    if (this.model?.showHandle) {
      this.modelDragged.emit(true);
    }
    this.model.showHandle = false;
  }

  /**
   * Raycaster to know at which object we clicked
   * @param event
   */
  onModelMouseClick(event: MouseEvent) {
    const rect = this.renderer.domElement.getBoundingClientRect();
    this.mouse.x =
      ((event.clientX - rect.left) / (rect.right - rect.left)) * 2 - 1;
    this.mouse.y =
      -((event.clientY - rect.top) / (rect.bottom - rect.top)) * 2 + 1;

    this.ray.setFromCamera(this.mouse, this.camera);

    var intersects = this.ray.intersectObjects(this.scene.children, true);
    if (intersects.length > 0) {
      if (intersects[0].object != this.intersectedObject) {
        this.intersectedObject = intersects[0].object;
        if (
          this.intersectedObject.userData.hasOwnProperty('annotation') &&
          typeof this.intersectedObject.userData.annotation === 'object'
        ) {
          this.annotation = this.intersectedObject.userData.annotation;
        }
      }
    } else {
      this.intersectedObject = null;
    }
  }

  /**
   * Raycaster to know at which object we hover over
   * @param event
   */
  onModelMouseMove(event: MouseEvent) {
    const rect = this.renderer.domElement.getBoundingClientRect();
    this.mouse.x =
      ((event.clientX - rect.left) / (rect.right - rect.left)) * 2 - 1;
    this.mouse.y =
      -((event.clientY - rect.top) / (rect.bottom - rect.top)) * 2 + 1;

    this.ray.setFromCamera(this.mouse, this.camera);

    var intersects = this.ray.intersectObjects(this.scene.children, true);
    if (intersects.length > 0) {
      if (intersects[0].object != this.intersectedObjectHover) {
        this.intersectedObjectHover = intersects[0].object;
        if (
          this.intersectedObjectHover.userData.hasOwnProperty('annotation') &&
          typeof this.intersectedObjectHover.userData.annotation === 'object'
        ) {
          this.canvas.style.cursor = 'pointer';
        } else {
          this.canvas.style.cursor = 'grab';
        }
      }
    } else {
      this.canvas.style.cursor = 'grab';
      this.intersectedObjectHover = null;
    }
  }

  /**
   * Setting up floor
   */
  setupFloor() {
    this.floorMesh = new Mesh(this.floorGeom, this.floorMat);
    this.floorMesh.receiveShadow = true;
    this.floorMesh.rotation.x = -Math.PI / 2;
    this.floorMesh.position.y = -1;

    this.scene.add(this.floorMesh);
  }

  /**
   * Resetting annotation
   */
  closeAnnotation() {
    this.annotation = null;
    this.intersectedObject = null;
  }

  /**
   * Render trigger to animate the model
   */
  render() {
    window.requestAnimationFrame(this.render.bind(this));
    this.renderCalls.forEach((callback) => {
      callback();
    });
  }
}

results matching ""

    No results matching ""