From 81c7d6d28be04415fb3cc174aa3632267e4ae131 Mon Sep 17 00:00:00 2001 From: Lucas Dower Date: Fri, 24 Mar 2023 22:12:49 +0000 Subject: [PATCH] Major refactor to `app_context.ts` --- src/actions/action_import.ts | 3 + src/app_context.ts | 654 +++++++++++++++-------------------- src/ui/components/button.ts | 251 +++++++------- src/ui/components/config.ts | 15 - src/ui/layout.ts | 58 +++- src/worker_controller.ts | 15 +- 6 files changed, 459 insertions(+), 537 deletions(-) create mode 100644 src/actions/action_import.ts diff --git a/src/actions/action_import.ts b/src/actions/action_import.ts new file mode 100644 index 0000000..c3bd007 --- /dev/null +++ b/src/actions/action_import.ts @@ -0,0 +1,3 @@ +export class ActionImport { + +} diff --git a/src/app_context.ts b/src/app_context.ts index 7590638..f9ed6e3 100644 --- a/src/app_context.ts +++ b/src/app_context.ts @@ -4,22 +4,17 @@ import { FallableBehaviour } from './block_mesh'; import { ArcballCamera } from './camera'; import { AppConfig } from './config'; import { EAppEvent, EventManager } from './event'; -import { TExporters } from './exporters/exporters'; import { MaterialMapManager } from './material-map'; -import { MaterialType } from './mesh'; import { MeshType, Renderer } from './renderer'; -import { PlaceholderComponent } from './ui/components/placeholder'; -import { SolidMaterialComponent } from './ui/components/solid_material'; -import { TexturedMaterialComponent } from './ui/components/textured_material'; import { AppConsole, TMessage } from './ui/console'; import { UI } from './ui/layout'; import { ColourSpace, EAction } from './util'; import { ASSERT } from './util/error_util'; import { download } from './util/file_util'; -import { Logger } from './util/log_util'; +import { LOG_ERROR, Logger } from './util/log_util'; import { Vector3 } from './vector'; -import { TWorkerJob, WorkerController } from './worker_controller'; -import { TFromWorkerMessage, TToWorkerMessage } from './worker_types'; +import { WorkerController } from './worker_controller'; +import { TFromWorkerMessage } from './worker_types'; export class AppContext { private _ui: UI; @@ -37,9 +32,6 @@ export class AppContext { AppConfig.Get.dumpConfig(); - // TODO Unimplemented - //FileUtil.rmdirIfExist(AppPaths.Get.gen); - const gl = (document.getElementById('canvas')).getContext('webgl'); if (!gl) { throw Error('Could not load WebGL context'); @@ -48,11 +40,14 @@ export class AppContext { this._ui = new UI(this); this._ui.build(); this._ui.registerEvents(); - this._ui.disable(EAction.Materials); - this._updateMaterialsAction(); + //this._ui.disable(EAction.Materials); + this._ui.updateMaterialsAction(this._materialManager); + this._ui.disableAll(); this._workerController = new WorkerController(); - this._workerController.addJob({ id: 'init', payload: { action: 'Init', params: {} } }); + this._workerController.execute({ action: 'Init', params: {}}).then(() => { + this._ui.enable(EAction.Import); + }); Renderer.Get.toggleIsAxesEnabled(); ArcballCamera.Get.setCameraMode('perspective'); @@ -84,47 +79,268 @@ export class AppContext { AppConsole.info('Ready'); } - public async do(action: EAction) { - this._ui.cacheValues(action); - this._ui.disable(action); - this._ui.disableAll(); + private async _import(): Promise { + // Gather data from the UI to send to the worker + const components = this._ui.layout.import.components; - const workerJob = await this._getWorkerJob(action); - if (workerJob === undefined) { - this._ui.enableTo(action); - return; + AppConsole.info('Importing mesh...'); + { + // Instruct the worker to perform the job and await the result + const resultImport = await this._workerController.execute({ + action: 'Import', + params: { + file: components.input.getValue(), + rotation: components.rotation.getValue(), + }, + }); + + this._ui.getActionButton(EAction.Import).resetLoading(); + if (this._handleErrors(resultImport)) { + return false; + } + ASSERT(resultImport.action === 'Import'); + + AppConsole.success('Imported mesh'); + this._addWorkerMessagesToConsole(resultImport.messages); + + this.maxConstraint = Vector3.copy(resultImport.result.dimensions) + .mulScalar(AppConfig.Get.CONSTRAINT_MAXIMUM_HEIGHT / 8.0).floor(); + this._materialManager = new MaterialMapManager(resultImport.result.materials); + this._ui.updateMaterialsAction(this._materialManager); } - const jobCallback = (payload: TFromWorkerMessage) => { - switch (payload.action) { - case 'KnownError': - case 'UnknownError': { - AppConsole.error(payload.action === 'KnownError' ? payload.error.message : 'Something unexpectedly went wrong'); - this._ui.getActionButton(action) - .stopLoading() - .setProgress(0.0); + AppConsole.info('Rendering mesh...'); + { + // Instruct the worker to perform the job and await the result + const resultRender = await this._workerController.execute({ + action: 'RenderMesh', + params: {}, + }); - this._ui.enableTo(action); - break; - } - default: { - ASSERT(payload.action !== 'Progress'); - this._addWorkerMessagesToConsole(payload.messages); - - if (workerJob.callback) { - workerJob.callback(payload); - } - } + this._ui.getActionButton(EAction.Import).resetLoading(); + if (this._handleErrors(resultRender)) { + return false; } - }; + ASSERT(resultRender.action === 'RenderMesh'); + + this._addWorkerMessagesToConsole(resultRender.messages); + Renderer.Get.useMesh(resultRender.result); + } + AppConsole.success('Rendered mesh'); + + return true; + } + + + private async _materials(): Promise { + AppConsole.info('Updating materials...'); + { + // Instruct the worker to perform the job and await the result + const resultMaterials = await this._workerController.execute({ + action: 'SetMaterials', + params: { + materials: this._materialManager.materials, + }, + }); + + this._ui.getActionButton(EAction.Materials).resetLoading(); + if (this._handleErrors(resultMaterials)) { + return false; + } + ASSERT(resultMaterials.action === 'SetMaterials'); + + resultMaterials.result.materialsChanged.forEach((materialName) => { + const material = this._materialManager.materials.get(materialName); + ASSERT(material !== undefined); + Renderer.Get.recreateMaterialBuffer(materialName, material); + Renderer.Get.setModelToUse(MeshType.TriangleMesh); + }); + + this._addWorkerMessagesToConsole(resultMaterials.messages); + } + AppConsole.success('Updated materials'); + + return true; + } + + private async _voxelise(): Promise { + // Gather data from the UI to send to the worker + const components = this._ui.layout.voxelise.components; + + AppConsole.info('Loading voxel mesh...'); + { + // Instruct the worker to perform the job and await the result + const resultVoxelise = await this._workerController.execute({ + action: 'Voxelise', + params: { + constraintAxis: components.constraintAxis.getValue(), + voxeliser: components.voxeliser.getValue(), + size: components.size.getValue(), + useMultisampleColouring: components.multisampleColouring.getValue(), + enableAmbientOcclusion: components.ambientOcclusion.getValue(), + voxelOverlapRule: components.voxelOverlapRule.getValue(), + }, + }); + + this._ui.getActionButton(EAction.Voxelise).resetLoading(); + if (this._handleErrors(resultVoxelise)) { + return false; + } + ASSERT(resultVoxelise.action === 'Voxelise'); + + this._addWorkerMessagesToConsole(resultVoxelise.messages); + } + AppConsole.success('Loaded voxel mesh'); + + AppConsole.info('Rendering voxel mesh...'); + { + let moreVoxelsToBuffer = false; + do { + // Instruct the worker to perform the job and await the result + const resultRender = await this._workerController.execute({ + action: 'RenderNextVoxelMeshChunk', + params: { + enableAmbientOcclusion: components.ambientOcclusion.getValue(), + desiredHeight: components.size.getValue(), + }, + }); + + this._ui.getActionButton(EAction.Voxelise).resetLoading(); + if (this._handleErrors(resultRender)) { + return false; + } + ASSERT(resultRender.action === 'RenderNextVoxelMeshChunk'); + + moreVoxelsToBuffer = resultRender.result.moreVoxelsToBuffer; + this._addWorkerMessagesToConsole(resultRender.messages); + + Renderer.Get.useVoxelMeshChunk(resultRender.result); + } while (moreVoxelsToBuffer); + } + AppConsole.success('Rendered voxel mesh'); + + return true; + } + + private async _assign(): Promise { + // Gather data from the UI to send to the worker + const components = this._ui.layout.assign.components; + + AppConsole.info('Loading block mesh...'); + { + // Instruct the worker to perform the job and await the result + const resultAssign = await this._workerController.execute({ + action: 'Assign', + params: { + textureAtlas: components.textureAtlas.getValue(), + blockPalette: components.blockPalette.getValue().getBlocks(), + dithering: components.dithering.getValue(), + colourSpace: ColourSpace.RGB, + fallable: components.fallable.getValue() as FallableBehaviour, + resolution: Math.pow(2, components.colourAccuracy.getValue()), + calculateLighting: components.calculateLighting.getValue(), + lightThreshold: components.lightThreshold.getValue(), + contextualAveraging: components.contextualAveraging.getValue(), + errorWeight: components.errorWeight.getValue() / 10, + }, + }); + + this._ui.getActionButton(EAction.Assign).resetLoading(); + if (this._handleErrors(resultAssign)) { + return false; + } + ASSERT(resultAssign.action === 'Assign'); + + this._addWorkerMessagesToConsole(resultAssign.messages); + } + AppConsole.success('Loaded block mesh'); + + AppConsole.info('Rendering block mesh...'); + { + let moreBlocksToBuffer = false; + do { + // Instruct the worker to perform the job and await the result + const resultRender = await this._workerController.execute({ + action: 'RenderNextBlockMeshChunk', + params: { + textureAtlas: components.textureAtlas.getValue(), + }, + }); + + this._ui.getActionButton(EAction.Assign).resetLoading(); + if (this._handleErrors(resultRender)) { + return false; + } + ASSERT(resultRender.action === 'RenderNextBlockMeshChunk'); + + moreBlocksToBuffer = resultRender.result.moreBlocksToBuffer; + this._addWorkerMessagesToConsole(resultRender.messages); + + Renderer.Get.useBlockMeshChunk(resultRender.result); + } while (moreBlocksToBuffer); + } + AppConsole.success('Rendered voxel mesh'); + + return true; + } + + private async _export(): Promise { + // Gather data from the UI to send to the worker + const components = this._ui.layout.export.components; + + AppConsole.info('Exporting structure...'); + { + // Instruct the worker to perform the job and await the result + const resultExport = await this._workerController.execute({ + action: 'Export', + params: { + filepath: '', + exporter: components.export.getValue(), + }, + }); + + this._ui.getActionButton(EAction.Export).resetLoading(); + if (this._handleErrors(resultExport)) { + return false; + } + ASSERT(resultExport.action === 'Export'); + + this._addWorkerMessagesToConsole(resultExport.messages); + download(resultExport.result.buffer, 'result.' + resultExport.result.extension); + } + AppConsole.success('Exported structure'); + + return true; + } + + /** + * Check if the result from the worker is an error message + * if so, handle it and return true, otherwise false. + */ + private _handleErrors(result: TFromWorkerMessage) { + if (result.action === 'KnownError') { + AppConsole.error(result.error.message); + return true; + } else if (result.action === 'UnknownError') { + AppConsole.error('Something unexpectedly went wrong'); + LOG_ERROR(result.error); + return true; + } + return false; + } + + public async do(action: EAction) { + // Disable the UI while the worker is working + this._ui.disableAll(); this._lastAction = action; - - this._workerController.addJob({ - id: workerJob.id, - payload: workerJob.payload, - callback: jobCallback, - }); + + const success = await this._executeAction(action); + if (success) { + this._ui.enableTo(action + 1); + } else { + this._ui.enableTo(action); + } } private _addWorkerMessagesToConsole(messages: TMessage[]) { @@ -133,352 +349,22 @@ export class AppContext { }); } - private _getWorkerJob(action: EAction): (Promise) { + private async _executeAction(action: EAction): Promise { switch (action) { case EAction.Import: - return this._import(); + return await this._import(); case EAction.Materials: - return Promise.resolve(this._materials()); + return await this._materials(); case EAction.Voxelise: - return Promise.resolve(this._voxelise()); + return await this._voxelise(); case EAction.Assign: - return Promise.resolve(this._assign()); + return await this._assign(); case EAction.Export: - return Promise.resolve(this._export()); + return await this._export(); } ASSERT(false); } - private async _import(): Promise { - const uiElements = this._ui.layout.import.components; - AppConsole.info('Importing mesh...'); - - const payload: TToWorkerMessage = { - action: 'Import', - params: { - file: uiElements.input.getValue(), - rotation: uiElements.rotation.getValue(), - }, - }; - - const callback = (payload: TFromWorkerMessage) => { - // This callback is managed through `AppContext::do`, therefore - // this callback is only called if the job is successful. - ASSERT(payload.action === 'Import'); - AppConsole.success('Imported mesh'); - - const dimensions = new Vector3( - payload.result.dimensions.x, - payload.result.dimensions.y, - payload.result.dimensions.z, - ); - dimensions.mulScalar(AppConfig.Get.CONSTRAINT_MAXIMUM_HEIGHT / 8.0).floor(); - this.maxConstraint = dimensions; - this._materialManager = new MaterialMapManager(payload.result.materials); - - if (payload.result.triangleCount < AppConfig.Get.RENDER_TRIANGLE_THRESHOLD) { - this._workerController.addJob(this._renderMesh()); - } else { - AppConsole.warning(`Will not render mesh as its over ${AppConfig.Get.RENDER_TRIANGLE_THRESHOLD.toLocaleString()} triangles.`); - } - - this._updateMaterialsAction(); - }; - callback.bind(this); - - return { id: 'Import', payload: payload, callback: callback }; - } - - private _materials(): TWorkerJob { - AppConsole.info('Updating materials...'); - - const payload: TToWorkerMessage = { - action: 'SetMaterials', - params: { - materials: this._materialManager.materials, - }, - }; - - const callback = (payload: TFromWorkerMessage) => { - // This callback is managed through `AppContext::do`, therefore - // this callback is only called if the job is successful. - ASSERT(payload.action === 'SetMaterials'); - AppConsole.success('Updated materials'); - - // The material map shouldn't need updating because the materials - // returned from the worker **should** be the same as the materials - // sent. - { - //this._materialMap = payload.result.materials; - //this._onMaterialMapChanged(); - } - - payload.result.materialsChanged.forEach((materialName) => { - const material = this._materialManager.materials.get(materialName); - ASSERT(material !== undefined); - Renderer.Get.recreateMaterialBuffer(materialName, material); - Renderer.Get.setModelToUse(MeshType.TriangleMesh); - }); - - this._ui.enableTo(EAction.Voxelise); - }; - - return { id: 'Import', payload: payload, callback: callback }; - } - - private _updateMaterialsAction() { - this._ui.layoutDull['materials'].components = {}; - this._ui.layoutDull['materials'].componentOrder = []; - - if (this._materialManager.materials.size == 0) { - this._ui.layoutDull['materials'].components[`placeholder_element`] = new PlaceholderComponent('No materials loaded'); - this._ui.layoutDull['materials'].componentOrder.push(`placeholder_element`); - } else { - this._materialManager.materials.forEach((material, materialName) => { - if (material.type === MaterialType.solid) { - this._ui.layoutDull['materials'].components[`mat_${materialName}`] = new SolidMaterialComponent(materialName, material) - .setLabel(materialName) - .onChangeTypeDelegate(() => { - this._materialManager.changeMaterialType(materialName, MaterialType.textured); - this._updateMaterialsAction(); - }); - } else { - this._ui.layoutDull['materials'].components[`mat_${materialName}`] = new TexturedMaterialComponent(materialName, material) - .setLabel(materialName) - .onChangeTypeDelegate(() => { - this._materialManager.changeMaterialType(materialName, MaterialType.solid); - this._updateMaterialsAction(); - }) - .onChangeTransparencyTypeDelegate((newTransparency) => { - this._materialManager.changeTransparencyType(materialName, newTransparency); - this._updateMaterialsAction(); - }); - } - - this._ui.layoutDull['materials'].componentOrder.push(`mat_${materialName}`); - }); - } - - this._ui.refreshComponents(EAction.Materials); - } - - private _renderMesh(): TWorkerJob { - AppConsole.info('Rendering mesh...'); - - const payload: TToWorkerMessage = { - action: 'RenderMesh', - params: {}, - }; - - const callback = (payload: TFromWorkerMessage) => { - // This callback is not managed through `AppContext::do`, therefore - // we need to check the payload is not an error - this._ui.enableTo(EAction.Voxelise); - - switch (payload.action) { - case 'KnownError': - case 'UnknownError': { - AppConsole.error(payload.action === 'KnownError' ? payload.error.message : 'Could not render mesh'); - break; - } - default: { - ASSERT(payload.action === 'RenderMesh'); - this._addWorkerMessagesToConsole(payload.messages); - AppConsole.success('Rendered mesh'); - - Renderer.Get.useMesh(payload.result); - } - } - }; - - return { id: 'RenderMesh', payload: payload, callback: callback }; - } - - private _voxelise(): TWorkerJob { - AppConsole.info('Loading voxel mesh...'); - - const uiElements = this._ui.layout.voxelise.components; - - const payload: TToWorkerMessage = { - action: 'Voxelise', - params: { - constraintAxis: uiElements.constraintAxis.getValue(), - voxeliser: uiElements.voxeliser.getValue(), - size: uiElements.size.getValue(), - useMultisampleColouring: uiElements.multisampleColouring.getValue(), - enableAmbientOcclusion: uiElements.ambientOcclusion.getValue(), - voxelOverlapRule: uiElements.voxelOverlapRule.getValue(), - }, - }; - - const callback = (payload: TFromWorkerMessage) => { - // This callback is managed through `AppContext::do`, therefore - // this callback is only called if the job is successful. - ASSERT(payload.action === 'Voxelise'); - AppConsole.success('Loaded voxel mesh'); - - this._workerController.addJob(this._renderVoxelMesh(true)); - }; - - return { id: 'Voxelise', payload: payload, callback: callback }; - } - - private _renderVoxelMesh(firstChunk: boolean): TWorkerJob { - if (firstChunk) { - AppConsole.info('Rendering voxel mesh...'); - } - - const uiElements = this._ui.layout.voxelise.components; - - const payload: TToWorkerMessage = { - action: 'RenderNextVoxelMeshChunk', - params: { - enableAmbientOcclusion: uiElements.ambientOcclusion.getValue(), - desiredHeight: uiElements.size.getValue(), - }, - }; - - const callback = (payload: TFromWorkerMessage) => { - // This callback is not managed through `AppContext::do`, therefore - // we need to check the payload is not an error - - switch (payload.action) { - case 'KnownError': - case 'UnknownError': { - AppConsole.error(payload.action === 'KnownError' ? payload.error.message : 'Could not render voxel mesh'); - this._ui.enableTo(EAction.Assign); - break; - } - default: { - ASSERT(payload.action === 'RenderNextVoxelMeshChunk'); - this._addWorkerMessagesToConsole(payload.messages); - - Renderer.Get.useVoxelMeshChunk(payload.result); - - if (payload.result.moreVoxelsToBuffer) { - this._workerController.addJob(this._renderVoxelMesh(false)); - } else { - AppConsole.success('Rendered voxel mesh'); - this._ui.enableTo(EAction.Assign); - } - } - } - }; - - return { id: 'RenderNextVoxelMeshChunk', payload: payload, callback: callback }; - } - - private _assign(): (TWorkerJob | undefined) { - const uiElements = this._ui.layout.assign.components; - - if (uiElements.blockPalette.getValue().count() <= 0) { - AppConsole.error('No blocks selected'); - return; - } - AppConsole.info('Loading block mesh...'); - - Renderer.Get.setLightingAvailable(uiElements.calculateLighting.getValue()); - - const payload: TToWorkerMessage = { - action: 'Assign', - params: { - textureAtlas: uiElements.textureAtlas.getValue(), - blockPalette: uiElements.blockPalette.getValue().getBlocks(), - dithering: uiElements.dithering.getValue(), - colourSpace: ColourSpace.RGB, - fallable: uiElements.fallable.getValue() as FallableBehaviour, - resolution: Math.pow(2, uiElements.colourAccuracy.getValue()), - calculateLighting: uiElements.calculateLighting.getValue(), - lightThreshold: uiElements.lightThreshold.getValue(), - contextualAveraging: uiElements.contextualAveraging.getValue(), - errorWeight: uiElements.errorWeight.getValue() / 10, - }, - }; - - const callback = (payload: TFromWorkerMessage) => { - // This callback is managed through `AppContext::do`, therefore - // this callback is only called if the job is successful. - ASSERT(payload.action === 'Assign'); - AppConsole.success('Loaded block mesh'); - - this._workerController.addJob(this._renderBlockMesh(true)); - }; - - return { id: 'Assign', payload: payload, callback: callback }; - } - - private _renderBlockMesh(firstChunk: boolean): TWorkerJob { - if (firstChunk) { - AppConsole.info('Rendering block mesh...'); - } - - const uiElements = this._ui.layout.assign.components; - - const payload: TToWorkerMessage = { - action: 'RenderNextBlockMeshChunk', - params: { - textureAtlas: uiElements.textureAtlas.getValue(), - }, - }; - - const callback = (payload: TFromWorkerMessage) => { - // This callback is not managed through `AppContext::do`, therefore - // we need to check the payload is not an error - - switch (payload.action) { - case 'KnownError': - case 'UnknownError': { - AppConsole.error(payload.action === 'KnownError' ? payload.error.message : 'Could not draw block mesh'); - this._ui.enableTo(EAction.Export); - break; - } - default: { - ASSERT(payload.action === 'RenderNextBlockMeshChunk'); - this._addWorkerMessagesToConsole(payload.messages); - - Renderer.Get.useBlockMeshChunk(payload.result); - - if (payload.result.moreBlocksToBuffer) { - this._workerController.addJob(this._renderBlockMesh(false)); - } else { - AppConsole.success('Rendered block mesh'); - this._ui.enableTo(EAction.Export); - } - } - } - }; - - return { id: 'RenderNextBlockMeshChunk', payload: payload, callback: callback }; - } - - private _export(): (TWorkerJob | undefined) { - AppConsole.info('Exporting structure...'); - - const exporterID: TExporters = this._ui.layout.export.components.export.getValue(); - const filepath = ''; - - const payload: TToWorkerMessage = { - action: 'Export', - params: { - filepath: filepath, - exporter: exporterID, - }, - }; - - const callback = (payload: TFromWorkerMessage) => { - // This callback is managed through `AppContext::do`, therefore - // this callback is only called if the job is successful. - ASSERT(payload.action === 'Export'); - AppConsole.success('Exported structure'); - - download(payload.result.buffer, 'result.' + payload.result.extension); - - this._ui.enableTo(EAction.Export); - }; - - return { id: 'Export', payload: payload, callback: callback }; - } - public draw() { Renderer.Get.update(); this._ui.tick(this._workerController.isBusy()); diff --git a/src/ui/components/button.ts b/src/ui/components/button.ts index d04fc19..7ad0739 100644 --- a/src/ui/components/button.ts +++ b/src/ui/components/button.ts @@ -1,123 +1,128 @@ -import { UIUtil } from '../../util/ui_util'; -import { BaseComponent } from './base'; - -export class ButtonComponent extends BaseComponent { - private _label: string; - private _onClick: () => void; - - public constructor() { - super(); - this._label = 'Unknown'; - this._onClick = () => { }; - } - - /** - * Sets the delegate that is called when this button is clicked. - */ - public setOnClick(delegate: () => void) { - this._onClick = delegate; - return this; - } - - /** - * Sets the label of this button. - */ - public setLabel(label: string) { - this._label = label; - return this; - } - - /** - * Override the current label with a new value. - */ - public setLabelOverride(label: string) { - this._getElement().innerHTML = label; - return this; - } - - /** - * Remove the label override and set the label back to its default - */ - public removeLabelOverride() { - this._getElement().innerHTML = this._label; - return this; - } - - /** - * Start the loading animation - */ - public startLoading() { - this._getElement().classList.add('button-loading'); - return this; - } - - /** - * Set the progress bar progress. - * @param progress A number between 0.0 and 1.0 inclusive. - */ - public setProgress(progress: number) { - const progressBarElement = UIUtil.getElementById(this._getProgressBarId()); - progressBarElement.style.width = `${progress * 100}%`; - return this; - } - - /** - * Stop the loading animation - */ - public stopLoading() { - this._getElement().classList.remove('button-loading'); - return this; - } - - public override generateHTML() { - return ` -
-
-
${this._label}
-
-
-
- `; - } - - public override registerEvents(): void { - this._getElement().addEventListener('click', () => { - if (this.enabled) { - this._onClick?.(); - } - }); - - this._getElement().addEventListener('mouseenter', () => { - this._setHovered(true); - this._updateStyles(); - }); - - this._getElement().addEventListener('mouseleave', () => { - this._setHovered(false); - this._updateStyles(); - }); - } - - protected override _onEnabledChanged() { - this._updateStyles(); - } - - public override finalise(): void { - this._updateStyles(); - } - - /** - * Gets the ID of the DOM element for the button's progress bar. - */ - private _getProgressBarId() { - return this._getId() + '-progress'; - } - - protected _updateStyles(): void { - UIUtil.updateStyles(this._getElement(), { - isActive: true, - isEnabled: this.enabled, - isHovered: this.hovered, - }); - } -} +import { UIUtil } from '../../util/ui_util'; +import { BaseComponent } from './base'; + +export class ButtonComponent extends BaseComponent { + private _label: string; + private _onClick: () => void; + + public constructor() { + super(); + this._label = 'Unknown'; + this._onClick = () => { }; + } + + /** + * Sets the delegate that is called when this button is clicked. + */ + public setOnClick(delegate: () => void) { + this._onClick = delegate; + return this; + } + + /** + * Sets the label of this button. + */ + public setLabel(label: string) { + this._label = label; + return this; + } + + /** + * Override the current label with a new value. + */ + public setLabelOverride(label: string) { + this._getElement().innerHTML = label; + return this; + } + + /** + * Remove the label override and set the label back to its default + */ + public removeLabelOverride() { + this._getElement().innerHTML = this._label; + return this; + } + + /** + * Start the loading animation + */ + public startLoading() { + this._getElement().classList.add('button-loading'); + return this; + } + + /** + * Set the progress bar progress. + * @param progress A number between 0.0 and 1.0 inclusive. + */ + public setProgress(progress: number) { + const progressBarElement = UIUtil.getElementById(this._getProgressBarId()); + progressBarElement.style.width = `${progress * 100}%`; + return this; + } + + /** + * Stop the loading animation + */ + public stopLoading() { + this._getElement().classList.remove('button-loading'); + return this; + } + + public resetLoading() { + this.stopLoading(); + this.setProgress(0.0); + } + + public override generateHTML() { + return ` +
+
+
${this._label}
+
+
+
+ `; + } + + public override registerEvents(): void { + this._getElement().addEventListener('click', () => { + if (this.enabled) { + this._onClick?.(); + } + }); + + this._getElement().addEventListener('mouseenter', () => { + this._setHovered(true); + this._updateStyles(); + }); + + this._getElement().addEventListener('mouseleave', () => { + this._setHovered(false); + this._updateStyles(); + }); + } + + protected override _onEnabledChanged() { + this._updateStyles(); + } + + public override finalise(): void { + this._updateStyles(); + } + + /** + * Gets the ID of the DOM element for the button's progress bar. + */ + private _getProgressBarId() { + return this._getId() + '-progress'; + } + + protected _updateStyles(): void { + UIUtil.updateStyles(this._getElement(), { + isActive: true, + isEnabled: this.enabled, + isHovered: this.hovered, + }); + } +} diff --git a/src/ui/components/config.ts b/src/ui/components/config.ts index 6015a19..6ab9280 100644 --- a/src/ui/components/config.ts +++ b/src/ui/components/config.ts @@ -8,7 +8,6 @@ import { BaseComponent } from './base'; export abstract class ConfigComponent extends BaseComponent { protected _label: string; private _value?: T; - private _cachedValue?: T; private _onValueChangedListeners: Array<(newValue: T) => void>; private _onEnabledChangedListeners: Array<(isEnabled: boolean) => void>; @@ -30,20 +29,6 @@ export abstract class ConfigComponent extends BaseComponent { return this; } - /** - * Caches the current value. - */ - public cacheValue() { - this._cachedValue = this._value; - } - - /** - * Returns whether the value stored is different from the cached value. - */ - public hasChanged() { - return this._cachedValue !== this._value; - } - /** * Get the currently set value of this UI element. */ diff --git a/src/ui/layout.ts b/src/ui/layout.ts index a0cbc3b..e759890 100644 --- a/src/ui/layout.ts +++ b/src/ui/layout.ts @@ -4,11 +4,11 @@ import { AppContext } from '../app_context'; import { FallableBehaviour } from '../block_mesh'; import { ArcballCamera } from '../camera'; import { TExporters } from '../exporters/exporters'; -import { PaletteManager, TPalettes } from '../palette'; +import { MaterialMapManager } from '../material-map'; +import { MaterialType } from '../mesh'; import { MeshType, Renderer } from '../renderer'; import { EAction } from '../util'; import { ASSERT } from '../util/error_util'; -import { LOG } from '../util/log_util'; import { TAxis } from '../util/type_util'; import { TDithering } from '../util/type_util'; import { UIUtil } from '../util/ui_util'; @@ -16,12 +16,15 @@ import { TVoxelOverlapRule } from '../voxel_mesh'; import { TVoxelisers } from '../voxelisers/voxelisers'; import { ButtonComponent } from './components/button'; import { CheckboxComponent } from './components/checkbox'; -import { ComboboxComponent, ComboBoxItem } from './components/combobox'; +import { ComboboxComponent } from './components/combobox'; import { ConfigComponent } from './components/config'; import { FileComponent } from './components/file_input'; import { HeaderComponent } from './components/header'; import { PaletteComponent } from './components/palette'; +import { PlaceholderComponent } from './components/placeholder'; import { SliderComponent } from './components/slider'; +import { SolidMaterialComponent } from './components/solid_material'; +import { TexturedMaterialComponent } from './components/textured_material'; import { ToolbarItemComponent } from './components/toolbar_item'; import { VectorSpinboxComponent } from './components/vector_spinbox'; import { AppConsole } from './console'; @@ -512,15 +515,6 @@ export class UI { } } - /** - * Caches the current value of each component in an action group. - */ - public cacheValues(action: EAction) { - this._forEachComponent(action, (component) => { - component.cacheValue(); - }); - } - /** * Rebuilds the HTML for all components in an action group. */ @@ -630,11 +624,11 @@ export class UI { */ public disable(action: EAction) { for (let i = action; i < EAction.MAX; ++i) { - this._forEachComponent(action, (component) => { + this._forEachComponent(i, (component) => { component.setEnabled(false); }); - this._getGroup(action).execButton.setEnabled(false); + this._getGroup(i).execButton.setEnabled(false); } } @@ -652,4 +646,40 @@ export class UI { const key = this.uiOrder[action]; return this._uiDull[key]; } + + public updateMaterialsAction(materialManager: MaterialMapManager) { + this.layout.materials.components = {}; + this.layout.materials.componentOrder = []; + + if (materialManager.materials.size == 0) { + this.layoutDull['materials'].components[`placeholder_element`] = new PlaceholderComponent('No materials loaded'); + this.layoutDull['materials'].componentOrder.push(`placeholder_element`); + } else { + materialManager.materials.forEach((material, materialName) => { + if (material.type === MaterialType.solid) { + this.layoutDull['materials'].components[`mat_${materialName}`] = new SolidMaterialComponent(materialName, material) + .setLabel(materialName) + .onChangeTypeDelegate(() => { + materialManager.changeMaterialType(materialName, MaterialType.textured); + this.updateMaterialsAction(materialManager); + }); + } else { + this.layoutDull['materials'].components[`mat_${materialName}`] = new TexturedMaterialComponent(materialName, material) + .setLabel(materialName) + .onChangeTypeDelegate(() => { + materialManager.changeMaterialType(materialName, MaterialType.solid); + this.updateMaterialsAction(materialManager); + }) + .onChangeTransparencyTypeDelegate((newTransparency) => { + materialManager.changeTransparencyType(materialName, newTransparency); + this.updateMaterialsAction(materialManager); + }); + } + + this.layoutDull['materials'].componentOrder.push(`mat_${materialName}`); + }); + } + + this.refreshComponents(EAction.Materials); + } } diff --git a/src/worker_controller.ts b/src/worker_controller.ts index 71beeca..eb4027a 100644 --- a/src/worker_controller.ts +++ b/src/worker_controller.ts @@ -1,6 +1,6 @@ import { AppConfig } from './config'; import { EAppEvent, EventManager } from './event'; -import { ASSERT } from './util/error_util'; +import { AppError, ASSERT } from './util/error_util'; import { LOG } from './util/log_util'; import { doWork } from './worker'; // @ts-ignore @@ -33,6 +33,19 @@ export class WorkerController { this._timerOn = false; } + public async execute(payload: TToWorkerMessage): Promise { + return new Promise((res, rej) => { + const success = this.addJob({ + id: 'ExecuteJob', + payload: payload, + callback: res, + }); + if (!success) { + rej(new AppError('Already performing a job')); + } + }); + } + public addJob(newJob: TWorkerJob): boolean { const isJobAlreadyQueued = this._jobQueue.some((queuedJob) => { return queuedJob.id === newJob.id; }); if (isJobAlreadyQueued) {