From 5e7f10bae6d975a9bf7134b5e008c78266e88ba8 Mon Sep 17 00:00:00 2001 From: Lucas Dower Date: Fri, 16 Sep 2022 18:51:22 +0100 Subject: [PATCH] Added framework for progress bars --- index.html | 4 +++ src/app_context.ts | 1 + src/event.ts | 5 ++- src/progress.ts | 52 +++++++++++++++++++++++++++++ src/voxelisers/bvh-ray-voxeliser.ts | 14 ++++++-- src/worker_client.ts | 41 ++++++++++++++++++++++- src/worker_controller.ts | 23 +++++++++++-- src/worker_types.ts | 6 ++++ styles.css | 13 ++++++++ 9 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 src/progress.ts diff --git a/index.html b/index.html index 24a8a8b..1d785c0 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,10 @@ +
+
+
+
diff --git a/src/app_context.ts b/src/app_context.ts index 2640a39..6934ef5 100644 --- a/src/app_context.ts +++ b/src/app_context.ts @@ -73,6 +73,7 @@ export class AppContext { default: { this._ui.enableTo(action + 1); + ASSERT(payload.action !== 'Progress'); const { builder, style } = this._getActionMessageBuilder(action, payload.statusMessages); uiOutput.setMessage(builder, style as OutputStyle); diff --git a/src/event.ts b/src/event.ts index 42be5f4..7797200 100644 --- a/src/event.ts +++ b/src/event.ts @@ -3,6 +3,9 @@ import { LOG } from './util/log_util'; /* eslint-disable */ export enum EAppEvent { + onTaskStart, + onTaskProgress, + onTaskEnd, } /* eslint-enable */ @@ -18,7 +21,7 @@ export class EventManager { this._eventsToListeners = new Map(); } - public add(event: EAppEvent, delegate: () => void) { + public add(event: EAppEvent, delegate: (...args: any[]) => void) { if (!this._eventsToListeners.has(event)) { this._eventsToListeners.set(event, []); } diff --git a/src/progress.ts b/src/progress.ts new file mode 100644 index 0000000..d5f3fdd --- /dev/null +++ b/src/progress.ts @@ -0,0 +1,52 @@ +import { EAppEvent, EventManager } from './event'; +import { ASSERT } from './util/error_util'; + + +export class ProgressManager { + /* Singleton */ + private static _instance: ProgressManager; + public static get Get() { + return this._instance || (this._instance = new this()); + } + + private _tasks: string[]; + + private constructor() { + this._tasks = []; + } + + /** + * Start tracking the progress of a task. + * @param taskId The id of the task (created here). + * @param max The maximum number the task progress index can reach. + * For example, if you are iterating over an array of 1000 elements, this will be 1000. + * @param divisions The number of updates the manager should track. + * For example, 4 means an event will be emitted for 20%, 40%, 60%, 80% progress. + */ + public start(taskId: string) { + ASSERT(!this._tasks.includes(taskId), 'Task with that Id already being tracked'); + this._tasks.push(taskId); + EventManager.Get.broadcast(EAppEvent.onTaskStart, taskId); + } + + /** + * Announce progress has been made on a task. + * @param taskId The id of the task (created in `start`). + * @param progressIndex The index of the progress made so far. + * For example, if you are iteratinve over an array of 1000 elements, and are on index 230, this should be 230. + */ + public progress(taskId: string, percentage: number) { + EventManager.Get.broadcast(EAppEvent.onTaskProgress, taskId, percentage); + } + + /** + * Announce a task has completed. + * @param taskId The id of the task (created in `start`). + */ + public end(taskId: string) { + const taskIndex = this._tasks.findIndex((task) => { return task === taskId }); + ASSERT(taskIndex !== -1, 'Task with that Id is not being tracked'); + this._tasks.splice(taskIndex, 1); + EventManager.Get.broadcast(EAppEvent.onTaskEnd, taskId); + } +} diff --git a/src/voxelisers/bvh-ray-voxeliser.ts b/src/voxelisers/bvh-ray-voxeliser.ts index 48a18f0..d85124c 100644 --- a/src/voxelisers/bvh-ray-voxeliser.ts +++ b/src/voxelisers/bvh-ray-voxeliser.ts @@ -1,4 +1,5 @@ import { Mesh } from '../mesh'; +import { ProgressManager } from '../progress'; import { Axes, axesToDirection, Ray } from '../ray'; import { ASSERT } from '../util/error_util'; import { LOG } from '../util/log_util'; @@ -19,12 +20,12 @@ export class BVHRayVoxeliser extends IVoxeliser { const scale = (voxeliseParams.desiredHeight - 1) / Mesh.desiredHeight; const offset = (voxeliseParams.desiredHeight % 2 === 0) ? new Vector3(0.0, 0.5, 0.0) : new Vector3(0.0, 0.0, 0.0); const useMesh = mesh.copy(); // TODO: Voxelise without copying mesh, too expensive for dense meshes - + useMesh.scaleMesh(scale); useMesh.translateMesh(offset); // Build BVH - const triangles = Array<{x: Number, y: Number, z: Number}[]>(useMesh._tris.length); + const triangles = Array<{ x: Number, y: Number, z: Number }[]>(useMesh._tris.length); for (let triIndex = 0; triIndex < useMesh.getTriangleCount(); ++triIndex) { const positionData = useMesh.getVertices(triIndex); triangles[triIndex] = [positionData.v0, positionData.v1, positionData.v2]; @@ -77,7 +78,15 @@ export class BVHRayVoxeliser extends IVoxeliser { LOG('Rays created...'); // Ray test BVH + let nextPercentage = 0.0; + ProgressManager.Get.start('Voxelising'); for (rayIndex = 0; rayIndex < rays.length; ++rayIndex) { + const percentage = rayIndex / rays.length; + if (rayIndex / rays.length >= nextPercentage) { + ProgressManager.Get.progress('Voxelising', percentage); + nextPercentage += 0.1; + } + const ray = rays[rayIndex]; const intersections = bvh.intersectRay(ray.origin, axesToDirection(ray.axis), false); for (const intersection of intersections) { @@ -98,6 +107,7 @@ export class BVHRayVoxeliser extends IVoxeliser { } } } + ProgressManager.Get.end('Voxelising'); return voxelMesh; } diff --git a/src/worker_client.ts b/src/worker_client.ts index f1b41af..c624a39 100644 --- a/src/worker_client.ts +++ b/src/worker_client.ts @@ -1,15 +1,17 @@ import { Atlas } from './atlas'; import { BlockMesh } from './block_mesh'; import { BufferGenerator } from './buffer'; +import { EAppEvent, EventManager } from './event'; import { IExporter } from './exporters/base_exporter'; import { ExporterFactory } from './exporters/exporters'; import { ObjImporter } from './importers/obj_importer'; import { Mesh } from './mesh'; import { ASSERT } from './util/error_util'; +import { Logger } from './util/log_util'; import { VoxelMesh } from './voxel_mesh'; import { IVoxeliser } from './voxelisers/base-voxeliser'; import { VoxeliserFactory } from './voxelisers/voxelisers'; -import { AssignParams, ExportParams, ImportParams, RenderBlockMeshParams, RenderMeshParams, RenderVoxelMeshParams, VoxeliseParams } from './worker_types'; +import { AssignParams, ExportParams, ImportParams, RenderBlockMeshParams, RenderMeshParams, RenderVoxelMeshParams, TFromWorkerMessage, VoxeliseParams } from './worker_types'; export class WorkerClient { private static _instance: WorkerClient; @@ -18,6 +20,43 @@ export class WorkerClient { } private constructor() { + Logger.Get.enableLOG(); + Logger.Get.enableLOGMAJOR(); + Logger.Get.enableLOGWARN(); + + EventManager.Get.add(EAppEvent.onTaskStart, (e: any) => { + const message: TFromWorkerMessage = { + action: 'Progress', + payload: { + type: 'Started', + taskId: e[0], + }, + }; + postMessage(message); + }); + + EventManager.Get.add(EAppEvent.onTaskProgress, (e: any) => { + const message: TFromWorkerMessage = { + action: 'Progress', + payload: { + type: 'Progress', + taskId: e[0], + percentage: e[1], + }, + }; + postMessage(message); + }); + + EventManager.Get.add(EAppEvent.onTaskEnd, (e: any) => { + const message: TFromWorkerMessage = { + action: 'Progress', + payload: { + type: 'Finished', + taskId: e[0], + }, + }; + postMessage(message); + }); } private _loadedMesh?: Mesh; diff --git a/src/worker_controller.ts b/src/worker_controller.ts index 1b9015b..2a8f380 100644 --- a/src/worker_controller.ts +++ b/src/worker_controller.ts @@ -37,8 +37,27 @@ export class WorkerController { return this._jobPending !== undefined; } - private _onWorkerMessage(payload: any) { + private _onWorkerMessage(payload: MessageEvent) { ASSERT(this._jobPending !== undefined, `Received worker message when no job is pending`); + + if (payload.data.action === 'Progress') { + const element = document.getElementById('progress-bar') as HTMLDivElement; + if (element) { + switch (payload.data.payload.type) { + case 'Started': + element.style.width = `0%`; + break; + case 'Progress': + element.style.width = `${payload.data.payload.percentage * 100}%`; + break; + case 'Finished': + element.style.width = `100%`; + break; + } + } + return; + } + TIME_END(this._jobPending.id); LOG(`[WorkerController]: Job '${this._jobPending.id}' finished:`); @@ -54,7 +73,7 @@ export class WorkerController { if (this.isBusy()) { return; } - + this._jobPending = this._jobQueue.shift(); if (this._jobPending === undefined) { return; diff --git a/src/worker_types.ts b/src/worker_types.ts index b28a781..b45f580 100644 --- a/src/worker_types.ts +++ b/src/worker_types.ts @@ -104,6 +104,11 @@ export type TStatus = { statusMessages: StatusMessage[], } +export type TaskParams = + | { type: 'Started', taskId: string } + | { type: 'Progress', taskId: string, percentage: number } + | { type: 'Finished', taskId: string } + export type TToWorkerMessage = | { action: 'Import', params: ImportParams.Input } | { action: 'RenderMesh', params: RenderMeshParams.Input } @@ -116,6 +121,7 @@ export type TToWorkerMessage = export type TFromWorkerMessage = | { action: 'KnownError', error: AppError } | { action: 'UnknownError', error: Error } + | { action: 'Progress', payload: TaskParams } | (TStatus & ( | { action: 'Import', result: ImportParams.Output } | { action: 'RenderMesh', result: RenderMeshParams.Output } diff --git a/styles.css b/styles.css index db1a4ce..ca58a3e 100644 --- a/styles.css +++ b/styles.css @@ -575,4 +575,17 @@ svg { 100% { opacity: 100%; } +} + + + +.progress-bar-container { + width: 100%; + height: 2px; +} + +.progress-bar { + background-color: var(--prop-accent-standard); + height: 100%; + transition: width 0.1s; } \ No newline at end of file