New warning handler, minor refactor

This commit is contained in:
Lucas Dower 2022-04-13 20:10:53 +01:00
parent 335f6ed984
commit 081b602493
16 changed files with 344 additions and 225 deletions

View File

@ -3,7 +3,7 @@ import { Litematic, Schematic } from './schematic';
import { Renderer } from './renderer';
import { Mesh } from './mesh';
import { ObjImporter } from './importers/obj_importer';
import { ASSERT, ColourSpace, CustomError, CustomWarning, LOG, LOG_ERROR, LOG_WARN } from './util';
import { ASSERT, ColourSpace, AppError, LOG, LOG_ERROR, LOG_WARN } from './util';
import { remote } from 'electron';
import { VoxelMesh, VoxelMeshParams } from './voxel_mesh';
@ -13,22 +13,12 @@ import { RayVoxeliser } from './voxelisers/ray-voxeliser';
import { IVoxeliser } from './voxelisers/base-voxeliser';
import { NormalCorrectedRayVoxeliser } from './voxelisers/normal-corrected-ray-voxeliser';
import { BVHRayVoxeliser } from './voxelisers/bvh-ray-voxeliser';
import { StatusHandler } from './status';
import { UIMessageBuilder } from './ui/misc';
import { OutputStyle } from './ui/elements/output';
/* eslint-disable */
export enum ActionReturnType {
Success,
Warning,
Failure
}
/* eslint-enable */
export interface ReturnStatus {
message: string,
type: ActionReturnType,
error?: unknown
}
/* eslint-disable */
export enum Action {
export enum EAction {
Import = 0,
Simplify = 1,
Voxelise = 2,
@ -38,21 +28,13 @@ export enum Action {
}
/* eslint-enable */
const ReturnMessages = new Map<Action, { onSuccess: string, onFailure: string }>();
ReturnMessages.set(Action.Import, { onSuccess: 'Loaded mesh successfully', onFailure: 'Failed to load mesh' });
ReturnMessages.set(Action.Simplify, { onSuccess: 'Simplified mesh successfully', onFailure: 'Failed to simplify mesh' });
ReturnMessages.set(Action.Voxelise, { onSuccess: 'Voxelised mesh successfully', onFailure: 'Failed to voxelise mesh' });
ReturnMessages.set(Action.Palette, { onSuccess: 'Assigned blocks successfully', onFailure: 'Failed to assign blocks' });
ReturnMessages.set(Action.Export, { onSuccess: 'Exported structure successfully', onFailure: 'Failed to export structure' });
export class AppContext {
private _loadedMesh?: Mesh;
private _loadedVoxelMesh?: VoxelMesh;
private _loadedBlockMesh?: BlockMesh;
private _warnings: string[];
private _ui: UI;
private _actionMap = new Map<Action, {
private _actionMap = new Map<EAction, {
action: () => void;
onFailure?: () => void
}>();
@ -63,86 +45,91 @@ export class AppContext {
throw Error('Could not load WebGL context');
}
this._warnings = [];
this._actionMap.set(Action.Import, {
action: () => {
return this._import();
},
onFailure: () => {
this._loadedMesh = undefined;
},
});
this._actionMap.set(Action.Simplify, {
action: () => {
return this._simplify();
},
});
this._actionMap.set(Action.Voxelise, {
action: () => {
return this._voxelise();
},
onFailure: () => {
this._loadedVoxelMesh = undefined;
},
});
this._actionMap.set(Action.Palette, {
action: () => {
return this._palette();
},
onFailure: () => {
this._loadedBlockMesh = undefined;
},
});
this._actionMap.set(Action.Export, {
action: () => {
return this._export();
},
});
this._actionMap = new Map([
[
EAction.Import, {
action: () => { return this._import(); },
onFailure: () => { this._loadedMesh = undefined; },
},
],
[
EAction.Simplify, {
action: () => { return this._simplify(); },
},
],
[
EAction.Voxelise, {
action: () => { return this._voxelise(); },
onFailure: () => { this._loadedVoxelMesh = undefined; },
},
],
[
EAction.Palette, {
action: () => { return this._palette(); },
onFailure: () => { this._loadedBlockMesh = undefined; },
},
],
[
EAction.Export, {
action: () => { return this._export(); },
},
],
]);
this._ui = new UI(this);
this._ui.build();
this._ui.registerEvents();
// this._ui.disablePost(Action.Import);
this._ui.disable(Action.Simplify);
this._ui.disable(EAction.Simplify);
Renderer.Get.toggleIsGridEnabled();
}
public do(action: Action) {
this._ui.disable(action + 1);
this._warnings = [];
const groupName = this._ui.uiOrder[action];
public do(action: EAction) {
LOG(`Doing ${action}`);
const groupName = this._ui.uiOrder[action];
this._ui.disable(action + 1);
this._ui.cacheValues(action);
StatusHandler.Get.clear();
const delegate = this._actionMap.get(action)!;
try {
delegate.action();
} catch (err: any) {
LOG_ERROR(err);
if (err instanceof CustomError) {
this._ui.layoutDull[groupName].output.setMessage(err.message, ActionReturnType.Failure);
} else if (err instanceof CustomWarning) {
this._ui.layoutDull[groupName].output.setMessage(err.message, ActionReturnType.Warning);
} catch (error: any) {
// On failure...
LOG_ERROR(error);
const message = new UIMessageBuilder();
if (error instanceof AppError) {
message.addHeading(StatusHandler.Get.getDefaultFailureMessage(action));
message.add(error.message);
} else {
this._ui.layoutDull[groupName].output.setMessage(ReturnMessages.get(action)!.onFailure, ActionReturnType.Failure);
}
if (delegate.onFailure) {
delegate.onFailure();
message.addBold(StatusHandler.Get.getDefaultFailureMessage(action));
}
this._ui.layoutDull[groupName].output.setMessage(message, 'error');
delegate.onFailure?.();
return;
}
const successMessage = ReturnMessages.get(action)!.onSuccess;
if (this._warnings.length !== 0) {
const allWarnings = this._warnings.join('<br>');
this._ui.layoutDull[groupName].output.setMessage(successMessage + `, with ${this._warnings.length} warning(s):` + '<br><b>' + allWarnings + '</b>', ActionReturnType.Warning);
// On success...
const message = new UIMessageBuilder();
if (StatusHandler.Get.hasStatusMessages('info')) {
message.addHeading(StatusHandler.Get.getDefaultSuccessMessage(action));
message.add(...StatusHandler.Get.getStatusMessages('info'));
} else {
this._ui.layoutDull[groupName].output.setMessage(successMessage, ActionReturnType.Success);
message.addBold(StatusHandler.Get.getDefaultSuccessMessage(action));
}
LOG(`Finished ${action}`);
let returnStyle: OutputStyle = 'success';
if (StatusHandler.Get.hasStatusMessages('warning')) {
message.addHeading('There were some warnings');
message.add(...StatusHandler.Get.getStatusMessages('warning'));
returnStyle = 'warning';
}
this._ui.layoutDull[groupName].output.setMessage(message, returnStyle);
this._ui.enable(action + 1);
LOG(`Finished ${action}`);
}
private _import() {
@ -154,8 +141,6 @@ export class AppContext {
this._loadedMesh = importer.toMesh();
this._loadedMesh.processMesh();
Renderer.Get.useMesh(this._loadedMesh);
this._warnings = this._loadedMesh.getWarnings();
}
private _simplify() {
@ -164,13 +149,13 @@ export class AppContext {
private _voxelise() {
ASSERT(this._loadedMesh);
const uiElements = this._ui.layout.build.elements;
const voxelMeshParams: VoxelMeshParams = {
desiredHeight: uiElements.height.getCachedValue() as number,
useMultisampleColouring: uiElements.multisampleColouring.getCachedValue() === 'on',
textureFiltering: uiElements.textureFiltering.getCachedValue() === 'linear' ? TextureFiltering.Linear : TextureFiltering.Nearest,
};
const ambientOcclusionEnabled = uiElements.ambientOcclusion.getCachedValue() === 'on';
@ -184,7 +169,7 @@ export class AppContext {
ASSERT(voxeliserID === 'normalcorrectedraybased');
voxeliser = new NormalCorrectedRayVoxeliser();
}
this._loadedVoxelMesh = voxeliser.voxelise(this._loadedMesh, voxelMeshParams);
Renderer.Get.useVoxelMesh(this._loadedVoxelMesh, ambientOcclusionEnabled);
}
@ -222,8 +207,6 @@ export class AppContext {
}
exporter.export(this._loadedBlockMesh, filePath);
}
this._warnings = exporter.getWarnings();
}
public draw() {
@ -237,6 +220,5 @@ export class AppContext {
public addWarning(warning: string) {
LOG_WARN(warning);
this._warnings.push(warning);
}
}

View File

@ -1,7 +1,7 @@
import { BasicBlockAssigner, OrderedDitheringBlockAssigner } from './block_assigner';
import { Voxel, VoxelMesh } from './voxel_mesh';
import { BlockAtlas, BlockInfo } from './block_atlas';
import { ColourSpace, CustomError } from './util';
import { ColourSpace, AppError } from './util';
import { Renderer } from './renderer';
interface Block {
@ -64,7 +64,7 @@ export class BlockMesh {
public getVoxelMesh() {
if (!this._voxelMesh) {
throw new CustomError('Could not get voxel mesh');
throw new AppError('Could not get voxel mesh');
}
return this._voxelMesh;
}

View File

@ -1,7 +1,6 @@
import { Mesh } from './mesh';
import { Warnable } from './util';
export abstract class IImporter extends Warnable {
export abstract class IImporter {
abstract parseFile(filePath: string): void;
abstract toMesh(): Mesh;
}

View File

@ -1,11 +1,12 @@
import { IImporter } from '../importer';
import { MaterialType, Mesh, SolidMaterial, TexturedMaterial, Tri } from '../mesh';
import { Vector3 } from '../vector';
import { UV, ASSERT, RGB, CustomError, REGEX_NUMBER, RegExpBuilder, REGEX_NZ_ANY, LOG_ERROR } from '../util';
import { UV, ASSERT, RGB, AppError, REGEX_NUMBER, RegExpBuilder, REGEX_NZ_ANY, LOG_ERROR } from '../util';
import { checkFractional, checkNaN } from '../math';
import fs from 'fs';
import path from 'path';
import { StatusHandler } from '../status';
export class ObjImporter extends IImporter {
private _vertices: Vector3[] = [];
@ -106,7 +107,7 @@ export class ObjImporter extends IImporter {
if (vertices.length < 3) {
// this.addWarning('')
// throw new CustomError('Face data should have at least 3 vertices');
// throw new AppError('Face data should have at least 3 vertices');
}
const points: {
@ -118,37 +119,37 @@ export class ObjImporter extends IImporter {
for (const vertex of vertices) {
const vertexData = vertex.split('/');
switch (vertexData.length) {
case 1: {
const index = parseInt(vertexData[0]);
points.push({
positionIndex: index,
texcoordIndex: index,
normalIndex: index,
});
break;
}
case 2: {
const positionIndex = parseInt(vertexData[0]);
const texcoordIndex = parseInt(vertexData[1]);
points.push({
positionIndex: positionIndex,
texcoordIndex: texcoordIndex,
});
break;
}
case 3: {
const positionIndex = parseInt(vertexData[0]);
const texcoordIndex = parseInt(vertexData[1]);
const normalIndex = parseInt(vertexData[2]);
points.push({
positionIndex: positionIndex,
texcoordIndex: texcoordIndex,
normalIndex: normalIndex,
});
break;
}
default:
throw new CustomError(`Face data has unexpected number of vertex data: ${vertexData.length}`);
case 1: {
const index = parseInt(vertexData[0]);
points.push({
positionIndex: index,
texcoordIndex: index,
normalIndex: index,
});
break;
}
case 2: {
const positionIndex = parseInt(vertexData[0]);
const texcoordIndex = parseInt(vertexData[1]);
points.push({
positionIndex: positionIndex,
texcoordIndex: texcoordIndex,
});
break;
}
case 3: {
const positionIndex = parseInt(vertexData[0]);
const texcoordIndex = parseInt(vertexData[1]);
const normalIndex = parseInt(vertexData[2]);
points.push({
positionIndex: positionIndex,
texcoordIndex: texcoordIndex,
normalIndex: normalIndex,
});
break;
}
default:
throw new AppError(`Face data has unexpected number of vertex data: ${vertexData.length}`);
}
}
@ -244,7 +245,7 @@ export class ObjImporter extends IImporter {
this._parseOBJ(filePath);
if (this._mtlLibs.length === 0) {
this.addWarning('Could not find associated .mtl file');
StatusHandler.Get.add('warning', 'Could not find associated .mtl file');
}
for (let i = 0; i < this._mtlLibs.length; ++i) {
const mtlLib = this._mtlLibs[i];
@ -263,11 +264,11 @@ export class ObjImporter extends IImporter {
private _parseOBJ(path: string) {
if (!fs.existsSync(path)) {
throw new CustomError(`Could not find ${path}`);
throw new AppError(`Could not find ${path}`);
}
const fileContents = fs.readFileSync(path, 'utf8');
if (fileContents.includes('<27>')) {
throw new CustomError(`Unrecognised character found, please encode <b>${path}</b> using UTF-8`);
throw new AppError(`Unrecognised character found, please encode <b>${path}</b> using UTF-8`);
}
fileContents.replace('\r', ''); // Convert Windows carriage return
@ -288,8 +289,8 @@ export class ObjImporter extends IImporter {
parser.delegate(match.groups);
} catch (error) {
LOG_ERROR('Caught', error);
if (error instanceof CustomError) {
throw new CustomError(`Failed attempt to parse '${line}', because '${error.message}'`);
if (error instanceof AppError) {
throw new AppError(`Failed attempt to parse '${line}', because '${error.message}'`);
}
}
return;
@ -300,14 +301,14 @@ export class ObjImporter extends IImporter {
return line.startsWith(token);
});
if (beginsWithEssentialToken) {
throw new CustomError(`Failed to parse essential token for <b>${line}</b>`);
throw new AppError(`Failed to parse essential token for <b>${line}</b>`);
}
}
private _parseMTL() {
for (const mtlLib of this._mtlLibs) {
if (!fs.existsSync(mtlLib)) {
throw new CustomError(`Could not find ${mtlLib}`);
throw new AppError(`Could not find ${mtlLib}`);
}
const fileContents = fs.readFileSync(mtlLib, 'utf8');
@ -331,8 +332,8 @@ export class ObjImporter extends IImporter {
try {
parser.delegate(match.groups);
} catch (error) {
if (error instanceof CustomError) {
throw new CustomError(`Failed attempt to parse '${line}', because '${error.message}'`);
if (error instanceof AppError) {
throw new AppError(`Failed attempt to parse '${line}', because '${error.message}'`);
}
}
return;
@ -343,7 +344,7 @@ export class ObjImporter extends IImporter {
return line.startsWith(token);
});
if (beginsWithEssentialToken) {
throw new CustomError(`Failed to parse essential token for ${line}`);
throw new AppError(`Failed to parse essential token for ${line}`);
}
}

View File

@ -1,4 +1,4 @@
import { CustomError, LOG_ERROR } from './util';
import { AppError, LOG_ERROR } from './util';
import { Vector3 } from './vector';
@ -48,7 +48,7 @@ export const checkNaN = (...args: number[]) => {
});
if (existsNaN) {
LOG_ERROR(args);
throw new CustomError('Found NaN');
throw new AppError('Found NaN');
}
};
@ -60,7 +60,7 @@ export const checkFractional = (...args: number[]) => {
return arg > 1.0 || arg < 0.0;
});
if (existsOutside) {
throw new CustomError('Found value outside of [0, 1]');
throw new AppError('Found value outside of [0, 1]');
}
};

View File

@ -1,11 +1,12 @@
import { Vector3 } from './vector';
import { UV, Bounds, ASSERT, CustomError, LOG_WARN, Warnable, getRandomID } from './util';
import { UV, Bounds, ASSERT, AppError, LOG_WARN, getRandomID } from './util';
import { Triangle, UVTriangle } from './triangle';
import { RGB } from './util';
import path from 'path';
import fs from 'fs';
import { Texture, TextureFiltering } from './texture';
import { StatusHandler } from './status';
interface VertexIndices {
x: number;
@ -27,7 +28,7 @@ export interface SolidMaterial { colour: RGB; type: MaterialType.solid }
export interface TexturedMaterial { path: string; type: MaterialType.textured }
export type MaterialMap = {[key: string]: (SolidMaterial | TexturedMaterial)};
export class Mesh extends Warnable {
export class Mesh {
public readonly id: string;
public _vertices: Vector3[];
@ -40,7 +41,6 @@ export class Mesh extends Warnable {
public static desiredHeight = 8.0;
constructor(vertices: Vector3[], normals: Vector3[], uvs: UV[], tris: Tri[], materials: MaterialMap) {
super();
this.id = getRandomID();
this._vertices = vertices;
@ -86,17 +86,25 @@ export class Mesh extends Warnable {
// TODO: Check indices exist
if (this._vertices.length === 0) {
throw new CustomError('No vertices were loaded');
throw new AppError('No vertices were loaded');
}
if (this._tris.length === 0) {
throw new CustomError('No triangles were loaded');
throw new AppError('No triangles were loaded');
}
if (this._tris.length >= 100_000) {
this.addWarning(`The imported mesh has ${this._tris.length} triangles, consider simplifying it in a DDC such as Blender`);
StatusHandler.Get.add(
'warning',
`The imported mesh has ${this._tris.length.toLocaleString()} triangles, consider simplifying it in a DDC such as Blender`,
);
}
StatusHandler.Get.add(
'info',
`${this._vertices.length.toLocaleString()} vertices, ${this._tris.length.toLocaleString()} triangles`,
);
// Give warning if normals are not defined
let giveNormalsWarning = false;
for (let triIndex = 0; triIndex < this.getTriangleCount(); ++triIndex) {
@ -116,13 +124,16 @@ export class Mesh extends Warnable {
}
}
if (giveNormalsWarning) {
this.addWarning('Some vertices do not have their normals defined, this may cause voxels to be aligned incorrectly');
}
StatusHandler.Get.add(
'warning',
'Some vertices do not have their normals defined, this may cause voxels to be aligned incorrectly',
);
};
}
private _checkMaterials() {
if (Object.keys(this._materials).length === 0) {
throw new CustomError('Loaded mesh has no materials');
throw new AppError('Loaded mesh has no materials');
}
// Check used materials exist
@ -142,7 +153,10 @@ export class Mesh extends Warnable {
}
if (wasRemapped) {
LOG_WARN('Triangles use these materials but they were not found', missingMaterials);
this.addWarning('Some materials were not loaded correctly');
StatusHandler.Get.add(
'warning',
'Some materials were not loaded correctly',
);
this._materials[debugName] = {
type: MaterialType.solid,
colour: RGB.white,
@ -155,7 +169,10 @@ export class Mesh extends Warnable {
if (material.type === MaterialType.textured) {
ASSERT(path.isAbsolute(material.path), 'Material texture path not absolute');
if (!fs.existsSync(material.path)) {
this.addWarning(`Could not find ${material.path}`);
StatusHandler.Get.add(
'warning',
`Could not find ${material.path}`,
);
LOG_WARN(`Could not find ${material.path} for material ${materialName}, changing to solid-white material`);
this._materials[materialName] = {
type: MaterialType.solid,
@ -185,7 +202,7 @@ export class Mesh extends Warnable {
const centre = this.getBounds().getCentre();
if (!centre.isNumber()) {
throw new CustomError('Could not find centre of mesh');
throw new AppError('Could not find centre of mesh');
}
// Translate each triangle
@ -198,7 +215,7 @@ export class Mesh extends Warnable {
const scaleFactor = Mesh.desiredHeight / size.y;
if (isNaN(scaleFactor) || !isFinite(scaleFactor)) {
throw new CustomError('<b>Could not scale mesh correctly</b>: Mesh is likely 2D, rotate it so that it has a non-zero height');
throw new AppError('Could not scale mesh correctly - mesh is likely 2D, rotate it so that it has a non-zero height');
} else {
this.scaleMesh(scaleFactor);
}

View File

@ -89,15 +89,15 @@ export class Renderer {
this._setupScene();
switch (this._meshToUse) {
case MeshType.TriangleMesh:
this._drawMesh();
break;
case MeshType.VoxelMesh:
this._drawVoxelMesh();
break;
case MeshType.BlockMesh:
this._drawBlockMesh();
break;
case MeshType.TriangleMesh:
this._drawMesh();
break;
case MeshType.VoxelMesh:
this._drawVoxelMesh();
break;
case MeshType.BlockMesh:
this._drawBlockMesh();
break;
};
this._drawDebug();

View File

@ -4,9 +4,10 @@ import path from 'path';
import { NBT, TagType, writeUncompressed } from 'prismarine-nbt';
import { Vector3 } from './vector';
import { BlockMesh } from './block_mesh';
import { RESOURCES_DIR, Warnable } from './util';
import { RESOURCES_DIR } from './util';
import { StatusHandler } from './status';
export abstract class IExporter extends Warnable {
export abstract class IExporter {
protected _sizeVector!: Vector3;
public abstract convertToNBT(blockMesh: BlockMesh): NBT
@ -70,8 +71,10 @@ export class Schematic extends IExporter {
}
if (unsupportedBlocks.size > 0) {
this.addWarning(`${numBlocksUnsupported} blocks (${unsupportedBlocks.size} unique) are not supported by the .schematic format, Stone block are used in their place. Try using the schematic-friendly palette, or export using .litematica`);
// LOG('Unsupported blocks', unsupportedBlocks);
StatusHandler.Get.add(
'warning',
`${numBlocksUnsupported} blocks (${unsupportedBlocks.size} unique) are not supported by the .schematic format, Stone block are used in their place. Try using the schematic-friendly palette, or export using .litematica`,
);
}
const nbt: NBT = {

75
src/status.ts Normal file
View File

@ -0,0 +1,75 @@
import { EAction } from './app_context';
import { LOG, LOG_WARN } from './util';
export type StatusType = 'warning' | 'info';
export type StatusMessage = {
status: StatusType,
message: string,
}
export class StatusHandler {
/** Singleton accessor */
private static _instance: StatusHandler;
public static get Get() {
return this._instance || (this._instance = new this());
}
private _statusMessages: StatusMessage[];
private constructor() {
this._statusMessages = [];
}
public clear() {
this._statusMessages = [];
}
public add(status: StatusType, message: string) {
(status === 'warning' ? LOG_WARN : LOG)(message);
this._statusMessages.push({ status: status, message: message });
}
public hasStatusMessages(statusType: StatusType): boolean {
return this.getStatusMessages(statusType).length > 0;
}
public getStatusMessages(statusType: StatusType): string[] {
const messagesToReturn = (statusType !== undefined) ? this._statusMessages.filter((m) => m.status === statusType ): this._statusMessages;
return messagesToReturn.map((m) => m.message);
}
public getDefaultSuccessMessage(action: EAction): string {
switch (action) {
case EAction.Import:
return 'Successfully loaded mesh';
case EAction.Simplify:
return 'Successfully simplified mesh';
case EAction.Voxelise:
return 'Successfully voxelised mesh';
case EAction.Palette:
return 'Successfully assigned blocks';
case EAction.Export:
return 'Successfully exported mesh';
default:
return 'Successfully performed action';
}
}
public getDefaultFailureMessage(action: EAction): string {
switch (action) {
case EAction.Import:
return 'Failed to load mesh';
case EAction.Simplify:
return 'Failed to simplify mesh';
case EAction.Voxelise:
return 'Failed to voxelise mesh';
case EAction.Palette:
return 'Failed to assign blocks';
case EAction.Export:
return 'Failed to export mesh';
default:
return 'Failed to perform action';
}
}
}

View File

@ -1,4 +1,4 @@
import { UV, ASSERT, CustomError, LOG } from './util';
import { UV, ASSERT, AppError, LOG } from './util';
import { RGB } from './util';
import * as fs from 'fs';
@ -39,13 +39,13 @@ export class Texture {
} else if (['.jpg', '.jpeg'].includes(filePath.ext.toLowerCase())) {
this._image = jpeg.decode(data);
} else {
throw new CustomError(`Failed to load: ${filename}`);
throw new AppError(`Failed to load ${filename}`);
}
if (this._image.width * this._image.height * 4 !== this._image.data.length) {
throw new CustomError(`Unexpected image resolution mismatch: ${filename}`);
throw new AppError(`Unexpected image resolution mismatch: ${filename}`);
}
} catch (err) {
throw new CustomError(`Could not read ${filename}`);
throw new AppError(`Could not read ${filename}`);
}
}

View File

@ -1,5 +1,7 @@
import { ASSERT } from '../../util';
import { ActionReturnType } from '../../app_context';
import { UIMessageBuilder } from '../misc';
export type OutputStyle = 'success' | 'warning' | 'error';
export class OutputElement {
private _id: string;
@ -25,18 +27,22 @@ export class OutputElement {
element.classList.remove('border-error');
}
public setMessage(message: string, returnType: ActionReturnType) {
public setMessage(message: UIMessageBuilder, style: OutputStyle) {
const element = document.getElementById(this._id) as HTMLDivElement;
ASSERT(element !== null);
this.clearMessage();
element.innerHTML = message;
if (returnType === ActionReturnType.Success) {
element.classList.add('border-success');
} else if (returnType === ActionReturnType.Warning) {
element.classList.add('border-warning');
} else if (returnType === ActionReturnType.Failure) {
element.classList.add('border-error');
element.innerHTML = message.toString();
switch (style) {
case 'success':
element.classList.add('border-success');
break;
case 'warning':
element.classList.add('border-warning');
break;
case 'error':
element.classList.add('border-error');
break;
}
}
}

View File

@ -4,7 +4,7 @@ import { ComboBoxElement, ComboBoxItem } from './elements/combobox';
import { FileInputElement } from './elements/file_input';
import { ButtonElement } from './elements/button';
import { OutputElement } from './elements/output';
import { Action, AppContext } from '../app_context';
import { EAction, AppContext } from '../app_context';
import { ASSERT, ATLASES_DIR, LOG, PALETTES_DIR } from '../util';
import fs from 'fs';
@ -38,7 +38,7 @@ export class UI {
},
elementsOrder: ['input'],
submitButton: new ButtonElement('Load mesh', () => {
this._appContext.do(Action.Import);
this._appContext.do(EAction.Import);
}),
output: new OutputElement(),
},
@ -49,7 +49,7 @@ export class UI {
},
elementsOrder: ['ratio'],
submitButton: new ButtonElement('Simplify mesh', () => {
this._appContext.do(Action.Simplify);
this._appContext.do(EAction.Simplify);
}),
output: new OutputElement(),
},
@ -77,7 +77,7 @@ export class UI {
},
elementsOrder: ['height', 'voxeliser', 'ambientOcclusion', 'multisampleColouring', 'textureFiltering'],
submitButton: new ButtonElement('Voxelise mesh', () => {
this._appContext.do(Action.Voxelise);
this._appContext.do(EAction.Voxelise);
}),
output: new OutputElement(),
},
@ -97,7 +97,7 @@ export class UI {
},
elementsOrder: ['textureAtlas', 'blockPalette', 'dithering', 'colourSpace'],
submitButton: new ButtonElement('Assign blocks', () => {
this._appContext.do(Action.Palette);
this._appContext.do(EAction.Palette);
}),
output: new OutputElement(),
},
@ -111,7 +111,7 @@ export class UI {
},
elementsOrder: ['export'],
submitButton: new ButtonElement('Export structure', () => {
this._appContext.do(Action.Export);
this._appContext.do(EAction.Export);
}),
output: new OutputElement(),
},
@ -296,8 +296,8 @@ export class UI {
document.getElementById('toolbar')!.innerHTML = toolbarHTML;
}
public cacheValues(action: Action) {
const group = this._getActionGroup(action);
public cacheValues(action: EAction) {
const group = this._getEActionGroup(action);
for (const elementName of group.elementsOrder) {
LOG(`Caching ${elementName}`);
const element = group.elements[elementName];
@ -386,23 +386,23 @@ export class UI {
return this._uiDull;
}
public enable(action: Action) {
if (action >= Action.MAX) {
public enable(action: EAction) {
if (action >= EAction.MAX) {
return;
}
LOG('enabling', action);
// TODO: Remove once Simplify has been implemented
if (action === Action.Simplify) {
action = Action.Voxelise;
if (action === EAction.Simplify) {
action = EAction.Voxelise;
}
const group = this._getActionGroup(action);
const group = this._getEActionGroup(action);
for (const compName in group.elements) {
group.elements[compName].setEnabled(true);
}
group.submitButton.setEnabled(true);
// Enable the post elements of the previous group
const prevGroup = this._getActionGroup(action - 1);
const prevGroup = this._getEActionGroup(action - 1);
if (prevGroup && prevGroup.postElements) {
ASSERT(prevGroup.postElementsOrder);
for (const postElementName in prevGroup.postElements) {
@ -411,13 +411,13 @@ export class UI {
}
}
public disable(action: Action) {
public disable(action: EAction) {
if (action < 0) {
return;
}
for (let i = action; i < Action.MAX; ++i) {
const group = this._getActionGroup(i);
for (let i = action; i < EAction.MAX; ++i) {
const group = this._getEActionGroup(i);
LOG('disabling', group.label);
for (const compName in group.elements) {
group.elements[compName].setEnabled(false);
@ -434,7 +434,7 @@ export class UI {
}
}
// Disable the post elements of the previous group
const prevGroup = this._getActionGroup(action - 1);
const prevGroup = this._getEActionGroup(action - 1);
if (prevGroup && prevGroup.postElements) {
ASSERT(prevGroup.postElementsOrder);
for (const postElementName in prevGroup.postElements) {
@ -443,7 +443,7 @@ export class UI {
}
}
private _getActionGroup(action: Action): Group {
private _getEActionGroup(action: EAction): Group {
const key = this.uiOrder[action];
return this._uiDull[key];
}

27
src/ui/misc.ts Normal file
View File

@ -0,0 +1,27 @@
export class UIMessageBuilder {
private _messages: string[];
public constructor() {
this._messages = [];
}
public addHeading(message: string) {
this.addBold(message + ':');
}
public addBold(...messages: string[]) {
for (const message of messages) {
this._messages.push(`<b>${message}</b>`);
}
}
public add(...messages: string[]) {
for (const message of messages) {
this._messages.push(message);
}
}
public toString(): string {
return this._messages.join('<br>');
}
}

View File

@ -207,17 +207,10 @@ export function buildRegex(...args: (string | RegExp)[]) {
}).join(''));
}
export class CustomError extends Error {
export class AppError extends Error {
constructor(msg: string) {
super(msg);
Object.setPrototypeOf(this, CustomError.prototype);
}
}
export class CustomWarning extends Error {
constructor(msg: string) {
super(msg);
Object.setPrototypeOf(this, CustomWarning.prototype);
Object.setPrototypeOf(this, AppError.prototype);
}
}
@ -272,22 +265,6 @@ export class RegExpBuilder {
}
}
export class Warnable {
private _warnings: string[];
constructor() {
this._warnings = [];
}
public addWarning(warning: string) {
this._warnings.push('- ' + warning);
}
public getWarnings() {
return this._warnings;
}
}
export const BASE_DIR = path.join(__dirname, '/../../');
export const RESOURCES_DIR = path.join(BASE_DIR, './res/');
export const ATLASES_DIR = path.join(RESOURCES_DIR, './atlases');

31
tests/status.test.ts Normal file
View File

@ -0,0 +1,31 @@
import { StatusHandler } from '../src/status';
test('Status', () => {
StatusHandler.Get.add(
'warning',
'This is a warning',
);
expect(StatusHandler.Get.getStatusMessages('warning').length).toBe(1);
StatusHandler.Get.clear();
expect(StatusHandler.Get.getStatusMessages('warning').length).toBe(0);
});
test('Status', () => {
StatusHandler.Get.add(
'warning',
'This is a warning',
);
StatusHandler.Get.add(
'info',
'This is some info',
);
StatusHandler.Get.add(
'info',
'This is some more info',
);
expect(StatusHandler.Get.getStatusMessages( 'info').length).toBe(2);
expect(StatusHandler.Get.getStatusMessages( 'warning').length).toBe(1);
StatusHandler.Get.clear();
expect(StatusHandler.Get.getStatusMessages( 'info').length).toBe(0);
expect(StatusHandler.Get.getStatusMessages( 'warning').length).toBe(0);
});

View File

@ -56,29 +56,30 @@ interface ExportParams {
exporter: IExporter;
}
// TODO: Log status messages
function _import(params: ImportParams): Mesh {
log(LogStyle.Info, 'Importing...');
const importer = new ObjImporter();
importer.parseFile(params.absoluteFilePathLoad);
for (const warning of importer.getWarnings()) {
log(LogStyle.Warning, warning);
}
const mesh = importer.toMesh();
mesh.processMesh();
return mesh;
}
// TODO: Log status messages
function _voxelise(mesh: Mesh, params: VoxeliseParams): VoxelMesh {
log(LogStyle.Info, 'Voxelising...');
const voxeliser: IVoxeliser = params.voxeliser;
return voxeliser.voxelise(mesh, params.voxelMeshParams);
}
// TODO: Log status messages
function _palette(voxelMesh: VoxelMesh, params: PaletteParams): BlockMesh {
log(LogStyle.Info, 'Assigning blocks...');
return BlockMesh.createFromVoxelMesh(voxelMesh, params.blockMeshParams);
}
// TODO: Log status messages
function _export(blockMesh: BlockMesh, params: ExportParams) {
log(LogStyle.Info, 'Exporting...');
params.exporter.export(blockMesh, params.absoluteFilePathSave);