File

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

Description

Model component

Visualizes any model we pass in

tag: no-model

Implements

OnInit AfterViewInit

Metadata

Index

Properties
Methods
Inputs
Outputs
HostListeners

Inputs

annotations
Type : any

Adds annotations to the model (ONLY WHEN MODEL LOADED) heavily relies on placement definition from the model itself

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;
isInUse
Type : string
Default value : 'not_active'

Trigger to enable rotating wheels on model WIP

model
Type : ModelData | any

Adds the model to be used

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;

Outputs

modelDragged
Type : EventEmitter<boolean>

Listener if model is loaded

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
})
modelLoaded
Type : EventEmitter<boolean>

Listener if model is loaded

in js

const model = document.querySelector('no-model');
model.addEventListener('modelLoaded', (res) => {
//could be res.detail, since its event
  console.log(res); //bool
})

HostListeners

window:resize
Arguments : '$event'
window:resize(event: any)

Listener to resize canvas based on parent

Parameters :
Name Optional
event No

Methods

angleFrontWheels
angleFrontWheels()

Toggle to rotate wheels to the side when vehicle not in use

Returns : void
closeAnnotation
closeAnnotation()

Resetting annotation

Returns : void
mouseDrag
mouseDrag(e: MouseEvent)

Event to know if user dragged the model

Parameters :
Name Type Optional
e MouseEvent No
Returns : void
ngAfterViewInit
ngAfterViewInit()

Initiation of the whole model

Returns : void
ngOnChanges
ngOnChanges(changes: SimpleChanges)

Listener for changes when model or annotations are updated

Parameters :
Name Type Optional
changes SimpleChanges No
Returns : void
onModelMouseClick
onModelMouseClick(event: MouseEvent)

Raycaster to know at which object we clicked

Parameters :
Name Type Optional
event MouseEvent No
Returns : void
onModelMouseMove
onModelMouseMove(event: MouseEvent)

Raycaster to know at which object we hover over

Parameters :
Name Type Optional
event MouseEvent No
Returns : void
onResize
onResize(event: any)
Decorators :
@HostListener('window:resize', ['$event'])

Listener to resize canvas based on parent

Parameters :
Name Type Optional
event any No
Returns : void
render
render()

Render trigger to animate the model

Returns : void
setupCamera
setupCamera()

Setting up camera

Returns : void
setupControls
setupControls()

Setting up controls

Returns : void
setupFloor
setupFloor()

Setting up floor

Returns : void
setupLights
setupLights()

Setting up lights

Returns : void
setupModel
setupModel()

Setting up the model

Returns : void
setupRenderer
setupRenderer()

Setting up render

Returns : void
setupScene
setupScene()

Setting up scene

Returns : void

Properties

annoRears
Type : any[]
Default value : []

Holder for all annotation backgrounds

annotation
Type : any
Default value : null

Current active annotation holder

camera
Type : PerspectiveCamera

Three camera

canvas
Type : any

Holder for canvas element

controls
Type : OrbitControls

Three orbit controls

floorGeom
Default value : new PlaneBufferGeometry(1000, 1000, 30)

Floor geometry

floorMat
Type : MeshPhongMaterial

Floor mesh

floorMesh
Type : Mesh<PlaneBufferGeometry | MeshPhongMaterial>

Floor mesh

intersectedObject
Type : any

THREE Raycaster object

intersectedObjectHover
Type : any

THREE Raycaster object

loader
Type : GLTFLoader

Three GLTFLoader

modelContainer
Type : ElementRef
Decorators :
@ViewChild('modelContainer')

Model container reference

modelIsLoaded
Type : boolean
Default value : false

internal holder if model is loaded

mouse
Default value : new Vector2(0, 0)

mouse holder for hovering

object
Type : any

Current model holder

progress
Type : number
Default value : 0

Variable to hold progress percentage in component

projector
Type : Projector

THREE projector

ray
Default value : new Raycaster()

THREE Raycaster

renderCalls
Type : any[]
Default value : []

Holder for render animations

renderer
Type : WebGLRenderer

Three renderer

scene
Type : Scene

Three scene

sprites
Type : any[]
Default value : []

Holder for current annotations

wheels
Type : any[]
Default value : []

Holder for all wheels

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();
    });
  }
}
<div class="model_wrapper">
  <div class="promotion" *ngIf="model?.promoteModel" [innerHtml]="model?.promoteText"></div>
  <div class="progress" *ngIf="progress < 100 && progress > 1">
    <div>
      <span class="mdi mdi-36px mdi-spin mdi-loading"></span>
      {{progress}} %
    </div>
  </div>
  <div class="annotation-wrapper" *ngIf="annotation">
    <div class="annotation">
      <div class="annotation-close" (click)="closeAnnotation()"><span class="mdi mdi-close"></span></div>
      <div class="annotation-title type-{{annotation?.type}}">
        {{annotation?.type}}
      </div>
      <div class="annotation-content">
        <div *ngFor="let item of annotation?.content">
          <h3>
            {{item.title}}
          </h3>
          <div [innerHtml]="item?.text"></div>
        </div>
      </div>
    </div>
  </div>
  <div class="handle" *ngIf="model?.showHandle">
    <div class="info" [innerHtml]="model?.handleText">
    </div>
  </div>
  <canvas #modelContainer></canvas>
</div>

./model.component.scss

:host {
  display: block;
  width: 100%;
  height: 100%;
  --progress-bg: white;
  --progress-color: black;
  --link-color: #343e5f;
  .model_wrapper {
    width: 100%;
    height: 100%;
    position: relative;
    overflow: hidden;
  }

  .promotion {
    position: absolute;
    top: 20px;
    left: 0;
    width: 100%;
    z-index: 2;
    text-align: center;
    color: black;
    ::ng-deep a {
      padding-left: 20px;
      color: var(--link-color);
      text-decoration: none;
      font-weight: bold;
      &:hover {
        text-decoration: underline;
      }
    }
  }

  .handle {
    position: absolute;
    width: 80%;
    bottom: 25px;
    left: 10%;
    right: 0;
    //height: 200px;
    display: flex;
    justify-content: center;
    align-items: flex-end;
    pointer-events: none;
    //perspective-origin: bottom center;
    //transform: perspective(360px) rotateX(45deg);
    &:before {
      content: "";
      position: absolute;
      width: 100%;
      height: 200px;
      top: 0;
      left: 0;
      box-shadow: 0px 4px 0px #fbfbfb;
      border-radius: 0px 0px 999px 999px;
    }
    .info {
      background: white;
      position: absolute;
      bottom: -30px;
      padding: 20px;
      color: #a5a5a5;
    }
  }

  .progress {
    position: absolute;
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    div {
      background: var(--progress-bg);
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;
      padding: 10px 20px;
      border-radius: 5px;
      color: var(--progress-color);
    }
  }
  canvas {
    border-radius: 5px;
  }
  .annotation-wrapper {
    position: absolute;
    z-index: 2;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    display: flex;
    justify-content: center;
    align-items: center;

    .annotation {
      background: white; //rgba(0, 0, 0, 0.8);
      border-radius: 5px;
      padding: 16px;
      color: black; // white;
      width: 50%;
      height: 50%;
      position: relative;
      box-shadow: 0 1px 5px 0 rgb(0 0 0 / 25%);
      .annotation-close {
        position: absolute;
        top: 12px;
        right: 16px;
        cursor: pointer;
      }
      .annotation-title {
        font-size: 25px;
        height: 40px;
        color: black;
        &.type-Error {
          color: #dc3737;
        }
        &.type-Warning {
          color: #ffaa53;
        }
      }
      .annotation-content {
        h3 {
          color: #343e5f;
        }
        overflow-y: auto;
        height: calc(100% - 50px);

        ::ng-deep a {
          color: #343e5f;
          text-decoration: none;
          font-weight: bold;
          &:hover {
            text-decoration: underline;
          }
        }
      }
    }
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""