angular-stl-model-viewer
Version:
Angular component for rendering a STL model
228 lines (222 loc) • 9.83 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, Output, Input, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
/* eslint-disable @typescript-eslint/no-explicit-any */
const defaultMeshOptions = {
castShadow: true,
position: new THREE.Vector3(0, 0, 0),
receiveShadow: true,
scale: new THREE.Vector3(0.03, 0.03, 0.03)
};
const isWebGLAvailable = () => {
try {
const canvas = document.createElement('canvas');
return !!(window.WebGLRenderingContext
&& (canvas.getContext('webgl') || canvas.getContext('experimental-webgl')));
}
catch {
return false;
}
};
class StlModelViewerComponent {
constructor(cdr, eleRef, ngZone) {
this.cdr = cdr;
this.eleRef = eleRef;
this.ngZone = ngZone;
this.stlModels = [];
this.stlModelFiles = [];
this.hasControls = true;
this.camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 15);
this.cameraTarget = new THREE.Vector3(0, 0, 0);
this.light = new THREE.PointLight(0xffffff, 80);
this.material = new THREE.MeshPhongMaterial({
color: 0x999999,
shininess: 400,
specular: 0x222222
});
this.scene = new THREE.Scene();
this.renderer = new THREE.WebGLRenderer({
antialias: true
});
this.controls = null;
this.meshOptions = [];
this.centered = true;
this.rendered = new EventEmitter();
this.hasWebGL = isWebGLAvailable();
this.meshGroup = new THREE.Object3D();
this.isRendered = false;
this.showStlModel = true;
this.stlLoader = new STLLoader();
this.middle = new THREE.Vector3();
this.render = () => {
this.renderer.render(this.scene, this.camera);
};
this.onWindowResize = () => {
this.setSizes();
this.render();
};
this.cdr.detach();
// default light position
this.light.position.set(1, 1, 2);
// default camera position
this.camera.position.set(3, 3, 3);
// default scene background
this.scene.background = new THREE.Color(0xffffff);
// default renderer options
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.shadowMap.enabled = true;
}
ngOnInit() {
if (!this.hasWebGL) {
console.error('stl-model-viewer: Seems like your system does not support webgl.');
return;
}
this.ngZone.runOutsideAngular(() => {
this.init();
});
}
ngOnDestroy() {
window.removeEventListener('resize', this.onWindowResize, false);
this.meshGroup.children.forEach(child => this.meshGroup.remove(child));
this.scene.children.forEach(child => this.scene.remove(child));
this.camera.remove(this.light);
if (this.material) {
this.material.dispose();
}
if (this.controls) {
this.controls.removeEventListener('change', this.render);
this.controls.dispose();
}
if (this.eleRef && this.eleRef.nativeElement.children.length > 1) {
this.eleRef.nativeElement.removeChild(this.renderer.domElement);
}
this.renderer.renderLists.dispose();
this.renderer.domElement.remove();
this.renderer.dispose();
}
async createMesh(path, meshOptions = {}, parse = false) {
let geometry = null;
if (parse) {
geometry = this.stlLoader.parse(path);
}
else {
geometry = await this.stlLoader.loadAsync(path);
}
this.material.shininess = 100;
const mesh = new THREE.Mesh(geometry, this.material);
if (this.centered) {
geometry.computeBoundingBox();
geometry.boundingBox.getCenter(this.middle);
mesh.geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(-this.middle.x, -this.middle.y, -this.middle.z));
}
const vectorOptions = ['position', 'scale', 'up'];
const options = Object.assign({}, defaultMeshOptions, meshOptions);
Object.getOwnPropertyNames(options).forEach((option) => {
if (vectorOptions.indexOf(option) > -1) {
const vector = options[option];
const meshVectorOption = mesh[option];
meshVectorOption.set(vector.x, vector.y, vector.z);
}
else {
mesh[option] = options[option];
}
});
return mesh;
}
setSizes() {
const width = this.eleRef.nativeElement.offsetWidth;
const height = this.eleRef.nativeElement.offsetHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
}
async init() {
this.camera.add(this.light);
this.scene.add(this.camera);
// use default controls
if (this.hasControls) {
if (!this.controls) {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableZoom = true;
this.controls.minDistance = 1;
this.controls.maxDistance = 7;
}
this.controls.addEventListener('change', this.render);
}
window.addEventListener('resize', this.onWindowResize, false);
let meshCreations = [];
if (this.stlModels.length > 0) {
meshCreations = this.stlModels.map((modelPath, index) => this.createMesh(modelPath, this.meshOptions[index]));
}
else {
meshCreations = this.stlModelFiles.map((modelFile, index) => this.createMesh(modelFile, this.meshOptions[index], true));
}
const meshes = await Promise.all(meshCreations);
meshes.map(mesh => this.meshGroup.add(mesh));
this.scene.add(this.meshGroup);
this.eleRef.nativeElement.appendChild(this.renderer.domElement);
this.setSizes();
this.render();
this.ngZone.run(() => {
this.isRendered = true;
this.rendered.emit();
this.cdr.detectChanges();
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.1", ngImport: i0, type: StlModelViewerComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.1", type: StlModelViewerComponent, isStandalone: true, selector: "stl-model-viewer", inputs: { stlModels: "stlModels", stlModelFiles: "stlModelFiles", hasControls: "hasControls", camera: "camera", cameraTarget: "cameraTarget", light: "light", material: "material", scene: "scene", renderer: "renderer", controls: "controls", meshOptions: "meshOptions", centered: "centered" }, outputs: { rendered: "rendered" }, ngImport: i0, template: '', isInline: true, styles: [":host{width:100%;height:100%;display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.1", ngImport: i0, type: StlModelViewerComponent, decorators: [{
type: Component,
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'stl-model-viewer', template: '', styles: [":host{width:100%;height:100%;display:block}\n"] }]
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.NgZone }], propDecorators: { stlModels: [{
type: Input
}], stlModelFiles: [{
type: Input
}], hasControls: [{
type: Input
}], camera: [{
type: Input
}], cameraTarget: [{
type: Input
}], light: [{
type: Input
}], material: [{
type: Input
}], scene: [{
type: Input
}], renderer: [{
type: Input
}], controls: [{
type: Input
}], meshOptions: [{
type: Input
}], centered: [{
type: Input
}], rendered: [{
type: Output
}] } });
class StlModelViewerModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.1", ngImport: i0, type: StlModelViewerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.1", ngImport: i0, type: StlModelViewerModule, imports: [StlModelViewerComponent], exports: [StlModelViewerComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.1", ngImport: i0, type: StlModelViewerModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.1", ngImport: i0, type: StlModelViewerModule, decorators: [{
type: NgModule,
args: [{
exports: [StlModelViewerComponent],
imports: [
StlModelViewerComponent
]
}]
}] });
/*
* Public API Surface of angular-stl-model-viewer
*/
/**
* Generated bundle index. Do not edit.
*/
export { StlModelViewerComponent, StlModelViewerModule };
//# sourceMappingURL=angular-stl-model-viewer.mjs.map