forked from mirror/ObjToSchematic
8e7544075f
* Added tracking disclaimer * Usage events are now tracked to improve usage
425 lines
16 KiB
TypeScript
425 lines
16 KiB
TypeScript
import '../styles.css';
|
|
import { AppAnalytics } from './analytics';
|
|
|
|
import { FallableBehaviour } from './block_mesh';
|
|
import { ArcballCamera } from './camera';
|
|
import { AppConfig } from './config';
|
|
import { EAppEvent, EventManager } from './event';
|
|
import { LOC, Localiser, TLocalisedString } from './localiser';
|
|
import { MaterialMapManager } from './material-map';
|
|
import { MouseManager } from './mouse';
|
|
import { MeshType, Renderer } from './renderer';
|
|
import { AppConsole, TMessage } from './ui/console';
|
|
import { UI } from './ui/layout';
|
|
import { ColourSpace, EAction } from './util';
|
|
import { ASSERT } from './util/error_util';
|
|
import { download, downloadAsZip } from './util/file_util';
|
|
import { LOG_ERROR, Logger } from './util/log_util';
|
|
import { Vector3 } from './vector';
|
|
import { WorkerController } from './worker_controller';
|
|
import { TFromWorkerMessage } from './worker_types';
|
|
|
|
export class AppContext {
|
|
/* Singleton */
|
|
private static _instance: AppContext;
|
|
public static get Get() {
|
|
return this._instance || (this._instance = new this());
|
|
}
|
|
|
|
private _workerController: WorkerController;
|
|
private _lastAction?: EAction;
|
|
public maxConstraint?: Vector3;
|
|
private _materialManager: MaterialMapManager;
|
|
private _loadedFilename: string | null;
|
|
|
|
private constructor() {
|
|
this._workerController = new WorkerController();
|
|
this._materialManager = new MaterialMapManager(new Map());
|
|
this._loadedFilename = null;
|
|
}
|
|
|
|
public static async init() {
|
|
AppAnalytics.Init();
|
|
|
|
await Localiser.Get.init();
|
|
AppConsole.info(LOC('init.initialising'));
|
|
|
|
Logger.Get.enableLOG();
|
|
Logger.Get.enableLOGMAJOR();
|
|
Logger.Get.enableLOGWARN();
|
|
|
|
AppConfig.Get.dumpConfig();
|
|
|
|
|
|
EventManager.Get.bindToContext(this.Get);
|
|
|
|
UI.Get.bindToContext(this.Get);
|
|
UI.Get.build();
|
|
UI.Get.registerEvents();
|
|
UI.Get.updateMaterialsAction(this.Get._materialManager);
|
|
UI.Get.disableAll();
|
|
|
|
ArcballCamera.Get.init();
|
|
MouseManager.Get.init();
|
|
|
|
window.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
|
|
this.Get._workerController.execute({ action: 'Init', params: {}}).then(() => {
|
|
UI.Get.enableTo(EAction.Import);
|
|
AppConsole.success(LOC('init.ready'));
|
|
});
|
|
|
|
ArcballCamera.Get.toggleAngleSnap();
|
|
|
|
EventManager.Get.add(EAppEvent.onLanguageChanged, () => {
|
|
this.Get._workerController.execute({ action: 'Settings', params: { language: Localiser.Get.getCurrentLanguage() }}).then(() => {
|
|
});
|
|
});
|
|
}
|
|
|
|
public getLastAction() {
|
|
return this._lastAction;
|
|
}
|
|
|
|
private async _import(): Promise<boolean> {
|
|
// Gather data from the UI to send to the worker
|
|
const components = UI.Get.layout.import.components;
|
|
let filetype: string;
|
|
|
|
AppConsole.info(LOC('import.importing_mesh'));
|
|
{
|
|
// Instruct the worker to perform the job and await the result
|
|
const file = components.input.getValue();
|
|
filetype = file.type;
|
|
|
|
const resultImport = await this._workerController.execute({
|
|
action: 'Import',
|
|
params: {
|
|
file: file,
|
|
rotation: components.rotation.getValue(),
|
|
},
|
|
});
|
|
|
|
UI.Get.getActionButton(EAction.Import)?.resetLoading();
|
|
if (this._handleErrors(resultImport)) {
|
|
return false;
|
|
}
|
|
ASSERT(resultImport.action === 'Import');
|
|
|
|
AppConsole.success(LOC('import.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);
|
|
UI.Get.updateMaterialsAction(this._materialManager);
|
|
|
|
this._loadedFilename = file.name.split('.')[0] ?? 'result';
|
|
}
|
|
|
|
AppConsole.info(LOC('import.rendering_mesh'));
|
|
{
|
|
// Instruct the worker to perform the job and await the result
|
|
const resultRender = await this._workerController.execute({
|
|
action: 'RenderMesh',
|
|
params: {},
|
|
});
|
|
|
|
UI.Get.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(LOC('import.rendered_mesh'));
|
|
|
|
AppAnalytics.Event('import', {
|
|
'filetype': filetype,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
|
|
private async _materials(): Promise<boolean> {
|
|
AppConsole.info(LOC('materials.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,
|
|
},
|
|
});
|
|
|
|
UI.Get.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(LOC('materials.updated_materials'));
|
|
|
|
AppAnalytics.Event('materials')
|
|
return true;
|
|
}
|
|
|
|
private async _voxelise(): Promise<boolean> {
|
|
// Gather data from the UI to send to the worker
|
|
const components = UI.Get.layout.voxelise.components;
|
|
|
|
AppConsole.info(LOC('voxelise.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(),
|
|
},
|
|
});
|
|
|
|
UI.Get.getActionButton(EAction.Voxelise)?.resetLoading();
|
|
if (this._handleErrors(resultVoxelise)) {
|
|
return false;
|
|
}
|
|
ASSERT(resultVoxelise.action === 'Voxelise');
|
|
|
|
this._addWorkerMessagesToConsole(resultVoxelise.messages);
|
|
}
|
|
AppConsole.success(LOC('voxelise.loaded_voxel_mesh'));
|
|
|
|
AppConsole.info(LOC('voxelise.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(),
|
|
},
|
|
});
|
|
|
|
UI.Get.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(LOC('voxelise.rendered_voxel_mesh'));
|
|
|
|
AppAnalytics.Event('voxelise', {
|
|
constraintAxis: components.constraintAxis.getValue(),
|
|
voxeliser: components.voxeliser.getValue(),
|
|
size: components.size.getValue(),
|
|
useMultisampleColouring: components.multisampleColouring.getValue(),
|
|
enableAmbientOcclusion: components.ambientOcclusion.getValue(),
|
|
voxelOverlapRule: components.voxelOverlapRule.getValue(),
|
|
});
|
|
return true;
|
|
}
|
|
|
|
private async _assign(): Promise<boolean> {
|
|
// Gather data from the UI to send to the worker
|
|
const components = UI.Get.layout.assign.components;
|
|
|
|
AppConsole.info(LOC('assign.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(),
|
|
ditheringMagnitude: components.ditheringMagnitude.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,
|
|
},
|
|
});
|
|
|
|
UI.Get.getActionButton(EAction.Assign)?.resetLoading();
|
|
if (this._handleErrors(resultAssign)) {
|
|
return false;
|
|
}
|
|
ASSERT(resultAssign.action === 'Assign');
|
|
|
|
this._addWorkerMessagesToConsole(resultAssign.messages);
|
|
}
|
|
AppConsole.success(LOC('assign.loaded_block_mesh'));
|
|
|
|
Renderer.Get.setLightingAvailable(components.calculateLighting.getValue());
|
|
|
|
AppConsole.info(LOC('assign.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(),
|
|
},
|
|
});
|
|
|
|
UI.Get.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(LOC('assign.rendered_block_mesh'));
|
|
|
|
AppAnalytics.Event('assign', {
|
|
dithering: components.dithering.getValue(),
|
|
ditheringMagnitude: components.ditheringMagnitude.getValue(),
|
|
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,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
private async _export(): Promise<boolean> {
|
|
// Gather data from the UI to send to the worker
|
|
const components = UI.Get.layout.export.components;
|
|
|
|
AppConsole.info(LOC('export.exporting_structure'));
|
|
{
|
|
// Instruct the worker to perform the job and await the result
|
|
const resultExport = await this._workerController.execute({
|
|
action: 'Export',
|
|
params: {
|
|
exporter: components.export.getValue(),
|
|
},
|
|
});
|
|
|
|
UI.Get.getActionButton(EAction.Export)?.resetLoading();
|
|
if (this._handleErrors(resultExport)) {
|
|
return false;
|
|
}
|
|
ASSERT(resultExport.action === 'Export');
|
|
|
|
this._addWorkerMessagesToConsole(resultExport.messages);
|
|
|
|
ASSERT(this._loadedFilename !== null)
|
|
const fileExport = resultExport.result.files;
|
|
if (fileExport.type === 'single') {
|
|
download(fileExport.content, `${this._loadedFilename}_OTS${fileExport.extension}`);
|
|
} else {
|
|
const zipFiles = fileExport.regions.map((region) => {
|
|
// .nbt exports need to be lowercase
|
|
return { content: region.content, filename: `ots_${region.name}${fileExport.extension}` }
|
|
});
|
|
|
|
downloadAsZip(`${this._loadedFilename}_OTS.zip`, zipFiles);
|
|
}
|
|
}
|
|
AppConsole.success(LOC('export.exported_structure'));
|
|
|
|
AppAnalytics.Event('export', {
|
|
exporter: components.export.getValue(),
|
|
});
|
|
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 as TLocalisedString);
|
|
return true;
|
|
} else if (result.action === 'UnknownError') {
|
|
AppConsole.error(LOC('something_went_wrong'));
|
|
LOG_ERROR(result.error);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public async do(action: EAction) {
|
|
// Disable the UI while the worker is working
|
|
UI.Get.disableAll();
|
|
|
|
this._lastAction = action;
|
|
|
|
const success = await this._executeAction(action);
|
|
if (success) {
|
|
if (action === EAction.Import) {
|
|
UI.Get.enableTo(EAction.Voxelise);
|
|
} else {
|
|
UI.Get.enableTo(action + 1);
|
|
}
|
|
} else {
|
|
UI.Get.enableTo(action);
|
|
}
|
|
}
|
|
|
|
private _addWorkerMessagesToConsole(messages: TMessage[]) {
|
|
messages.forEach((message) => {
|
|
AppConsole.add(message);
|
|
});
|
|
}
|
|
|
|
private async _executeAction(action: EAction): Promise<boolean> {
|
|
switch (action) {
|
|
case EAction.Import:
|
|
return await this._import();
|
|
case EAction.Materials:
|
|
return await this._materials();
|
|
case EAction.Voxelise:
|
|
return await this._voxelise();
|
|
case EAction.Assign:
|
|
return await this._assign();
|
|
case EAction.Export:
|
|
return await this._export();
|
|
}
|
|
ASSERT(false);
|
|
}
|
|
|
|
public static draw() {
|
|
Renderer.Get.update();
|
|
UI.Get.tick(this.Get._workerController.isBusy());
|
|
Renderer.Get.draw();
|
|
}
|
|
}
|