Skip to content
This repository has been archived by the owner on Feb 10, 2023. It is now read-only.

Commit

Permalink
feat(soba): add TransformControls
Browse files Browse the repository at this point in the history
  • Loading branch information
nartc committed Jan 4, 2022
1 parent d4b0bf7 commit 0e33191
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/soba/controls/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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: `
<ngt-primitive *ngIf="controls" [object]="controls"></ngt-primitive>
<ngt-group
*ngIf="!object"
(ready)="set({ group: $event })"
[object3dInputsController]="objectInputsController"
>
<ng-content></ng-content>
</ngt-group>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [NGT_OBJECT_INPUTS_CONTROLLER_PROVIDER],
})
export class NgtSobaTransformControls
extends EnhancedRxState<NgtSobaTransformControlsState>
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<THREE.Event>();
@Output() mousedown = new EventEmitter<THREE.Event>();
@Output() mouseup = new EventEmitter<THREE.Event>();
@Output() objectChange = new EventEmitter<THREE.Event>();

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<NgtObject3dInputsController>
) {
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 {}
Original file line number Diff line number Diff line change
@@ -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: `
<ngt-soba-transform-controls>
<ngt-soba-box>
<ngt-mesh-basic-material [parameters]='{wireframe: true}'></ngt-mesh-basic-material>
</ngt-soba-box>
</ngt-soba-transform-controls>
`,
});
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,
],
}),
];

0 comments on commit 0e33191

Please sign in to comment.