forked from mirror/ObjToSchematic
New warning handler, minor refactor
This commit is contained in:
parent
335f6ed984
commit
081b602493
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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]');
|
||||
}
|
||||
};
|
||||
|
||||
|
43
src/mesh.ts
43
src/mesh.ts
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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
75
src/status.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
27
src/ui/misc.ts
Normal 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>');
|
||||
}
|
||||
}
|
27
src/util.ts
27
src/util.ts
@ -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
31
tests/status.test.ts
Normal 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);
|
||||
});
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user