Major refactor to app_context.ts

This commit is contained in:
Lucas Dower 2023-03-24 22:12:49 +00:00
parent 4144497cd5
commit 81c7d6d28b
6 changed files with 459 additions and 537 deletions

View File

@ -0,0 +1,3 @@
export class ActionImport {
}

View File

@ -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 = (<HTMLCanvasElement>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<boolean> {
// 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<boolean> {
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<boolean> {
// 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<boolean> {
// 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<boolean> {
// 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<TWorkerJob | undefined>) {
private async _executeAction(action: EAction): Promise<boolean> {
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<TWorkerJob> {
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());

View File

@ -1,123 +1,128 @@
import { UIUtil } from '../../util/ui_util';
import { BaseComponent } from './base';
export class ButtonComponent extends BaseComponent<HTMLDivElement> {
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 `
<div class="container-button">
<div class="struct-prop button" id="${this._getId()}">
<div class="button-label">${this._label}</div>
<div class="button-progress" id="${this._getProgressBarId()}"></div>
</div>
</div>
`;
}
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<HTMLDivElement> {
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 `
<div class="container-button">
<div class="struct-prop button" id="${this._getId()}">
<div class="button-label">${this._label}</div>
<div class="button-progress" id="${this._getProgressBarId()}"></div>
</div>
</div>
`;
}
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,
});
}
}

View File

@ -8,7 +8,6 @@ import { BaseComponent } from './base';
export abstract class ConfigComponent<T, F> extends BaseComponent<F> {
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<T, F> extends BaseComponent<F> {
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.
*/

View File

@ -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);
}
}

View File

@ -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<TFromWorkerMessage> {
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) {