|
| 1 | +import { |
| 2 | + ChangeDetectionStrategy, |
| 3 | + Component, |
| 4 | + computed, |
| 5 | + CUSTOM_ELEMENTS_SCHEMA, |
| 6 | + effect, |
| 7 | + ElementRef, |
| 8 | + input, |
| 9 | + output, |
| 10 | + Signal, |
| 11 | + viewChild, |
| 12 | +} from '@angular/core'; |
| 13 | +import { |
| 14 | + extend, |
| 15 | + getLocalState, |
| 16 | + injectStore, |
| 17 | + NgtArgs, |
| 18 | + NgtGroup, |
| 19 | + NgtObject3DNode, |
| 20 | + omit, |
| 21 | + pick, |
| 22 | + resolveRef, |
| 23 | +} from 'angular-three'; |
| 24 | +import { Camera, Group, Object3D, Event as ThreeEvent } from 'three'; |
| 25 | +import { TransformControls } from 'three-stdlib'; |
| 26 | + |
| 27 | +interface ControlsProto { |
| 28 | + enabled: boolean; |
| 29 | +} |
| 30 | + |
| 31 | +export type NgtsTransformControlsObject = NgtObject3DNode<TransformControls, typeof TransformControls>; |
| 32 | + |
| 33 | +export type NgtsTransformControlsOptions = Partial<NgtsTransformControlsObject> & |
| 34 | + Partial<NgtGroup> & { |
| 35 | + enabled?: boolean; |
| 36 | + axis?: string | null; |
| 37 | + domElement?: HTMLElement; |
| 38 | + mode?: 'translate' | 'rotate' | 'scale'; |
| 39 | + translationSnap?: number | null; |
| 40 | + rotationSnap?: number | null; |
| 41 | + scaleSnap?: number | null; |
| 42 | + space?: 'world' | 'local'; |
| 43 | + size?: number; |
| 44 | + showX?: boolean; |
| 45 | + showY?: boolean; |
| 46 | + showZ?: boolean; |
| 47 | + camera?: Camera; |
| 48 | + makeDefault?: boolean; |
| 49 | + }; |
| 50 | + |
| 51 | +@Component({ |
| 52 | + selector: 'ngts-transform-controls', |
| 53 | + standalone: true, |
| 54 | + template: ` |
| 55 | + <ngt-primitive *args="[controls()]" [parameters]="controlsOptions()" /> |
| 56 | +
|
| 57 | + <ngt-group #group [parameters]="parameters()"> |
| 58 | + <ng-content /> |
| 59 | + </ngt-group> |
| 60 | + `, |
| 61 | + schemas: [CUSTOM_ELEMENTS_SCHEMA], |
| 62 | + changeDetection: ChangeDetectionStrategy.OnPush, |
| 63 | + imports: [NgtArgs], |
| 64 | +}) |
| 65 | +export class NgtsTransformControls { |
| 66 | + object = input<Object3D | ElementRef<Object3D> | undefined | null>(null); |
| 67 | + options = input({} as Partial<NgtsTransformControlsOptions>); |
| 68 | + parameters = omit(this.options, [ |
| 69 | + 'enabled', |
| 70 | + 'axis', |
| 71 | + 'mode', |
| 72 | + 'translationSnap', |
| 73 | + 'rotationSnap', |
| 74 | + 'scaleSnap', |
| 75 | + 'space', |
| 76 | + 'size', |
| 77 | + 'showX', |
| 78 | + 'showY', |
| 79 | + 'showZ', |
| 80 | + 'domElement', |
| 81 | + 'makeDefault', |
| 82 | + 'camera', |
| 83 | + ]); |
| 84 | + |
| 85 | + protected controlsOptions = pick(this.options, [ |
| 86 | + 'enabled', |
| 87 | + 'axis', |
| 88 | + 'mode', |
| 89 | + 'translationSnap', |
| 90 | + 'rotationSnap', |
| 91 | + 'scaleSnap', |
| 92 | + 'space', |
| 93 | + 'size', |
| 94 | + 'showX', |
| 95 | + 'showY', |
| 96 | + 'showZ', |
| 97 | + ]); |
| 98 | + |
| 99 | + private camera = pick(this.options, 'camera'); |
| 100 | + private domElement = pick(this.options, 'domElement'); |
| 101 | + private makeDefault = pick(this.options, 'makeDefault'); |
| 102 | + |
| 103 | + change = output<ThreeEvent>(); |
| 104 | + mouseDown = output<ThreeEvent>(); |
| 105 | + mouseUp = output<ThreeEvent>(); |
| 106 | + objectChange = output<ThreeEvent>(); |
| 107 | + |
| 108 | + private groupRef = viewChild.required<ElementRef<Group>>('group'); |
| 109 | + |
| 110 | + private store = injectStore(); |
| 111 | + private glDomElement = this.store.select('gl', 'domElement'); |
| 112 | + private defaultCamera = this.store.select('camera'); |
| 113 | + private events = this.store.select('events'); |
| 114 | + private defaultControls = this.store.select('controls') as unknown as Signal<ControlsProto>; |
| 115 | + private invalidate = this.store.select('invalidate'); |
| 116 | + |
| 117 | + controls = computed(() => { |
| 118 | + let camera = this.camera(); |
| 119 | + if (!camera) { |
| 120 | + camera = this.defaultCamera(); |
| 121 | + } |
| 122 | + |
| 123 | + let domElement = this.domElement(); |
| 124 | + if (!domElement) { |
| 125 | + domElement = this.events().connected || this.glDomElement(); |
| 126 | + } |
| 127 | + |
| 128 | + return new TransformControls(camera, domElement); |
| 129 | + }); |
| 130 | + |
| 131 | + constructor() { |
| 132 | + extend({ Group }); |
| 133 | + effect((onCleanup) => { |
| 134 | + const cleanup = this.attachControlsEffect(); |
| 135 | + onCleanup(() => cleanup?.()); |
| 136 | + }); |
| 137 | + effect((onCleanup) => { |
| 138 | + const cleanup = this.disableDefaultControlsEffect(); |
| 139 | + onCleanup(() => cleanup?.()); |
| 140 | + }); |
| 141 | + effect((onCleanup) => { |
| 142 | + const cleanup = this.setupControlsEventsEffect(); |
| 143 | + onCleanup(() => cleanup?.()); |
| 144 | + }); |
| 145 | + effect((onCleanup) => { |
| 146 | + const cleanup = this.updateDefaultControlsEffect(); |
| 147 | + onCleanup(() => cleanup?.()); |
| 148 | + }); |
| 149 | + } |
| 150 | + |
| 151 | + private attachControlsEffect() { |
| 152 | + const group = this.groupRef().nativeElement; |
| 153 | + if (!group) return; |
| 154 | + |
| 155 | + const localState = getLocalState(group); |
| 156 | + if (!localState) return; |
| 157 | + |
| 158 | + const [object, controls] = [resolveRef(this.object()), this.controls(), localState.objects()]; |
| 159 | + if (object) { |
| 160 | + controls.attach(object); |
| 161 | + } else { |
| 162 | + controls.attach(group); |
| 163 | + } |
| 164 | + |
| 165 | + return () => controls.detach(); |
| 166 | + } |
| 167 | + |
| 168 | + private disableDefaultControlsEffect() { |
| 169 | + const defaultControls = this.defaultControls(); |
| 170 | + if (!defaultControls) return; |
| 171 | + |
| 172 | + const controls = this.controls(); |
| 173 | + const callback = (event: any) => { |
| 174 | + defaultControls.enabled = !event.value; |
| 175 | + }; |
| 176 | + |
| 177 | + // @ts-expect-error - three-stdlib types are not up-to-date |
| 178 | + controls.addEventListener('dragging-changed', callback); |
| 179 | + return () => { |
| 180 | + // @ts-expect-error - three-stdlib types are not up-to-date |
| 181 | + controls.removeEventListener('dragging-changed', callback); |
| 182 | + }; |
| 183 | + } |
| 184 | + |
| 185 | + private setupControlsEventsEffect() { |
| 186 | + const [controls, invalidate] = [this.controls(), this.invalidate()]; |
| 187 | + const onChange = (event: ThreeEvent) => { |
| 188 | + invalidate(); |
| 189 | + if (!this.change['destroyed']) { |
| 190 | + this.change.emit(event); |
| 191 | + } |
| 192 | + }; |
| 193 | + const onMouseDown = this.mouseDown.emit.bind(this.mouseDown); |
| 194 | + const onMouseUp = this.mouseUp.emit.bind(this.mouseUp); |
| 195 | + const onObjectChange = this.objectChange.emit.bind(this.objectChange); |
| 196 | + |
| 197 | + // @ts-expect-error - three-stdlib types are not up-to-date |
| 198 | + controls.addEventListener('change', onChange); |
| 199 | + // @ts-expect-error - three-stdlib types are not up-to-date |
| 200 | + controls.addEventListener('mouseDown', onMouseDown); |
| 201 | + // @ts-expect-error - three-stdlib types are not up-to-date |
| 202 | + controls.addEventListener('mouseUp', onMouseUp); |
| 203 | + // @ts-expect-error - three-stdlib types are not up-to-date |
| 204 | + controls.addEventListener('objectChange', onObjectChange); |
| 205 | + |
| 206 | + return () => { |
| 207 | + // @ts-expect-error - three-stdlib types are not up-to-date |
| 208 | + controls.removeEventListener('change', onChange); |
| 209 | + // @ts-expect-error - three-stdlib types are not up-to-date |
| 210 | + controls.removeEventListener('mouseDown', onMouseDown); |
| 211 | + // @ts-expect-error - three-stdlib types are not up-to-date |
| 212 | + controls.removeEventListener('mouseUp', onMouseUp); |
| 213 | + // @ts-expect-error - three-stdlib types are not up-to-date |
| 214 | + controls.removeEventListener('objectChange', onObjectChange); |
| 215 | + }; |
| 216 | + } |
| 217 | + |
| 218 | + private updateDefaultControlsEffect() { |
| 219 | + const [controls, makeDefault] = [this.controls(), this.makeDefault()]; |
| 220 | + if (!makeDefault) return; |
| 221 | + |
| 222 | + const oldControls = this.store.snapshot.controls; |
| 223 | + this.store.update({ controls }); |
| 224 | + return () => this.store.update(() => ({ controls: oldControls })); |
| 225 | + } |
| 226 | +} |
0 commit comments