|
| 1 | +import { Component, contentChildren, Directive, ElementRef, inject, input, signal, TemplateRef } from "@angular/core"; |
| 2 | +import { NgTemplateOutlet } from "@angular/common"; |
| 3 | +import { beforeRender, injectStore } from "angular-three"; |
| 4 | +import { mergeInputs } from 'ngxtension/inject-inputs'; |
| 5 | +import { Object3D, Vector3 } from "three"; |
| 6 | + |
| 7 | +export type NgtsLODLevelOptions = { |
| 8 | + distance: number; |
| 9 | + hysteresis: number; |
| 10 | +} |
| 11 | + |
| 12 | +const defaultLodLevelOptions: NgtsLODLevelOptions = { |
| 13 | + distance: 0, |
| 14 | + hysteresis: 0, |
| 15 | +}; |
| 16 | + |
| 17 | +const _v1 = new Vector3(); |
| 18 | +const _v2 = new Vector3(); |
| 19 | + |
| 20 | +/** |
| 21 | + * Helper directive to capture a template to attach to |
| 22 | + * an NgtsLOD component. |
| 23 | + */ |
| 24 | +@Directive({ |
| 25 | + selector: 'ng-template[lodLevel]' |
| 26 | +}) |
| 27 | +export class NgtsLODLevel { |
| 28 | + lodLevel = input(defaultLodLevelOptions, { transform: mergeInputs(defaultLodLevelOptions) }); |
| 29 | + template = inject(TemplateRef); |
| 30 | +} |
| 31 | + |
| 32 | +/** |
| 33 | + * Angular-native port of THREE.LOD |
| 34 | + * |
| 35 | + * Allows to display an object with several levels of details. |
| 36 | + * |
| 37 | + * The main difference with THREE.LOD is that we use angular-three |
| 38 | + * to add/remove the right object from the scene graph, rather than |
| 39 | + * setting the visible flag on one of the object, but keeping them |
| 40 | + * all in the graph. |
| 41 | + * |
| 42 | + * Usage: |
| 43 | + * |
| 44 | + * ```html |
| 45 | + * <ngt-group lod [maxDistance]="10000"> |
| 46 | + * <ngt-mesh *lodLevel /> |
| 47 | + * <ngt-mesh *lodLevel="{distance: 100, hysteresis: 0.1}" /> |
| 48 | + * <ngt-mesh *lodLevel="{distance: 1000}" /> |
| 49 | + * </ngt-group> |
| 50 | + * ``` |
| 51 | + */ |
| 52 | +@Component({ |
| 53 | + selector: '[lod]', |
| 54 | + template: ` |
| 55 | + <ng-container [ngTemplateOutlet]="level()?.template" /> |
| 56 | + `, |
| 57 | + imports: [NgTemplateOutlet], |
| 58 | +}) |
| 59 | +export class NgtsLODImpl { |
| 60 | + maxDistance = input<number>(); |
| 61 | + |
| 62 | + private store = injectStore(); |
| 63 | + private container = inject(ElementRef); |
| 64 | + |
| 65 | + readonly levels = contentChildren(NgtsLODLevel); |
| 66 | + readonly level = signal<NgtsLODLevel|undefined>(undefined); |
| 67 | + |
| 68 | + constructor() { |
| 69 | + beforeRender(() => { |
| 70 | + |
| 71 | + const levels = this.levels(); |
| 72 | + const currentLevel = this.level(); |
| 73 | + const maxDistance = this.maxDistance(); |
| 74 | + |
| 75 | + let level: NgtsLODLevel|undefined = levels[0]; |
| 76 | + |
| 77 | + if(level && (levels.length > 1 || maxDistance)) { |
| 78 | + |
| 79 | + const container = this.container.nativeElement as Object3D; |
| 80 | + const {matrixWorld, zoom} = this.store.snapshot.camera; |
| 81 | + |
| 82 | + _v1.setFromMatrixPosition( matrixWorld ); |
| 83 | + _v2.setFromMatrixPosition( container.matrixWorld ); |
| 84 | + |
| 85 | + const distance = _v1.distanceTo( _v2 ) / zoom; |
| 86 | + |
| 87 | + if(maxDistance && distance > maxDistance) { |
| 88 | + level = undefined; |
| 89 | + } |
| 90 | + else { |
| 91 | + for (let i = 1, l = levels.length; i < l; i ++ ) { |
| 92 | + const _level = levels[i]; |
| 93 | + let {distance: levelDistance, hysteresis} = _level.lodLevel(); |
| 94 | + |
| 95 | + if (hysteresis && currentLevel === _level) { |
| 96 | + levelDistance -= levelDistance * hysteresis; |
| 97 | + } |
| 98 | + |
| 99 | + if (distance >= levelDistance) { |
| 100 | + level = _level; |
| 101 | + } |
| 102 | + else { |
| 103 | + break; |
| 104 | + } |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + if(level !== currentLevel) { |
| 110 | + this.level.set(level); |
| 111 | + } |
| 112 | + }); |
| 113 | + } |
| 114 | +} |
| 115 | + |
| 116 | +export const NgtsLOD = [NgtsLODImpl, NgtsLODLevel] as const; |
0 commit comments