projects/components/src/lib/model/model.component.ts
Model component
Visualizes any model we pass in
tag: no-model
| selector | no-model |
| styleUrls | ./model.component.scss |
| templateUrl | ./model.component.html |
Properties |
Methods |
Inputs |
Outputs |
HostListeners |
| annotations | |
Type : any
|
|
|
Adds annotations to the model (ONLY WHEN MODEL LOADED) heavily relies on placement definition from the model itself |
|
| 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 |
|
| window:resize | ||||
Arguments : '$event'
|
||||
window:resize(event: any)
|
||||
|
Listener to resize canvas based on parent
Parameters :
|
| 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 :
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 :
Returns :
void
|
| onModelMouseClick | ||||||
onModelMouseClick(event: MouseEvent)
|
||||||
|
Raycaster to know at which object we clicked
Parameters :
Returns :
void
|
| onModelMouseMove | ||||||
onModelMouseMove(event: MouseEvent)
|
||||||
|
Raycaster to know at which object we hover over
Parameters :
Returns :
void
|
| onResize | ||||||
onResize(event: any)
|
||||||
Decorators :
@HostListener('window:resize', ['$event'])
|
||||||
|
Listener to resize canvas based on parent
Parameters :
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
|
| 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;
}
}
}
}
}
}