Rewrote parser, again, and misc fixes

This commit is contained in:
Lucas Dower 2022-02-27 23:08:53 +00:00
parent 39a83f9d0c
commit 78936d82f2
11 changed files with 391 additions and 115 deletions

View File

@ -7,7 +7,7 @@
"node": ">=14.0.0"
},
"scripts": {
"lint": "eslint --fix ./src/**/*.ts && eslint --fix ./src/**/*.ts",
"lint": "eslint --fix ./src/**/*.ts && eslint --fix ./tools/**/*.ts",
"debug": "tsc && electron ./dist/main.js --enable-logging",
"build": "npm run lint && tsc",
"start": "npm run build && electron ./dist/main.js --enable-logging",
@ -49,11 +49,9 @@
"eslint-config-google": "^0.14.0",
"expand-vertex-data": "^1.1.2",
"jpeg-js": "^0.4.3",
"mtltojs": "^0.2.1",
"obj-file-parser": "^0.5.3",
"pngjs": "^6.0.0",
"prismarine-nbt": "^1.6.0",
"twgl.js": "^4.19.1",
"wavefront-obj-parser": "^2.0.1"
"twgl.js": "^4.19.1"
}
}

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, CustomError, CustomWarning, LOG, LOG_ERROR } from './util';
import { ASSERT, CustomError, CustomWarning, LOG, LOG_ERROR, LOG_WARN } from './util';
import { remote } from 'electron';
import { VoxelMesh } from './voxel_mesh';
@ -131,7 +131,7 @@ export class AppContext {
const successMessage = ReturnMessages.get(action)!.onSuccess;
if (this._warnings.length !== 0) {
const allWarnings = this._warnings.join('<br>');
UI.Get.layoutDull[groupName].output.setMessage(successMessage + ', with warnings:' + '<br><b>' + allWarnings + '</b>', ActionReturnType.Warning);
UI.Get.layoutDull[groupName].output.setMessage(successMessage + `, with ${this._warnings.length} warning(s):` + '<br><b>' + allWarnings + '</b>', ActionReturnType.Warning);
} else {
UI.Get.layoutDull[groupName].output.setMessage(successMessage, ActionReturnType.Success);
}
@ -197,4 +197,9 @@ export class AppContext {
public getLoadedMesh() {
return this._loadedMesh;
}
public addWarning(warning: string) {
LOG_WARN(warning);
this._warnings.push(warning);
}
}

View File

@ -9,11 +9,6 @@ export class GeometryTemplates {
static getTriangleBufferData(triangle: UVTriangle): VoxelData {
const n = triangle.getNormal();
const uv0u = triangle.uv0.u;
if (isNaN(uv0u)) {
throw Error('oh no');
}
return {
custom: {
position: [

View File

@ -1,120 +1,292 @@
import * as fs from 'fs';
import path from 'path';
const OBJFile = require('obj-file-parser');
const mtlParser = require('mtltojs');
import { IImporter } from '../importer';
import { MaterialType, Mesh, SolidMaterial, TexturedMaterial, Tri } from '../mesh';
import { Vector3 } from '../vector';
import { UV, ASSERT, RGB, LOG_WARN } from '../util';
import { UV, ASSERT, RGB, CustomError, LOG } from '../util';
import { UI } from '../ui/layout';
import { checkFractional, checkNaN } from '../math';
import fs from 'fs';
import path from 'path';
import { AppContext } from '../app_context';
export class ObjImporter extends IImporter {
private _vertices!: Vector3[];
private _uvs!: UV[];
private _tris!: Tri[];
private _materials!: {[key: string]: (SolidMaterial | TexturedMaterial)};
private _mtlLibs!: string[];
private _vertices: Vector3[] = [];
private _uvs: UV[] = [];
private _tris: Tri[] = [];
private _materials: {[key: string]: (SolidMaterial | TexturedMaterial)} = {};
private _mtlLibs: string[] = [];
private _currentMaterialName: string = '';
private _objPath?: path.ParsedPath;
private _objParsers = [
{
regex: /mtllib (?<path>.*\.mtl)/,
delegate: (match: { [key: string]: string }) => {
this._mtlLibs.push(match.path);
},
},
{
regex: /usemtl (?<name>.*)/,
delegate: (match: { [key: string]: string }) => {
this._currentMaterialName = match.name;
},
},
{
regex: /v (?<x>.*) (?<y>.*) (?<z>.*)/,
delegate: (match: { [key: string]: string }) => {
const x = parseFloat(match.x);
const y = parseFloat(match.y);
const z = parseFloat(match.z);
checkNaN(x, y, z);
this._vertices.push(new Vector3(x, y, z));
},
},
{
regex: /vt (?<u>.*) (?<v>.*)/,
delegate: (match: { [key: string]: string }) => {
const u = parseFloat(match.u);
const v = parseFloat(match.v);
checkNaN(u, v);
this._uvs.push(new UV(u, v));
},
},
{
regex: /f (?<ix>.*)\/(?<iuvx>.*)\/.* (?<iy>.*)\/(?<iuvy>.*)\/.* (?<iz>.*)\/(?<iuvz>.*)\/.* (?<iw>.*)\/(?<iuvw>.*)\//,
delegate: (match: { [key: string]: string }) => {
const iX = parseInt(match.ix) - 1;
const iY = parseInt(match.iy) - 1;
const iZ = parseInt(match.iz) - 1;
const iW = parseInt(match.iw) - 1;
const iUVx = parseInt(match.iuvx) - 1;
const iUVy = parseInt(match.iuvy) - 1;
const iUVz = parseInt(match.iuvz) - 1;
const iUVw = parseInt(match.iuvw) - 1;
checkNaN(iX, iY, iZ, iW);
ASSERT(this._currentMaterialName);
this._tris.push({
iX: iW,
iY: iY,
iZ: iX,
iXUV: iUVw,
iYUV: iUVy,
iZUV: iUVx,
material: this._currentMaterialName,
});
this._tris.push({
iX: iW,
iY: iZ,
iZ: iY,
iXUV: iUVw,
iYUV: iUVz,
iZUV: iUVy,
material: this._currentMaterialName,
});
},
},
{
regex: /f (?<ix>.*)\/(?<iuvx>.*)\/.* (?<iy>.*)\/(?<iuvy>.*)\/.* (?<iz>.*)\/(?<iuvz>.*)\//,
delegate: (match: { [key: string]: string }) => {
const iX = parseInt(match.ix) - 1;
const iY = parseInt(match.iy) - 1;
const iZ = parseInt(match.iz) - 1;
const iUVx = parseInt(match.iuvx) - 1;
const iUVy = parseInt(match.iuvy) - 1;
const iUVz = parseInt(match.iuvz) - 1;
checkNaN(iX, iY, iZ);
ASSERT(this._currentMaterialName);
this._tris.push({
iX: iX,
iY: iY,
iZ: iZ,
iXUV: iUVx,
iYUV: iUVy,
iZUV: iUVz,
material: this._currentMaterialName,
});
},
},
{
regex: /f (?<ix>.*) (?<iy>.*) (?<iz>.*)/,
delegate: (match: { [key: string]: string }) => {
const iX = parseInt(match.ix) - 1;
const iY = parseInt(match.iy) - 1;
const iZ = parseInt(match.iz) - 1;
checkNaN(iX, iY, iZ);
ASSERT(this._currentMaterialName);
this._tris.push({
iX: iX,
iY: iY,
iZ: iZ,
iXUV: iX,
iYUV: iY,
iZUV: iZ,
material: this._currentMaterialName,
});
},
},
];
private _currentColour: RGB = RGB.black;
private _currentTexture: string = '';
private _materialReady: boolean = false;
private _mtlParsers = [
{
regex: /newmtl (?<name>.*)/,
delegate: (match: { [key: string]: string }) => {
this._addCurrentMaterial();
this._currentMaterialName = match.name;
this._currentTexture = '';
this._materialReady = false;
},
},
{
regex: /Kd (?<r>.*) (?<g>.*) (?<b>.*)/,
delegate: (match: { [key: string]: string }) => {
const r = parseFloat(match.r);
const g = parseFloat(match.g);
const b = parseFloat(match.b);
checkNaN(r, g, b);
checkFractional(r, g, b);
this._currentColour = new RGB(r, g, b);
this._materialReady = true;
},
},
{
regex: /map_Kd (?<path>.*)/,
delegate: (match: { [key: string]: string }) => {
let mtlPath = match.path;
if (!path.isAbsolute(mtlPath)) {
ASSERT(this._objPath);
mtlPath = path.join(this._objPath.dir, mtlPath);
}
this._currentTexture = mtlPath;
this._materialReady = true;
},
},
];
override createMesh(): Mesh {
const filePath = UI.Get.layout.import.elements.input.getCachedValue();
ASSERT(path.isAbsolute(filePath));
this._objPath = path.parse(filePath);
this._parseOBJ(filePath);
ASSERT(this._mtlLibs.length > 0);
if (this._mtlLibs.length > 1) {
LOG_WARN('Multiple mtl libs found, only the first will be used');
if (this._mtlLibs.length === 0) {
AppContext.Get.addWarning('Could not find associated .mtl file');
}
let mtlPath = this._mtlLibs[0];
if (!path.isAbsolute(mtlPath)) {
const objPath = path.parse(filePath);
mtlPath = path.join(objPath.dir, mtlPath);
for (let i = 0; i < this._mtlLibs.length; ++i) {
const mtlLib = this._mtlLibs[i];
if (!path.isAbsolute(mtlLib)) {
this._mtlLibs[i] = path.join(this._objPath.dir, mtlLib);
}
ASSERT(path.isAbsolute(this._mtlLibs[i]));
}
ASSERT(path.isAbsolute(mtlPath));
this._parseMTL(mtlPath);
this._parseMTL();
LOG(this);
return new Mesh(this._vertices, this._uvs, this._tris, this._materials);
}
private _parseOBJ(path: string) {
const fileContents = fs.readFileSync(path, 'utf-8');
const objFile = new OBJFile(fileContents);
const output = objFile.parse();
if (!fs.existsSync(path)) {
throw new CustomError(`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`);
}
this._mtlLibs = output.materialLibraries;
fileContents.replace('\r', ''); // Convert Windows carriage return
const fileLines = fileContents.split('\n');
for (const modelName in output.models) {
const model = output.models[modelName];
// Fill vertices
this._vertices = [];
for (const vertex of model.vertices) {
this._vertices.push(new Vector3(vertex.x, vertex.y, vertex.z));
}
// Fill UVs
this._uvs = [];
for (const uv of model.textureCoords) {
this._uvs.push({ u: uv.u, v: uv.v });
}
// Fill tris
this._tris = [];
for (const tri of model.faces) {
if (tri.vertices.length === 3) {
const iX = tri.vertices[0].vertexIndex - 1;
const iXUV = tri.vertices[0].textureCoordsIndex - 1;
const iY = tri.vertices[1].vertexIndex - 1;
const iYUV = tri.vertices[1].textureCoordsIndex - 1;
const iZ = tri.vertices[2].vertexIndex - 1;
const iZUV = tri.vertices[2].textureCoordsIndex - 1;
const material = tri.material;
this._tris.push({
iX: iX, iY: iY, iZ: iZ,
iXUV: iXUV, iYUV: iYUV, iZUV: iZUV,
material: material,
});
} else if (tri.vertices.length === 4) {
const iX = tri.vertices[0].vertexIndex - 1;
const iXUV = tri.vertices[0].textureCoordsIndex - 1;
const iY = tri.vertices[1].vertexIndex - 1;
const iYUV = tri.vertices[1].textureCoordsIndex - 1;
const iZ = tri.vertices[2].vertexIndex - 1;
const iZUV = tri.vertices[2].textureCoordsIndex - 1;
const iW = tri.vertices[3].vertexIndex - 1;
const iWUV = tri.vertices[3].textureCoordsIndex - 1;
const material = tri.material;
this._tris.push({
iX: iW, iY: iY, iZ: iX,
iXUV: iWUV, iYUV: iYUV, iZUV: iXUV,
material: material,
});
this._tris.push({
iX: iW, iY: iZ, iZ: iY,
iXUV: iWUV, iYUV: iZUV, iZUV: iYUV,
material: material,
});
}
}
for (const line of fileLines) {
this._parseOBJLine(line);
}
}
private _parseMTL(mtlPath: string) {
const output = mtlParser.parseSync(mtlPath);
const materials = output.data.data.material;
private _parseOBJLine(line: string) {
const essentialTokens = ['mtllib ', 'uselib ', 'v ', 'vt ', 'f '];
this._materials = {};
for (const material of materials) {
if (material?.texture_map?.diffuse) {
let texPath = material.texture_map.diffuse.file;
if (!path.isAbsolute(texPath)) {
const parsedPath = path.parse(mtlPath);
texPath = path.join(parsedPath.dir, texPath);
for (const parser of this._objParsers) {
const match = parser.regex.exec(line);
if (match && match.groups) {
try {
parser.delegate(match.groups);
} catch (error) {
if (error instanceof CustomError) {
throw new CustomError(`Failed attempt to parse '${line}', because '${error.message}'`);
}
}
this._materials[material.name] = { type: MaterialType.textured, path: texPath };
} else if (material?.diffuse) {
const rgb = material.diffuse.vals;
this._materials[material.name] = { type: MaterialType.solid, colour: RGB.fromArray(rgb) };
return;
}
}
const beginsWithEssentialToken = essentialTokens.some((token) => {
return line.startsWith(token);
});
if (beginsWithEssentialToken) {
throw new CustomError(`Failed to parse essential token for ${line}`);
}
}
private _parseMTL() {
for (const mtlLib of this._mtlLibs) {
if (!fs.existsSync(mtlLib)) {
throw new CustomError(`Could not find ${mtlLib}`);
}
const fileContents = fs.readFileSync(mtlLib, 'utf8');
fileContents.replace('\r', ''); // Convert Windows carriage return
const fileLines = fileContents.split('\n');
for (const line of fileLines) {
this._parseMTLLine(line);
}
this._addCurrentMaterial();
}
}
private _parseMTLLine(line: string) {
const essentialTokens = ['newmtl ', 'Kd ', 'map_Kd '];
for (const parser of this._mtlParsers) {
const match = parser.regex.exec(line);
if (match && match.groups) {
try {
parser.delegate(match.groups);
} catch (error) {
if (error instanceof CustomError) {
throw new CustomError(`Failed attempt to parse '${line}', because '${error.message}'`);
}
}
return;
}
}
const beginsWithEssentialToken = essentialTokens.some((token) => {
return line.startsWith(token);
});
if (beginsWithEssentialToken) {
throw new CustomError(`Failed to parse essential token for ${line}`);
}
}
private _addCurrentMaterial() {
if (this._materialReady && this._currentMaterialName !== '') {
if (this._currentTexture !== '') {
this._materials[this._currentMaterialName] = {
type: MaterialType.textured,
path: this._currentTexture,
};
} else {
ASSERT(false);
this._materials[this._currentMaterialName] = {
type: MaterialType.solid,
colour: this._currentColour,
};
}
}
}

View File

@ -1,3 +1,4 @@
import { CustomError, LOG_ERROR } from './util';
import { Vector3 } from './vector';
@ -38,4 +39,29 @@ export const wayThrough = (value: number, min: number, max: number) => {
return (value - min) / (max - min);
};
/**
* Throws is any number in args is NaN
*/
export const checkNaN = (...args: number[]) => {
const existsNaN = args.some((arg) => {
return isNaN(arg);
});
if (existsNaN) {
LOG_ERROR(args);
throw new CustomError('Found NaN');
}
};
/**
* Throws if any number in not within [0, 1]
*/
export const checkFractional = (...args: number[]) => {
const existsOutside = args.some((arg) => {
return arg > 1.0 || arg < 0.0;
});
if (existsOutside) {
throw new CustomError('Found value outside of [0, 1]');
}
};
export const degreesToRadians = Math.PI / 180;

View File

@ -1,7 +1,11 @@
import { Vector3 } from './vector';
import { UV, Bounds, LOG } from './util';
import { UV, Bounds, LOG, ASSERT, CustomError, LOG_WARN } from './util';
import { Triangle, UVTriangle } from './triangle';
import { RGB } from './util';
import { AppContext } from './app_context';
import path from 'path';
import fs from 'fs';
export interface Tri {
iX: number;
@ -36,8 +40,13 @@ export class Mesh {
this.tris = tris;
this.materials = materials;
this._checkMesh();
this._checkMaterials();
this._centreMesh();
this._scaleMesh();
LOG('Loaded mesh', this);
}
public getBounds() {
@ -48,6 +57,57 @@ export class Mesh {
return bounds;
}
private _checkMesh() {
// Check UVs are inside [0, 1]
for (const uv of this.uvs) {
if (uv.u < 0.0 || uv.u > 1.0) {
uv.u = Math.abs(uv.u % 1);
}
if (uv.v < 0.0 || uv.v > 1.0) {
uv.v = Math.abs(uv.v % 1);
}
}
}
private _checkMaterials() {
// Check used materials exist
let wasRemapped = false;
let debugName = (Math.random() + 1).toString(36).substring(7);
while (debugName in this.materials) {
debugName = (Math.random() + 1).toString(36).substring(7);
}
for (const tri of this.tris) {
if (!(tri.material in this.materials)) {
wasRemapped = true;
tri.material = debugName;
}
}
if (wasRemapped) {
AppContext.Get.addWarning('Some materials were not loaded correctly');
this.materials[debugName] = {
type: MaterialType.solid,
colour: RGB.white,
};
}
// Check texture paths are absolute and exist
for (const materialName in this.materials) {
const material = this.materials[materialName];
if (material.type === MaterialType.textured) {
ASSERT(path.isAbsolute(material.path), 'Material texture path not absolute');
if (!fs.existsSync(material.path)) {
AppContext.Get.addWarning(`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,
colour: RGB.white,
};
}
}
}
}
private _centreMesh() {
const centre = new Vector3(0, 0, 0);
let totalWeight = 0.0;
@ -63,6 +123,11 @@ export class Mesh {
});
centre.divScalar(totalWeight);
if (!centre.isNumber()) {
throw new CustomError('Could not find centre of mesh');
}
LOG('Centre', centre);
// Translate each triangle
this.vertices.forEach((vertex) => {
vertex.sub(centre);
@ -74,9 +139,13 @@ export class Mesh {
const size = Vector3.sub(bounds.max, bounds.min);
const scaleFactor = Mesh.desiredHeight / size.y;
this.vertices.forEach((vertex) => {
vertex.mulScalar(scaleFactor);
});
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');
} else {
this.vertices.forEach((vertex) => {
vertex.mulScalar(scaleFactor);
});
}
}
public getVertices(triIndex: number) {
@ -103,9 +172,9 @@ export class Mesh {
this.vertices[tri.iX],
this.vertices[tri.iY],
this.vertices[tri.iZ],
this.uvs[tri.iXUV],
this.uvs[tri.iYUV],
this.uvs[tri.iZUV],
this.uvs[tri.iXUV] || 0.0,
this.uvs[tri.iYUV] || 0.0,
this.uvs[tri.iZUV] || 0.0,
);
}

View File

@ -85,6 +85,8 @@ export class Renderer {
public useMesh(mesh: Mesh) {
LOG('Using mesh');
this._materialBuffers = [];
for (const materialName in mesh.materials) {
const materialBuffer = new RenderBuffer([
{ name: 'position', numComponents: 3 },
@ -92,14 +94,17 @@ export class Renderer {
{ name: 'normal', numComponents: 3 },
]);
for (let triIndex = 0; triIndex < mesh.tris.length; ++triIndex) {
const uvTri = mesh.getUVTriangle(triIndex);
const triGeom = GeometryTemplates.getTriangleBufferData(uvTri);
materialBuffer.add(triGeom);
}
mesh.tris.forEach((tri, triIndex) => {
if (tri.material === materialName) {
if (tri.material === materialName) {
const uvTri = mesh.getUVTriangle(triIndex);
const triGeom = GeometryTemplates.getTriangleBufferData(uvTri);
materialBuffer.add(triGeom);
}
}
});
const material = mesh.materials[materialName];
this._materialBuffers = [];
if (material.type === MaterialType.solid) {
this._materialBuffers.push({
buffer: materialBuffer,

View File

@ -77,7 +77,7 @@ export class UI {
]),
'colourSpace': new ComboBoxElement('Colour space', [
{ id: 'lab', displayText: 'LAB (recommended)' },
{ id: 'rgb', displayText: 'RGB' },
{ id: 'rgb', displayText: 'RGB (faster)' },
]),
},
elementsOrder: ['textureAtlas', 'blockPalette', 'dithering', 'colourSpace'],

View File

@ -220,6 +220,10 @@ export class Vector3 extends Hashable {
return new Vector3(0.0, 0.0, 1.0);
}
public isNumber() {
return !isNaN(this.x) && !isNaN(this.y) && !isNaN(this.z);
}
// Begin IHashable interface
override hash() {
const p0 = 73856093;

View File

@ -1,4 +1,4 @@
import { LOG, RGB, UV } from '../src/util';
import { RGB, UV } from '../src/util';
import { log, logBreak, LogStyle } from './logging';
import { isDirSetup, ASSERT, getAverageColour, getPermission } from './misc';

View File

@ -1,5 +1,6 @@
import chalk from 'chalk';
/* eslint-disable */
export enum LogStyle {
None = 'None',
Info = 'Info',
@ -7,6 +8,7 @@ export enum LogStyle {
Failure = 'Failure',
Success = 'Success'
}
/* eslint-enable */
const LogStyleDetails: {[style: string]: {style: chalk.Chalk, prefix: string}} = {};
LogStyleDetails[LogStyle.Info] = {style: chalk.blue, prefix: chalk.blue.inverse('INFO')};