From 0e33191be0b3fdc4a9c7e067ee42eba467583903 Mon Sep 17 00:00:00 2001 From: Chau Tran Date: Mon, 3 Jan 2022 21:40:47 -0600 Subject: [PATCH] feat(soba): add TransformControls --- packages/soba/controls/src/index.ts | 1 + .../transform-controls.component.ts | 210 ++++++++++++++++++ .../transform-controls.stories.ts | 54 +++++ 3 files changed, 265 insertions(+) create mode 100644 packages/soba/controls/src/lib/transform-controls/transform-controls.component.ts create mode 100644 packages/soba/controls/src/lib/transform-controls/transform-controls.stories.ts diff --git a/packages/soba/controls/src/index.ts b/packages/soba/controls/src/index.ts index b30380141..50a744910 100644 --- a/packages/soba/controls/src/index.ts +++ b/packages/soba/controls/src/index.ts @@ -1,3 +1,4 @@ export * from './lib/orbit-controls/orbit-controls.directive'; export * from './lib/fly-controls/fly-controls.directive'; export * from './lib/first-person-controls/first-person-controls.directive'; +export * from './lib/transform-controls/transform-controls.component'; diff --git a/packages/soba/controls/src/lib/transform-controls/transform-controls.component.ts b/packages/soba/controls/src/lib/transform-controls/transform-controls.component.ts new file mode 100644 index 000000000..7e6965ad5 --- /dev/null +++ b/packages/soba/controls/src/lib/transform-controls/transform-controls.component.ts @@ -0,0 +1,210 @@ +import { + EnhancedRxState, + NGT_OBJECT_INPUTS_CONTROLLER_PROVIDER, + NGT_OBJECT_INPUTS_WATCHED_CONTROLLER, + NgtLoopService, + NgtObject3dInputsController, + NgtStore, +} from '@angular-three/core'; +import { NgtGroupModule } from '@angular-three/core/group'; +import { NgtPrimitiveModule } from '@angular-three/core/primitive'; +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + ContentChildren, + EventEmitter, + Inject, + Input, + NgModule, + NgZone, + OnInit, + Output, + QueryList, +} from '@angular/core'; +import { combineLatest, map, merge } from 'rxjs'; +import * as THREE from 'three'; +import { TransformControls } from 'three-stdlib'; + +type ControlsProto = { + enabled: boolean; +}; + +interface NgtSobaTransformControlsState { + controls: TransformControls; + enabled: boolean; + object: THREE.Object3D; + group: THREE.Group; + camera: THREE.Camera | null; +} + +@Component({ + selector: 'ngt-soba-transform-controls', + template: ` + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [NGT_OBJECT_INPUTS_CONTROLLER_PROVIDER], +}) +export class NgtSobaTransformControls + extends EnhancedRxState + implements OnInit +{ + @Input() set enabled(enabled: boolean) { + this.set({ enabled }); + } + + @Input() set object(object: THREE.Object3D) { + this.set({ object }); + } + + @Input() set camera(camera: THREE.Camera) { + this.set({ camera }); + } + + readonly #attach$ = combineLatest([ + this.select('controls'), + merge(this.select('object'), this.select('group')), + ]); + + @Output() ready = this.#attach$; + @Output() change = new EventEmitter(); + @Output() mousedown = new EventEmitter(); + @Output() mouseup = new EventEmitter(); + @Output() objectChange = new EventEmitter(); + + readonly #initControls$ = combineLatest([ + this.store.select('ready'), + this.select('camera'), + ]).pipe(map(([ready, camera]) => ({ ready, camera }))); + + readonly #draggingChanged$ = combineLatest([ + this.store.select('controls'), + this.select('controls'), + ]).pipe( + map(([defaultControls, controls]) => ({ + defaultControls: defaultControls as unknown as ControlsProto, + controls, + })) + ); + + @ContentChildren(NgtObject3dInputsController, { descendants: true }) set test( + controllers: QueryList + ) { + controllers.forEach((controller) => { + controller.appendTo = () => this.group; + }); + } + + constructor( + private store: NgtStore, + private loopService: NgtLoopService, + private ngZone: NgZone, + @Inject(NGT_OBJECT_INPUTS_WATCHED_CONTROLLER) + public objectInputsController: NgtObject3dInputsController + ) { + super(); + this.set({ + enabled: true, + camera: null, + }); + } + + get object() { + return this.get('object'); + } + + get group() { + return this.get('group'); + } + + get controls() { + return this.get('controls'); + } + + ngOnInit() { + this.hold(this.#attach$, ([controls, object]) => { + controls.attach(object); + }); + + this.hold(this.#initControls$, ({ camera, ready }) => { + if (ready) { + this.ngZone.runOutsideAngular(() => { + const controlsCamera: THREE.Camera = + camera || this.store.get('camera'); + this.set({ + controls: new TransformControls( + controlsCamera, + this.store.get('renderer').domElement + ), + }); + }); + } + }); + + this.holdEffect(this.#draggingChanged$, ({ defaultControls, controls }) => { + return this.ngZone.runOutsideAngular(() => { + if (defaultControls) { + const callback = (event: THREE.Event) => + (defaultControls.enabled = !event.value); + controls.addEventListener('dragging-changed', callback); + return () => + controls.removeEventListener('dragging-changed', callback); + } + return; + }); + }); + + this.holdEffect(this.select('controls'), (controls) => { + return this.ngZone.runOutsideAngular(() => { + const callback = (e: THREE.Event) => { + this.loopService.invalidate(); + if (this.change.observed) this.change.emit(e); + }; + + controls.addEventListener('change', callback); + + const onMouseDown: ((event: THREE.Event) => void) | undefined = this + .mousedown.observed + ? this.mousedown.emit.bind(this.mousedown) + : undefined; + const onMouseUp: ((event: THREE.Event) => void) | undefined = this + .mouseup.observed + ? this.mouseup.emit.bind(this.mouseup) + : undefined; + const onObjectChange: ((event: THREE.Event) => void) | undefined = this + .objectChange.observed + ? this.objectChange.emit.bind(this.objectChange) + : undefined; + + if (onMouseDown) controls.addEventListener('mouseDown', onMouseDown); + if (onMouseUp) controls.addEventListener('mouseUp', onMouseUp); + if (onObjectChange) + controls.addEventListener('objectChange', onObjectChange); + + return () => { + controls.removeEventListener('change', callback); + if (onMouseDown) + controls.removeEventListener('mouseDown', onMouseDown); + if (onMouseUp) controls.removeEventListener('mouseUp', onMouseUp); + if (onObjectChange) + controls.removeEventListener('objectChange', onObjectChange); + }; + }); + }); + } +} + +@NgModule({ + declarations: [NgtSobaTransformControls], + exports: [NgtSobaTransformControls], + imports: [NgtGroupModule, NgtPrimitiveModule, CommonModule], +}) +export class NgtSobaTransformControlsModule {} diff --git a/packages/soba/controls/src/lib/transform-controls/transform-controls.stories.ts b/packages/soba/controls/src/lib/transform-controls/transform-controls.stories.ts new file mode 100644 index 000000000..7c2198401 --- /dev/null +++ b/packages/soba/controls/src/lib/transform-controls/transform-controls.stories.ts @@ -0,0 +1,54 @@ +import { NgtMeshBasicMaterialModule } from '@angular-three/core/materials'; +import { NgtSobaBoxModule } from '@angular-three/soba/shapes'; +import { setupCanvas, setupCanvasModules } from '@angular-three/storybook'; +import { + componentWrapperDecorator, + Meta, + moduleMetadata, + Story, +} from '@storybook/angular'; +import { + NgtSobaTransformControls, + NgtSobaTransformControlsModule, +} from './transform-controls.component'; + +export default { + title: 'Soba/Controls/Transform Controls', + component: NgtSobaTransformControls, +} as Meta; + +export const Default: Story = () => ({ + template: ` + + + + + + `, +}); +Default.decorators = [ + componentWrapperDecorator(setupCanvas({ black: true })), + moduleMetadata({ + imports: [ + ...setupCanvasModules, + NgtSobaTransformControlsModule, + NgtSobaBoxModule, + NgtMeshBasicMaterialModule, + ], + }), +]; + +export const LockOrbitControls: Story = Default.bind({}); +LockOrbitControls.decorators = [ + componentWrapperDecorator( + setupCanvas({ black: true, makeControlsDefault: true }) + ), + moduleMetadata({ + imports: [ + ...setupCanvasModules, + NgtSobaTransformControlsModule, + NgtSobaBoxModule, + NgtMeshBasicMaterialModule, + ], + }), +];