Skip to content

Commit

Permalink
feat(frontend): load obj in web worker & display progress-spinner (#42)
Browse files Browse the repository at this point in the history
* refactor(frontend): emit only if connection status changes

* doc(ui): add how to add new module & component with `ng g`

* refactor(shared): export `Nil` type

* feat(ui): add fullscreen-overlay module

* feat(ui): add simple progress-spinner component

* feat(ui): set `ChangeDetectionStrategy.OnPush`

* doc(kafka): stop/remove all docker containers with values

* refactor(ui): export progress-spinner module & include it in specs

* feat(frontend): add web worker for loading obj file

* feat(frontend): show progress-spinner while loading obj in worker

* test(frontend): provide mocks

* style(frontend): fix order
  • Loading branch information
PhilippeMorier committed Mar 18, 2020
1 parent 637a158 commit f0c11db
Show file tree
Hide file tree
Showing 27 changed files with 468 additions and 9 deletions.
9 changes: 7 additions & 2 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"tsConfig": "apps/frontend/tsconfig.app.json",
"assets": ["apps/frontend/src/favicon.ico", "apps/frontend/src/assets"],
"styles": ["apps/frontend/src/styles.scss", "apps/frontend/src/themes.scss"],
"scripts": []
"scripts": [],
"webWorkerTsConfig": "apps/frontend/tsconfig.worker.json"
},
"configurations": {
"production": {
Expand Down Expand Up @@ -80,7 +81,11 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["apps/frontend/tsconfig.app.json", "apps/frontend/tsconfig.spec.json"],
"tsConfig": [
"apps/frontend/tsconfig.app.json",
"apps/frontend/tsconfig.spec.json",
"apps/frontend/tsconfig.worker.json"
],
"exclude": ["**/node_modules/**", "!apps/frontend/**"]
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { OverlayRef } from '@angular/cdk/overlay';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { UiProgressSpinnerModule, UiSceneViewerTestModule, UI_OVERLAY_DATA } from '@talus/ui';
import { of } from 'rxjs';
import * as fromApp from '../../app.reducer';
import { initialMockState } from '../../testing';
import { LoadFileContainerComponent } from './load-file-container.component';
import { LoadFileService } from './load-file.service';

describe('LoadFileContainerComponent', () => {
let component: LoadFileContainerComponent;
let fixture: ComponentFixture<LoadFileContainerComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [LoadFileContainerComponent],
imports: [UiSceneViewerTestModule, UiProgressSpinnerModule],
providers: [
{ provide: UI_OVERLAY_DATA, useValue: {} },
{ provide: OverlayRef, useValue: {} },
{
provide: LoadFileService,
useValue: {
load: () => of({ coords: [], isConverting: false, isLoading: false, progress: 100 }),
},
},
provideMockStore<fromApp.State>({
initialState: initialMockState,
}),
],
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(LoadFileContainerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { OverlayRef } from '@angular/cdk/overlay';
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { Action, Store } from '@ngrx/store';
import { rgbaToInt } from '@talus/model';
import { finalizeWithValue } from '@talus/shared';
import { UI_OVERLAY_DATA } from '@talus/ui';
import { Coord } from '@talus/vdb';
import { tap } from 'rxjs/operators';
import * as fromApp from '../../app.reducer';
import { setVoxels } from '../scene-viewer-container.actions';
import { LoadFileService } from './load-file.service';

@Component({
selector: 'fe-load-file-container',
template: `
<ui-progress-spinner
*ngIf="load$ | async"
[mode]="mode"
[status]="status"
[value]="progress"
></ui-progress-spinner>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoadFileContainerComponent {
mode: 'indeterminate' | 'determinate' = 'indeterminate';
progress = 0;
status = 'Loading file...';

load$ = this.loadFileService.load(this.data).pipe(
tap(status => {
if (status.isConverting) {
this.mode = 'determinate';
this.status = 'Converting...';
}

this.progress = status.progress;
}),
finalizeWithValue(status => {
this.status = 'Adding voxels...';

setTimeout(() => {
const action = this.getActionFromCoords(status.coords);
this.store.dispatch(action);

this.overlayRef.detach();
}, 500);
}),
);

constructor(
@Inject(UI_OVERLAY_DATA) private readonly data: File,
private readonly overlayRef: OverlayRef,
private readonly store: Store<fromApp.State>,
private readonly loadFileService: LoadFileService,
) {}

private getActionFromCoords(coords: Coord[]): Action {
const colors: number[] = [];
const defaultColor = rgbaToInt({ r: 0, g: 255, b: 0, a: 255 });
const scaleFactor = 50;

for (let i = 0; i < coords.length; i++) {
coords[i] = [
coords[i][0] * scaleFactor,
coords[i][1] * scaleFactor,
coords[i][2] * scaleFactor,
];
colors.push(defaultColor);
}

return setVoxels({ coords, newValues: colors, needsSync: true });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UiProgressSpinnerModule } from '@talus/ui';
import { LoadFileContainerComponent } from './load-file-container.component';
import { LoadFileService } from './load-file.service';

@NgModule({
declarations: [LoadFileContainerComponent],
imports: [CommonModule, UiProgressSpinnerModule],
exports: [LoadFileContainerComponent],
providers: [LoadFileService],
})
export class LoadFileContainerModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { LoadFileStatus } from './load-file.worker';

@Injectable()
export class LoadFileService {
private worker: Worker;

constructor() {
if (typeof Worker !== 'undefined') {
this.worker = new Worker('./load-file.worker', { type: 'module' });
} else {
// Add a fallback so that program still executes correctly.
console.log('Web workers are not supported in this environment.');
}
}

load(file: File): Observable<LoadFileStatus> {
return new Observable<LoadFileStatus>(subscriber => {
this.worker.onmessage = ({ data: status }) => {
subscriber.next(status);

if (status.progress === 100) {
subscriber.complete();
}
};

this.worker.postMessage(file);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/// <reference lib="webworker" />

import { AssetContainer } from '@babylonjs/core';
import { NullEngine } from '@babylonjs/core/Engines/nullEngine';
import { SceneLoader } from '@babylonjs/core/Loading/sceneLoader';
import { VertexBuffer } from '@babylonjs/core/Meshes/buffer';
import { Scene } from '@babylonjs/core/scene';
import { FloatArray } from '@babylonjs/core/types';
// https://doc.babylonjs.com/how_to/obj
import '@babylonjs/loaders/OBJ/objFileLoader';
import { Coord } from '@talus/vdb';

export interface LoadFileStatus {
coords: Coord[];
isConverting: boolean;
isLoading: boolean;
progress: number;
}

addEventListener('message', ({ data: file }) => {
loadFile(file);
});

function loadFile(file: File): void {
// SceneLoader needs a scene. A scene object can't be passed/cloned to the worker.
// Therefore, create a no-op scene.
const nullScene = new Scene(new NullEngine());

postStatusMessage({ coords: [], isConverting: false, isLoading: true, progress: 0 });
SceneLoader.LoadAssetContainer('file:', file, nullScene, onSuccess);

function onSuccess(assets: AssetContainer): void {
postStatusMessage({ coords: [], isConverting: true, isLoading: false, progress: 0 });

const positions = getPositions(assets);
const coords = floatArrayToCoords(positions);

postStatusMessage({ coords, isConverting: false, isLoading: false, progress: 100 });
}
}

function postStatusMessage(status: LoadFileStatus): void {
postMessage(status);
}

function getPositions(assets: AssetContainer): FloatArray {
if (assets.meshes.length < 1) {
return [];
}

const positions = assets.meshes[0].getVerticesData(VertexBuffer.PositionKind);
if (!positions) {
return [];
}

return positions;
}

function floatArrayToCoords(positions: FloatArray): Coord[] {
const coords: Coord[] = [];

const progressFactor = 100 / positions.length;
let progress = 0;

for (let i = 0; i < positions.length; i += 3) {
coords.push([positions[i], positions[i + 1], positions[i + 2]]);

const currentProgress = Math.trunc(i * progressFactor);
if (progress !== currentProgress) {
progress = currentProgress;
postStatusMessage({ coords: [], isConverting: true, isLoading: false, progress });
}
}

return coords;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { OverlayRef } from '@angular/cdk/overlay';
import { ChangeDetectionStrategy, Component, Output } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MemoizedSelector, Store } from '@ngrx/store';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { rgbaToInt, Tool } from '@talus/model';
import { UiPointerButton, UiPointerPickInfo, UiSceneViewerService } from '@talus/ui';
import {
UiFullscreenOverlayModule,
UiPointerButton,
UiPointerPickInfo,
UiSceneViewerService,
UI_OVERLAY_DATA,
} from '@talus/ui';
import { Coord } from '@talus/vdb';
import { Subject } from 'rxjs';
import * as fromApp from '../app.reducer';
Expand Down Expand Up @@ -33,9 +40,12 @@ describe('SceneViewerContainerComponent', () => {

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [UiFullscreenOverlayModule],
declarations: [SceneViewerContainerComponent, SceneViewerStubComponent],
providers: [
GridService,
{ provide: UI_OVERLAY_DATA, useValue: {} },
{ provide: OverlayRef, useValue: {} },
{ provide: UiSceneViewerService, useValue: { resizeView: () => {} } },
provideMockStore<fromApp.State>({
initialState: initialMockState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, ViewChild } from '@a
import { select, Store } from '@ngrx/store';
import { rgbaToInt, Tool } from '@talus/model';
import {
UiFullscreenOverlayService,
UiPointerButton,
UiPointerPickInfo,
UiSceneViewerComponent,
Expand All @@ -12,6 +13,7 @@ import {
import { areEqual, Coord, createMaxCoord, removeFraction } from '@talus/vdb';
import { combineLatest, Observable } from 'rxjs';
import * as fromApp from '../app.reducer';
import { LoadFileContainerComponent } from './load-file-container/load-file-container.component';
import {
paintVoxel,
removeVoxel,
Expand All @@ -28,6 +30,7 @@ import {
*ngIf="selectedToolIdAndColor$ | async as selected"
(pointerPick)="onPointerPick($event, selected[0], selected[1])"
(pointUnderPointer)="onPointUnderPointer($event, selected[0], selected[1])"
(dropFiles)="onDropFiles($event)"
></ui-scene-viewer>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
Expand All @@ -45,8 +48,9 @@ export class SceneViewerContainerComponent implements AfterViewInit {
private lastUnderPointerPosition: Coord = createMaxCoord();

constructor(
private sceneViewerService: UiSceneViewerService,
private store: Store<fromApp.State>,
private readonly fullscreenOverlayService: UiFullscreenOverlayService,
private readonly sceneViewerService: UiSceneViewerService,
private readonly store: Store<fromApp.State>,
) {}

ngAfterViewInit(): void {
Expand All @@ -72,6 +76,14 @@ export class SceneViewerContainerComponent implements AfterViewInit {
}
}

onDropFiles(files: File[]): void {
if (files.length < 1) {
return;
}

this.fullscreenOverlayService.open(LoadFileContainerComponent, files[0]);
}

private dispatchVoxelUnderCursorChange(pickInfo: UiPointerPickInfo, selectedColor: number): void {
const toAddPosition = this.calcVoxelToAddPosition(pickInfo);
const underPointerPosition = this.calcVoxelUnderPointerPosition(pickInfo);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { UiSceneViewerModule, UiTopicDialogModule } from '@talus/ui';
import { UiFullscreenOverlayModule, UiSceneViewerModule, UiTopicDialogModule } from '@talus/ui';
import { GridService } from './grid.service';
import { LoadFileContainerModule } from './load-file-container/load-file-container.module';
import { SceneViewerContainerComponent } from './scene-viewer-container.component';
import { SceneViewerContainerEffects } from './scene-viewer-container.effects';

Expand All @@ -11,6 +12,8 @@ import { SceneViewerContainerEffects } from './scene-viewer-container.effects';
imports: [
CommonModule,
EffectsModule.forFeature([SceneViewerContainerEffects]),
LoadFileContainerModule,
UiFullscreenOverlayModule,
UiSceneViewerModule,
UiTopicDialogModule,
],
Expand Down
4 changes: 2 additions & 2 deletions apps/frontend/src/app/web-socket/web-socket.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { EventName } from '@talus/model';
import { fromEvent, Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { distinctUntilChanged, map } from 'rxjs/operators';
import io from 'socket.io-client';

export class WebSocketService {
private socket: SocketIOClient.Socket;

private readonly connectionStatusSubject = new Subject<boolean>();
connectionStatus$ = this.connectionStatusSubject.asObservable();
connectionStatus$ = this.connectionStatusSubject.asObservable().pipe(distinctUntilChanged());

socketId$ = this.connectionStatus$.pipe(map(() => this.socket.id));

Expand Down
9 changes: 9 additions & 0 deletions apps/frontend/tsconfig.worker.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/worker",
"lib": ["es2018", "webworker"],
"types": []
},
"include": ["src/**/*.worker.ts"]
}

0 comments on commit f0c11db

Please sign in to comment.