|
| 1 | +import { Component, computed, CUSTOM_ELEMENTS_SCHEMA, effect, EventEmitter, Input, Output } from '@angular/core'; |
| 2 | +import { |
| 3 | + injectBeforeRender, |
| 4 | + injectNgtRef, |
| 5 | + injectNgtStore, |
| 6 | + NgtArgs, |
| 7 | + signalStore, |
| 8 | + type NgtCamera, |
| 9 | + type NgtVector3, |
| 10 | +} from 'angular-three'; |
| 11 | +import { OrbitControls } from 'three-stdlib'; |
| 12 | + |
| 13 | +export type NgtsOrbitControlsState = { |
| 14 | + camera?: THREE.Camera; |
| 15 | + domElement?: HTMLElement; |
| 16 | + target?: NgtVector3; |
| 17 | + makeDefault: boolean; |
| 18 | + regress: boolean; |
| 19 | + enableDamping: boolean; |
| 20 | + keyEvents: boolean | HTMLElement; |
| 21 | +}; |
| 22 | + |
| 23 | +declare global { |
| 24 | + interface HTMLElementTagNameMap { |
| 25 | + /** |
| 26 | + * @extends three-stdlib|OrbitControls |
| 27 | + */ |
| 28 | + 'ngts-orbit-controls': OrbitControls & NgtsOrbitControlsState; |
| 29 | + } |
| 30 | +} |
| 31 | + |
| 32 | +@Component({ |
| 33 | + selector: 'ngts-orbit-controls', |
| 34 | + standalone: true, |
| 35 | + template: ` <ngt-primitive *args="args()" ngtCompound [enableDamping]="damping()" /> `, |
| 36 | + imports: [NgtArgs], |
| 37 | + schemas: [CUSTOM_ELEMENTS_SCHEMA], |
| 38 | +}) |
| 39 | +export class NgtsOrbitControls { |
| 40 | + private inputs = signalStore<NgtsOrbitControlsState>({ |
| 41 | + enableDamping: true, |
| 42 | + regress: false, |
| 43 | + makeDefault: false, |
| 44 | + keyEvents: false, |
| 45 | + }); |
| 46 | + |
| 47 | + @Input() controlsRef = injectNgtRef<OrbitControls>(); |
| 48 | + |
| 49 | + @Input() set camera(camera: THREE.Camera) { |
| 50 | + this.inputs.set({ camera }); |
| 51 | + } |
| 52 | + |
| 53 | + @Input() set domElement(domElement: HTMLElement) { |
| 54 | + this.inputs.set({ domElement }); |
| 55 | + } |
| 56 | + |
| 57 | + @Input() set makeDefault(makeDefault: boolean) { |
| 58 | + this.inputs.set({ makeDefault }); |
| 59 | + } |
| 60 | + |
| 61 | + @Input() set regress(regress: boolean) { |
| 62 | + this.inputs.set({ regress }); |
| 63 | + } |
| 64 | + |
| 65 | + @Input() set target(target: THREE.Vector3 | Parameters<THREE.Vector3['set']>) { |
| 66 | + this.inputs.set({ target }); |
| 67 | + } |
| 68 | + |
| 69 | + @Input() set enableDamping(enableDamping: boolean) { |
| 70 | + this.inputs.set({ enableDamping }); |
| 71 | + } |
| 72 | + |
| 73 | + @Input() set keyEvents(keyEvents: boolean) { |
| 74 | + this.inputs.set({ keyEvents }); |
| 75 | + } |
| 76 | + |
| 77 | + @Output() change = new EventEmitter<THREE.Event>(); |
| 78 | + @Output() start = new EventEmitter<THREE.Event>(); |
| 79 | + @Output() end = new EventEmitter<THREE.Event>(); |
| 80 | + |
| 81 | + private store = injectNgtStore(); |
| 82 | + |
| 83 | + readonly args = computed(() => [this.controlsRef.nativeElement]); |
| 84 | + readonly damping = this.inputs.select('enableDamping'); |
| 85 | + |
| 86 | + constructor() { |
| 87 | + injectBeforeRender( |
| 88 | + () => { |
| 89 | + const controls = this.controlsRef.untracked; |
| 90 | + if (controls && controls.enabled) { |
| 91 | + controls.update(); |
| 92 | + } |
| 93 | + }, |
| 94 | + { priority: -1 }, |
| 95 | + ); |
| 96 | + |
| 97 | + this.setControls(); |
| 98 | + this.connectElement(); |
| 99 | + this.makeControlsDefault(); |
| 100 | + this.setEvents(); |
| 101 | + } |
| 102 | + |
| 103 | + private setControls() { |
| 104 | + const camera = this.inputs.select('camera'); |
| 105 | + const defaultCamera = this.store.select('camera'); |
| 106 | + const trigger = computed(() => ({ camera: camera(), defaultCamera: defaultCamera() })); |
| 107 | + |
| 108 | + effect(() => { |
| 109 | + const { camera, defaultCamera } = trigger(); |
| 110 | + const controlsCamera = camera || defaultCamera; |
| 111 | + const controls = this.controlsRef.nativeElement; |
| 112 | + if (!controls || controls.object !== controlsCamera) { |
| 113 | + this.controlsRef.nativeElement = new OrbitControls(controlsCamera as NgtCamera); |
| 114 | + } |
| 115 | + }); |
| 116 | + } |
| 117 | + |
| 118 | + private connectElement() { |
| 119 | + const glDomElement = this.store.select('gl', 'domElement'); |
| 120 | + const domElement = this.inputs.select('domElement'); |
| 121 | + const regress = this.inputs.select('regress'); |
| 122 | + const invalidate = this.store.select('invalidate'); |
| 123 | + const keyEvents = this.inputs.select('keyEvents'); |
| 124 | + |
| 125 | + const trigger = computed(() => { |
| 126 | + const eventsSource = this.store.get('events', 'connected'); |
| 127 | + return { |
| 128 | + keyEvents: keyEvents(), |
| 129 | + controls: this.controlsRef.nativeElement, |
| 130 | + domElement: domElement() || eventsSource || glDomElement(), |
| 131 | + regress: regress(), |
| 132 | + invalidate: invalidate(), |
| 133 | + }; |
| 134 | + }); |
| 135 | + |
| 136 | + effect((onCleanup) => { |
| 137 | + const { domElement, controls, keyEvents } = trigger(); |
| 138 | + if (!controls) return; |
| 139 | + if (keyEvents) { |
| 140 | + controls.connect(keyEvents === true ? domElement : keyEvents); |
| 141 | + } else { |
| 142 | + controls.connect(domElement); |
| 143 | + } |
| 144 | + onCleanup(() => void controls.dispose()); |
| 145 | + }); |
| 146 | + } |
| 147 | + |
| 148 | + private makeControlsDefault() { |
| 149 | + const makeDefault = this.inputs.select('makeDefault'); |
| 150 | + const trigger = computed(() => ({ controls: this.controlsRef.nativeElement, makeDefault: makeDefault() })); |
| 151 | + |
| 152 | + effect((onCleanup) => { |
| 153 | + const { controls, makeDefault } = trigger(); |
| 154 | + if (!controls) return; |
| 155 | + if (makeDefault) { |
| 156 | + const oldControls = this.store.get('controls'); |
| 157 | + this.store.set({ controls }); |
| 158 | + onCleanup(() => void this.store.set({ controls: oldControls })); |
| 159 | + } |
| 160 | + }); |
| 161 | + } |
| 162 | + |
| 163 | + private setEvents() { |
| 164 | + const invalidate = this.store.select('invalidate'); |
| 165 | + const performance = this.store.select('performance'); |
| 166 | + const regress = this.inputs.select('regress'); |
| 167 | + |
| 168 | + const trigger = computed(() => ({ |
| 169 | + invalidate: invalidate(), |
| 170 | + performance: performance(), |
| 171 | + regress: regress(), |
| 172 | + controls: this.controlsRef.nativeElement, |
| 173 | + })); |
| 174 | + effect((onCleanup) => { |
| 175 | + const { controls, invalidate, performance, regress } = trigger(); |
| 176 | + if (!controls) return; |
| 177 | + const changeCallback: (e: THREE.Event) => void = (e) => { |
| 178 | + invalidate(); |
| 179 | + if (regress) performance.regress(); |
| 180 | + if (this.change.observed) this.change.emit(e); |
| 181 | + }; |
| 182 | + |
| 183 | + const startCallback = this.start.observed ? this.start.emit.bind(this.start) : null; |
| 184 | + const endCallback = this.end.observed ? this.end.emit.bind(this.end) : null; |
| 185 | + |
| 186 | + controls.addEventListener('change', changeCallback); |
| 187 | + if (startCallback) controls.addEventListener('start', startCallback); |
| 188 | + if (endCallback) controls.addEventListener('end', endCallback); |
| 189 | + |
| 190 | + onCleanup(() => { |
| 191 | + controls.removeEventListener('change', changeCallback); |
| 192 | + if (startCallback) controls.removeEventListener('start', startCallback); |
| 193 | + if (endCallback) controls.removeEventListener('end', endCallback); |
| 194 | + }); |
| 195 | + }); |
| 196 | + } |
| 197 | +} |
0 commit comments