Added proper path routing, added end-to-end tests

This commit is contained in:
Lucas Dower 2022-09-11 19:21:33 +01:00
parent 5b84c76a75
commit edcb9ec4b0
30 changed files with 349 additions and 111 deletions

View File

@ -33,6 +33,7 @@
"block-spacing": [2, "always"],
"semi": "error",
"spaced-comment": "off",
"keyword-spacing": "off"
"keyword-spacing": "off",
"space-before-function-paren": "off"
}
}

View File

@ -7,9 +7,9 @@
"node": ">=14.0.0"
},
"scripts": {
"lint": "eslint --fix ./**/*.ts",
"lint": "eslint --fix src tools --ext .ts",
"build": "tsc",
"test": "jest --config jestconfig.json",
"test": "jest --config jestconfig.json --runInBand",
"start": "npm run build && electron ./dist/src/main.js --enable-logging",
"atlas": "node ./dist/tools/build-atlas.js",
"palette": "node ./dist/tools/build-palette.js",

View File

@ -1,10 +1,11 @@
import { AppTypes, AppUtil, ATLASES_DIR, TOptional, UV } from './util';
import { AppTypes, AppUtil, TOptional, UV } from './util';
import fs from 'fs';
import path from 'path';
import { RGBA } from './colour';
import { ASSERT } from './util/error_util';
import { LOG } from './util/log_util';
import { AppPaths } from './util/path_util';
export type TAtlasBlockFace = {
name: string;
@ -32,7 +33,7 @@ export type TAtlasBlock = {
export class Atlas {
public static ATLAS_NAME_REGEX: RegExp = /^[a-zA-Z\-]+$/;
private static _FILE_VERSION: number = 1;
private _blocks: Map<AppTypes.TNamespacedBlockName, TAtlasBlock>;
private _atlasSize: number;
private _atlasName: string;
@ -86,7 +87,7 @@ export class Atlas {
}
public getAtlasTexturePath() {
return path.join(ATLASES_DIR, `./${this._atlasName}.png`);
return path.join(AppPaths.Get.atlases, `./${this._atlasName}.png`);
}
/*
@ -108,6 +109,6 @@ export class Atlas {
}
private static _getAtlasPath(atlasName: string): string {
return path.join(ATLASES_DIR, `./${atlasName}.atlas`);
return path.join(AppPaths.Get.atlases, `./${atlasName}.atlas`);
}
}

View File

@ -1,9 +1,8 @@
import { Voxel, VoxelMesh } from './voxel_mesh';
import { BlockInfo } from './block_atlas';
import { ColourSpace, RESOURCES_DIR } from './util';
import { ColourSpace } from './util';
import fs from 'fs';
import path from 'path';
import { StatusHandler } from './status';
import { Vector3 } from './vector';
import { Atlas } from './atlas';
@ -13,6 +12,7 @@ import { AtlasPalette } from './block_assigner';
import { AppError, ASSERT } from './util/error_util';
import { AssignParams } from './worker_types';
import { BufferGenerator, TBlockMeshBufferDescription } from './buffer';
import { AppPaths, PathUtil } from './util/path_util';
interface Block {
voxel: Voxel;
@ -49,7 +49,7 @@ export class BlockMesh {
this._atlas = Atlas.getVanillaAtlas()!;
//this._recreateBuffer = true;
const fallableBlocksString = fs.readFileSync(path.join(RESOURCES_DIR, 'fallable_blocks.json'), 'utf-8');
const fallableBlocksString = fs.readFileSync(PathUtil.join(AppPaths.Get.resources, 'fallable_blocks.json'), 'utf-8');
this._fallableBlocks = JSON.parse(fallableBlocksString).fallable_blocks;
}

View File

@ -1,13 +1,12 @@
import { BlockMesh } from '../block_mesh';
import { RESOURCES_DIR } from '../util';
import { IExporter } from './base_exporter';
import { Vector3 } from '../vector';
import { StatusHandler } from '../status';
import path from 'path';
import fs from 'fs';
import { NBT, TagType } from 'prismarine-nbt';
import { saveNBT } from '../util/nbt_util';
import { AppPaths, PathUtil } from '../util/path_util';
export class Schematic extends IExporter {
private _convertToNBT(blockMesh: BlockMesh) {
@ -18,7 +17,7 @@ export class Schematic extends IExporter {
const bounds = blockMesh.getVoxelMesh().getBounds();
const schematicBlocks: { [blockName: string]: { id: number, meta: number, name: string } } = JSON.parse(
fs.readFileSync(path.join(RESOURCES_DIR, './block_ids.json'), 'utf8'),
fs.readFileSync(PathUtil.join(AppPaths.Get.resources, './block_ids.json'), 'utf8'),
);
const blocks = blockMesh.getBlocks();

View File

@ -21,7 +21,7 @@ export class ObjImporter extends IImporter {
private _uvs: UV[] = [];
private _tris: Tri[] = [];
private _materials: {[key: string]: (SolidMaterial | TexturedMaterial)} = {
private _materials: { [key: string]: (SolidMaterial | TexturedMaterial) } = {
'DEFAULT_UNASSIGNED': { type: MaterialType.solid, colour: RGBAColours.WHITE },
};
private _mtlLibs: string[] = [];
@ -107,16 +107,16 @@ export class ObjImporter extends IImporter {
.toRegExp(),
delegate: (match: { [key: string]: string }) => {
const line = match.line.trim();
const vertices = line.split(' ').filter((x) => {
return x.length !== 0;
});
if (vertices.length < 3) {
// this.addWarning('')
// throw new AppError('Face data should have at least 3 vertices');
}
const points: {
positionIndex: number;
texcoordIndex?: number;
@ -163,7 +163,7 @@ export class ObjImporter extends IImporter {
const pointBase = points[0];
for (let i = 1; i < points.length - 1; ++i) {
const pointA = points[i];
const pointB = points[i+1];
const pointB = points[i + 1];
const tri: Tri = {
positionIndices: {
x: pointBase.positionIndex - 1,
@ -193,7 +193,7 @@ export class ObjImporter extends IImporter {
},
},
];
private _currentColour: RGBA = RGBAColours.BLACK;
private _currentAlpha: number = 1.0;
private _currentTexture: string = '';
@ -276,8 +276,8 @@ export class ObjImporter extends IImporter {
];
override parseFile(filePath: string) {
ASSERT(path.isAbsolute(filePath), 'path not absolute');
ASSERT(path.isAbsolute(filePath), `ObjImporter: ${filePath} not absolute`);
this._objPath = path.parse(filePath);
this._parseOBJ(filePath);
@ -351,10 +351,10 @@ export class ObjImporter extends IImporter {
continue;
}
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);
}

View File

@ -15,10 +15,9 @@
*/
import { app, BrowserWindow } from 'electron';
import path from 'path';
import url from 'url';
import { AppConfig } from './config';
import { BASE_DIR, STATIC_DIR } from './util';
import { AppPaths, PathUtil } from './util/path_util';
app.commandLine.appendSwitch('js-flags', `--max-old-space-size=${AppConfig.OLD_SPACE_SIZE}`);
@ -36,7 +35,7 @@ function createWindow() {
mainWindow = new BrowserWindow({
width: width,
height: height,
icon: path.join(STATIC_DIR, process.platform === 'win32' ? './icon.ico' : './icon.png'),
icon: PathUtil.join(AppPaths.Get.static, process.platform === 'win32' ? './icon.ico' : './icon.png'),
minWidth: 1280,
minHeight: 720,
webPreferences: {
@ -49,10 +48,10 @@ function createWindow() {
if (!AppConfig.DEBUG_ENABLED) {
mainWindow.removeMenu();
}
// Load index.html
mainWindow.loadURL(url.format({
pathname: path.join(BASE_DIR, './index.html'),
pathname: PathUtil.join(AppPaths.Get.base, './index.html'),
protocol: 'file:',
slashes: true,
}));
@ -65,12 +64,12 @@ function createWindow() {
} catch (e: any) {
mainWindow.setTitle(`${baseTitle} (release//v0.5.1)`);
}
// Open the DevTools.
// mainWindow.webContents.openDevTools();
// Emitted when the window is closed.
mainWindow.on('closed', function() {
mainWindow.on('closed', function () {
app.quit();
});
}
@ -81,7 +80,7 @@ function createWindow() {
app.on('ready', createWindow);
// Quit when all windows are closed.
app.on('window-all-closed', function() {
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
@ -89,7 +88,7 @@ app.on('window-all-closed', function() {
}
});
app.on('activate', function() {
app.on('activate', function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {

View File

@ -1,4 +1,4 @@
import { AppTypes, AppUtil, PALETTES_DIR, TOptional } from './util';
import { AppTypes, AppUtil, TOptional } from './util';
import fs from 'fs';
import path from 'path';
@ -7,19 +7,20 @@ import { Atlas } from './atlas';
import { AppError, ASSERT } from './util/error_util';
import { LOG_WARN } from './util/log_util';
import { RGBA, RGBAUtil } from './colour';
import { AppPaths, PathUtil } from './util/path_util';
export class PaletteManager {
public static getPalettesInfo(): { paletteID: string, paletteDisplayName: string }[] {
const palettes: { paletteID: string, paletteDisplayName: string }[] = [];
fs.readdirSync(PALETTES_DIR).forEach((file) => {
fs.readdirSync(AppPaths.Get.palettes).forEach((file) => {
const paletteFilePath = path.parse(file);
if (paletteFilePath.ext === Palette.PALETTE_FILE_EXT) {
const paletteID = paletteFilePath.name;
let paletteDisplayName = paletteID.replace('-', ' ').toLowerCase();
paletteDisplayName = AppUtil.Text.capitaliseFirstLetter(paletteDisplayName);
palettes.push({ paletteID: paletteID, paletteDisplayName: paletteDisplayName });
}
});
@ -168,7 +169,7 @@ export class Palette {
*/
public removeMissingAtlasBlocks(atlas: Atlas) {
const missingBlocks: AppTypes.TNamespacedBlockName[] = [];
for (let blockIndex = this._blocks.length-1; blockIndex >= 0; --blockIndex) {
for (let blockIndex = this._blocks.length - 1; blockIndex >= 0; --blockIndex) {
const blockName = this._blocks[blockIndex];
if (!atlas.hasBlock(blockName)) {
missingBlocks.push(blockName);
@ -187,6 +188,6 @@ export class Palette {
}
private static _getPalettePath(paletteName: string): string {
return path.join(PALETTES_DIR, `./${paletteName}.palette`);
return PathUtil.join(AppPaths.Get.palettes, `./${paletteName}.palette`);
}
}

View File

@ -1,8 +1,7 @@
import * as twgl from 'twgl.js';
import * as fs from 'fs';
import * as path from 'path';
import { Renderer } from './renderer';
import { SHADERS_DIR } from './util';
import { AppPaths, PathUtil } from './util/path_util';
export class ShaderManager {
public readonly textureTriProgram: twgl.ProgramInfo;
@ -41,7 +40,7 @@ export class ShaderManager {
}
private _getShader(filename: string) {
const absPath = path.join(SHADERS_DIR, filename);
const absPath = PathUtil.join(AppPaths.Get.shaders, filename);
return fs.readFileSync(absPath, 'utf8');
}
}

View File

@ -1,4 +1,4 @@
import { ASSERT } from "../../util/error_util";
import { ASSERT } from '../../util/error_util';
export abstract class BaseUIElement<Type> {
protected _id: string;
@ -34,7 +34,7 @@ export abstract class BaseUIElement<Type> {
public abstract generateHTML(): string;
public abstract registerEvents(): void;
protected abstract _onEnabledChanged(): void;
}

View File

@ -1,5 +1,5 @@
import { BaseUIElement } from './base';
import { ASSERT } from "../../util/error_util";
import { ASSERT } from '../../util/error_util';
export class ButtonElement extends BaseUIElement<any> {
private _onClick: () => void;

View File

@ -1,5 +1,5 @@
import { LabelledElement } from './labelled_element';
import { ASSERT } from "../../util/error_util";
import { ASSERT } from '../../util/error_util';
export type ComboBoxItem<T> = {
id: T;

View File

@ -1,5 +1,5 @@
import { LabelledElement } from './labelled_element';
import { ASSERT } from "../../util/error_util";
import { ASSERT } from '../../util/error_util';
import { remote } from 'electron';
import * as path from 'path';
@ -30,11 +30,11 @@ export class FileInputElement extends LabelledElement<string> {
element.onmouseenter = () => {
this._hovering = true;
}
};
element.onmouseleave = () => {
this._hovering = false;
}
};
element.onclick = () => {
if (!this._isEnabled) {
@ -69,7 +69,7 @@ export class FileInputElement extends LabelledElement<string> {
} else {
element.classList.add('input-text-disabled');
}
}
};
}
protected _onEnabledChanged() {

View File

@ -1,4 +1,4 @@
import { ASSERT } from "../../util/error_util";
import { ASSERT } from '../../util/error_util';
import { UIMessageBuilder } from '../misc';
export type OutputStyle = 'success' | 'warning' | 'error' | 'none';
@ -69,7 +69,7 @@ export class OutputElement {
if (taskItems.length > 0) {
builder.addHeading(taskId, taskHeading, style);
} else {
builder.addBold(taskId, [ taskHeading ], style);
builder.addBold(taskId, [taskHeading], style);
}
builder.addItem(taskId, taskItems, style);

View File

@ -1,4 +1,4 @@
import { ASSERT } from "../../util/error_util";
import { ASSERT } from '../../util/error_util';
import { clamp, mapRange, wayThrough } from '../../math';
import { LabelledElement } from './labelled_element';

View File

@ -1,8 +1,9 @@
import { getRandomID, STATIC_DIR } from '../../util';
import { getRandomID } from '../../util';
import path from 'path';
import fs from 'fs';
import { ASSERT } from '../../util/error_util';
import { AppPaths } from '../../util/path_util';
import { PathUtil } from '../../util/path_util';
export type TToolbarBooleanProperty = 'enabled' | 'active';
@ -20,9 +21,9 @@ export class ToolbarItemElement {
public constructor(params: TToolbarItemParams) {
this._id = getRandomID();
this._iconName = params.icon;
this._iconPath = path.join(STATIC_DIR, params.icon + '.svg');
this._iconPath = PathUtil.join(AppPaths.Get.static, params.icon + '.svg');
this._isEnabled = true;
this._isActive = false;
@ -80,7 +81,7 @@ export class ToolbarItemElement {
element.classList.add('toolbar-item-hover');
}
});
element.addEventListener('mouseleave', () => {
if (this._isEnabled) {
element.classList.remove('toolbar-item-hover');

View File

@ -15,9 +15,10 @@ import { TExporters } from '../exporters/exporters';
import { TVoxelOverlapRule } from '../voxel_mesh';
import { PaletteManager } from '../palette';
import { TBlockAssigners } from '../assigners/assigners';
import { ATLASES_DIR, EAction } from '../util';
import { EAction } from '../util';
import { ASSERT } from '../util/error_util';
import { LOG } from '../util/log_util';
import { AppPaths } from '../util/path_util';
export interface Group {
label: string;
@ -234,7 +235,7 @@ export class UI {
},
elementsOrder: ['grid', 'axes'],
},
},
groupsOrder: ['viewmode', 'debug'],
};
@ -429,7 +430,7 @@ export class UI {
</div>
`;
}
public getActionOutput(action: EAction) {
const group = this._getEActionGroup(action);
return group.output;
@ -548,7 +549,7 @@ export class UI {
private _getTextureAtlases(): ComboBoxItem<string>[] {
const textureAtlases: ComboBoxItem<string>[] = [];
fs.readdirSync(ATLASES_DIR).forEach((file) => {
fs.readdirSync(AppPaths.Get.atlases).forEach((file) => {
if (file.endsWith('.atlas')) {
const paletteID = file.split('.')[0];
let paletteName = paletteID.replace('-', ' ').toLowerCase();

View File

@ -1,5 +1,3 @@
import { PathUtil } from './util/path_util';
export namespace AppUtil {
export namespace Text {
export function capitaliseFirstLetter(text: string) {
@ -54,16 +52,6 @@ export enum ColourSpace {
export type TOptional<T> = T | undefined;
export const BASE_DIR = PathUtil.join(__dirname, '/../../');
export const RESOURCES_DIR = PathUtil.join(BASE_DIR, './res/');
export const ATLASES_DIR = PathUtil.join(RESOURCES_DIR, './atlases');
export const PALETTES_DIR = PathUtil.join(RESOURCES_DIR, './palettes/');
export const STATIC_DIR = PathUtil.join(RESOURCES_DIR, './static/');
export const SHADERS_DIR = PathUtil.join(RESOURCES_DIR, './shaders/');
export const TOOLS_DIR = PathUtil.join(BASE_DIR, './tools/');
export const TESTS_DATA_DIR = PathUtil.join(BASE_DIR, './tests/data/');
export function getRandomID(): string {
return (Math.random() + 1).toString(36).substring(7);
}
}

View File

@ -1,7 +1,60 @@
import path from 'path';
import { ASSERT } from './error_util';
export namespace PathUtil {
export function join(...paths: string[]) {
return path.join(...paths);
}
}
export class AppPaths {
/* Singleton */
private static _instance: AppPaths;
public static get Get() {
return this._instance || (this._instance = new this());
}
private _base: string;
private constructor() {
this._base = PathUtil.join(__dirname, '../../..');
}
public setBaseDir(dir: string) {
this._base = dir;
const parsed = path.parse(dir);
ASSERT(parsed.base === 'ObjToSchematic', `AppPaths: Not correct base ${dir}`);
}
public get base() {
return this._base;
}
public get resources() {
return PathUtil.join(this._base, './res/');
}
public get tools() {
return PathUtil.join(this._base, './tools/');
}
public get testData() {
return PathUtil.join(this._base, './tests/data/');
}
public get atlases() {
return PathUtil.join(this.resources, './atlases/');
}
public get palettes() {
return PathUtil.join(this.resources, './palettes/');
}
public get static() {
return PathUtil.join(this.resources, './static/');
}
public get shaders() {
return PathUtil.join(this.resources, './shaders/');
}
}

View File

@ -17,6 +17,9 @@ export class WorkerClient {
return this._instance || (this._instance = new this());
}
private constructor() {
}
private _loadedMesh?: Mesh;
private _loadedVoxelMesh?: VoxelMesh;
private _loadedBlockMesh?: BlockMesh;

View File

@ -0,0 +1,45 @@
import { TextureFiltering } from '../../src/texture';
import { ColourSpace } from '../../src/util';
import { AppPaths, PathUtil } from '../../src/util/path_util';
import { runHeadless, THeadlessConfig } from '../../tools/headless';
const baseConfig: THeadlessConfig = {
import: {
filepath: '', // Must be an absolute path
},
voxelise: {
voxeliser: 'bvh-ray',
desiredHeight: 80,
useMultisampleColouring: false,
textureFiltering: TextureFiltering.Linear,
voxelOverlapRule: 'average',
enableAmbientOcclusion: false, // Only want true if exporting to .obj
},
assign: {
textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases
blockPalette: 'all-snapshot', // Must be a palette name that exists in /resources/palettes
blockAssigner: 'ordered-dithering',
colourSpace: ColourSpace.RGB,
fallable: 'replace-falling',
},
export: {
filepath: '', // Must be an absolute path to the file (can be anywhere)
exporter: 'obj', // 'schematic' / 'litematic',
},
debug: {
showLogs: false,
showWarnings: false,
},
};
test('FULL Obj->Obj', () => {
AppPaths.Get.setBaseDir(PathUtil.join(__dirname, '../..'));
const config: THeadlessConfig = baseConfig;
config.import.filepath = PathUtil.join(AppPaths.Get.resources, './samples/skull.obj');
config.export.exporter = 'litematic';
config.export.filepath = PathUtil.join(AppPaths.Get.testData, '../out/out.litematic');
runHeadless(config);
});

45
tests/full/objobj.test.ts Normal file
View File

@ -0,0 +1,45 @@
import { TextureFiltering } from '../../src/texture';
import { ColourSpace } from '../../src/util';
import { AppPaths, PathUtil } from '../../src/util/path_util';
import { runHeadless, THeadlessConfig } from '../../tools/headless';
const baseConfig: THeadlessConfig = {
import: {
filepath: '', // Must be an absolute path
},
voxelise: {
voxeliser: 'bvh-ray',
desiredHeight: 80,
useMultisampleColouring: false,
textureFiltering: TextureFiltering.Linear,
voxelOverlapRule: 'average',
enableAmbientOcclusion: false, // Only want true if exporting to .obj
},
assign: {
textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases
blockPalette: 'all-snapshot', // Must be a palette name that exists in /resources/palettes
blockAssigner: 'ordered-dithering',
colourSpace: ColourSpace.RGB,
fallable: 'replace-falling',
},
export: {
filepath: '', // Must be an absolute path to the file (can be anywhere)
exporter: 'obj', // 'schematic' / 'litematic',
},
debug: {
showLogs: false,
showWarnings: false,
},
};
test('FULL Obj->Obj', () => {
AppPaths.Get.setBaseDir(PathUtil.join(__dirname, '../..'));
const config: THeadlessConfig = baseConfig;
config.import.filepath = PathUtil.join(AppPaths.Get.resources, './samples/skull.obj');
config.export.exporter = 'obj';
config.export.filepath = PathUtil.join(AppPaths.Get.testData, '../out/out.obj');
runHeadless(config);
});

View File

@ -0,0 +1,45 @@
import { TextureFiltering } from '../../src/texture';
import { ColourSpace } from '../../src/util';
import { AppPaths, PathUtil } from '../../src/util/path_util';
import { runHeadless, THeadlessConfig } from '../../tools/headless';
const baseConfig: THeadlessConfig = {
import: {
filepath: '', // Must be an absolute path
},
voxelise: {
voxeliser: 'bvh-ray',
desiredHeight: 80,
useMultisampleColouring: false,
textureFiltering: TextureFiltering.Linear,
voxelOverlapRule: 'average',
enableAmbientOcclusion: false, // Only want true if exporting to .obj
},
assign: {
textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases
blockPalette: 'all-snapshot', // Must be a palette name that exists in /resources/palettes
blockAssigner: 'ordered-dithering',
colourSpace: ColourSpace.RGB,
fallable: 'replace-falling',
},
export: {
filepath: '', // Must be an absolute path to the file (can be anywhere)
exporter: 'obj', // 'schematic' / 'litematic',
},
debug: {
showLogs: false,
showWarnings: false,
},
};
test('FULL Obj->Obj', () => {
AppPaths.Get.setBaseDir(PathUtil.join(__dirname, '../..'));
const config: THeadlessConfig = baseConfig;
config.import.filepath = PathUtil.join(AppPaths.Get.resources, './samples/skull.obj');
config.export.exporter = 'schem';
config.export.filepath = PathUtil.join(AppPaths.Get.testData, '../out/out.schem');
runHeadless(config);
});

View File

@ -0,0 +1,45 @@
import { TextureFiltering } from '../../src/texture';
import { ColourSpace } from '../../src/util';
import { AppPaths, PathUtil } from '../../src/util/path_util';
import { runHeadless, THeadlessConfig } from '../../tools/headless';
const baseConfig: THeadlessConfig = {
import: {
filepath: '', // Must be an absolute path
},
voxelise: {
voxeliser: 'bvh-ray',
desiredHeight: 80,
useMultisampleColouring: false,
textureFiltering: TextureFiltering.Linear,
voxelOverlapRule: 'average',
enableAmbientOcclusion: false, // Only want true if exporting to .obj
},
assign: {
textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases
blockPalette: 'all-snapshot', // Must be a palette name that exists in /resources/palettes
blockAssigner: 'ordered-dithering',
colourSpace: ColourSpace.RGB,
fallable: 'replace-falling',
},
export: {
filepath: '', // Must be an absolute path to the file (can be anywhere)
exporter: 'obj', // 'schematic' / 'litematic',
},
debug: {
showLogs: false,
showWarnings: false,
},
};
test('FULL Obj->Schematic', () => {
AppPaths.Get.setBaseDir(PathUtil.join(__dirname, '../..'));
const config: THeadlessConfig = baseConfig;
config.import.filepath = PathUtil.join(AppPaths.Get.resources, './samples/skull.obj');
config.export.exporter = 'schematic';
config.export.filepath = PathUtil.join(AppPaths.Get.testData, '../out/out.schematic');
runHeadless(config);
});

View File

@ -1,4 +1,4 @@
import { ATLASES_DIR, TOOLS_DIR, UV } from '../src/util';
import { UV } from '../src/util';
import { log, LogStyle } from './logging';
import { isDirSetup, ASSERT, getAverageColour, getPermission, getMinecraftDir } from './misc';
@ -9,10 +9,16 @@ import { PNG } from 'pngjs';
import chalk from 'chalk';
import prompt from 'prompt';
import { RGBA } from '../src/colour';
import { AppPaths, PathUtil } from '../src/util/path_util';
const AdmZip = require('adm-zip');
const copydir = require('copy-dir');
const BLOCKS_DIR = PathUtil.join(AppPaths.Get.tools, '/blocks');
const MODELS_DIR = PathUtil.join(AppPaths.Get.tools, '/models');
void async function main() {
AppPaths.Get.setBaseDir(PathUtil.join(__dirname, '../..'));
await getPermission();
checkMinecraftInstallation();
cleanupDirectories();
@ -33,8 +39,8 @@ function checkMinecraftInstallation() {
}
function cleanupDirectories() {
fs.rmSync(path.join(TOOLS_DIR, '/blocks'), { recursive: true, force: true });
fs.rmSync(path.join(TOOLS_DIR, '/models'), { recursive: true, force: true });
fs.rmSync(BLOCKS_DIR, { recursive: true, force: true });
fs.rmSync(MODELS_DIR, { recursive: true, force: true });
}
async function getResourcePack() {
@ -48,14 +54,14 @@ async function getResourcePack() {
const resourcePacks = fs.readdirSync(resourcePacksDir);
log(LogStyle.None, `1) Vanilla`);
for (let i = 0; i < resourcePacks.length; ++i) {
log(LogStyle.None, `${i+2}) ${resourcePacks[i]}`);
log(LogStyle.None, `${i + 2}) ${resourcePacks[i]}`);
}
const { packChoice } = await prompt.get({
properties: {
packChoice: {
description: `Which resource pack do you want to build an atlas for? (1-${resourcePacks.length+1})`,
message: `Response must be between 1 and ${resourcePacks.length+1}`,
description: `Which resource pack do you want to build an atlas for? (1-${resourcePacks.length + 1})`,
message: `Response must be between 1 and ${resourcePacks.length + 1}`,
required: true,
conform: (value) => {
return value >= 1 && value <= resourcePacks.length + 1;
@ -97,9 +103,9 @@ function fetchVanillModelsAndTextures(fetchTextures: boolean) {
const zipEntries = zip.getEntries();
zipEntries.forEach((zipEntry: any) => {
if (fetchTextures && zipEntry.entryName.startsWith('assets/minecraft/textures/block')) {
zip.extractEntryTo(zipEntry.entryName, path.join(TOOLS_DIR, './blocks'), false, true);
zip.extractEntryTo(zipEntry.entryName, BLOCKS_DIR, false, true);
} else if (zipEntry.entryName.startsWith('assets/minecraft/models/block')) {
zip.extractEntryTo(zipEntry.entryName, path.join(TOOLS_DIR, './models'), false, true);
zip.extractEntryTo(zipEntry.entryName, MODELS_DIR, false, true);
}
});
log(LogStyle.Success, `Extracted textures and models successfully\n`);
@ -120,7 +126,7 @@ async function fetchModelsAndTextures() {
if (fs.lstatSync(resourcePackDir).isDirectory()) {
log(LogStyle.Info, `Resource pack '${resourcePack}' is a directory`);
const blockTexturesSrc = path.join(resourcePackDir, 'assets/minecraft/textures/block');
const blockTexturesDst = path.join(TOOLS_DIR, './blocks');
const blockTexturesDst = BLOCKS_DIR;
log(LogStyle.Info, `Copying ${blockTexturesSrc} to ${blockTexturesDst}`);
copydir(blockTexturesSrc, blockTexturesDst, {
utimes: true,
@ -130,12 +136,12 @@ async function fetchModelsAndTextures() {
log(LogStyle.Success, `Copied block textures successfully`);
} else {
log(LogStyle.Info, `Resource pack '${resourcePack}' is not a directory, expecting to be a .zip`);
const zip = new AdmZip(resourcePackDir);
const zipEntries = zip.getEntries();
zipEntries.forEach((zipEntry: any) => {
if (zipEntry.entryName.startsWith('assets/minecraft/textures/block')) {
zip.extractEntryTo(zipEntry.entryName, path.join(TOOLS_DIR, './blocks'), false, true);
zip.extractEntryTo(zipEntry.entryName, BLOCKS_DIR, false, true);
}
});
log(LogStyle.Success, `Copied block textures successfully`);
@ -145,20 +151,20 @@ async function fetchModelsAndTextures() {
async function buildAtlas() {
// Check /blocks and /models is setup correctly
log(LogStyle.Info, 'Checking assets are provided...');
log(LogStyle.Info, 'Checking assets are provided...');
const texturesDirSetup = isDirSetup('./blocks', 'assets/minecraft/textures/block');
ASSERT(texturesDirSetup, '/blocks is not setup correctly');
log(LogStyle.Success, '/tools/blocks/ setup correctly');
log(LogStyle.Success, '/tools/blocks/ setup correctly');
const modelsDirSetup = isDirSetup('./models', 'assets/minecraft/models/block');
ASSERT(modelsDirSetup, '/models is not setup correctly');
log(LogStyle.Success, '/tools/models/ setup correctly');
log(LogStyle.Success, '/tools/models/ setup correctly');
// Load the ignore list
log(LogStyle.Info, 'Loading ignore list...');
let ignoreList: Array<string> = [];
const ignoreListPath = path.join(TOOLS_DIR, './ignore-list.txt');
const ignoreListPath = path.join(AppPaths.Get.tools, './ignore-list.txt');
if (fs.existsSync(ignoreListPath)) {
log(LogStyle.Success, 'Found ignore list');
ignoreList = fs.readFileSync(ignoreListPath, 'utf-8').replace(/\r/g, '').split('\n');
@ -179,7 +185,7 @@ async function buildAtlas() {
Leaves = 'minecraft:block/leaves',
}
/* eslint-enable */
interface Model {
name: string,
colour?: RGBA,
@ -187,24 +193,24 @@ async function buildAtlas() {
[face: string]: Texture
}
}
interface Texture {
name: string,
texcoord?: UV,
colour?: RGBA
}
log(LogStyle.Info, 'Loading block models...');
const faces = ['north', 'south', 'up', 'down', 'east', 'west'];
const allModels: Array<Model> = [];
const allBlockNames: Set<string> = new Set();
const usedTextures: Set<string> = new Set();
fs.readdirSync(path.join(TOOLS_DIR, './models')).forEach((filename) => {
fs.readdirSync(MODELS_DIR).forEach((filename) => {
if (path.extname(filename) !== '.json') {
return;
};
const filePath = path.join(TOOLS_DIR, './models', filename);
const filePath = path.join(MODELS_DIR, filename);
const fileData = fs.readFileSync(filePath, 'utf8');
const modelData = JSON.parse(fileData);
const parsedPath = path.parse(filePath);
@ -329,7 +335,7 @@ async function buildAtlas() {
log(LogStyle.Info, `Building ${atlasName}.png...`);
usedTextures.forEach((textureName) => {
const shortName = textureName.split('/')[1]; // Eww
const absolutePath = path.join(TOOLS_DIR, './blocks', shortName + '.png');
const absolutePath = path.join(BLOCKS_DIR, shortName + '.png');
const fileData = fs.readFileSync(absolutePath);
const pngData = PNG.sync.read(fileData);
const image = images(absolutePath);
@ -346,7 +352,7 @@ async function buildAtlas() {
16 * (3 * offsetY + 1) / (atlasWidth * 3),
),
colour: getAverageColour(pngData),
},
};
++offsetX;
if (offsetX >= atlasSize) {
@ -380,7 +386,7 @@ async function buildAtlas() {
log(LogStyle.Info, 'Exporting...');
const atlasDir = path.join(ATLASES_DIR, `./${atlasName}.png`);
const atlasDir = path.join(AppPaths.Get.atlases, `./${atlasName}.png`);
outputImage.save(atlasDir);
log(LogStyle.Success, `${atlasName}.png exported to /resources/atlases/`);
const outputJSON = {
@ -388,7 +394,7 @@ async function buildAtlas() {
blocks: allModels,
supportedBlockNames: Array.from(allBlockNames),
};
fs.writeFileSync(path.join(ATLASES_DIR, `./${atlasName}.atlas`), JSON.stringify(outputJSON, null, 4));
fs.writeFileSync(path.join(AppPaths.Get.atlases, `./${atlasName}.atlas`), JSON.stringify(outputJSON, null, 4));
log(LogStyle.Success, `${atlasName}.atlas exported to /resources/atlases/\n`);
/* eslint-disable */

View File

@ -1,23 +1,25 @@
import { log, LogStyle } from './logging';
import { TOOLS_DIR } from '../src/util';
import { Palette } from '../src/palette';
import fs from 'fs';
import path from 'path';
import prompt from 'prompt';
import { AppPaths, PathUtil } from '../src/util/path_util';
const PALETTE_NAME_REGEX = /^[a-zA-Z\-]+$/;
void async function main() {
AppPaths.Get.setBaseDir(PathUtil.join(__dirname, '../..'));
log(LogStyle.Info, 'Creating a new palette...');
const paletteBlocksDir = path.join(TOOLS_DIR, './new-palette-blocks.txt');
const paletteBlocksDir = path.join(AppPaths.Get.tools, './new-palette-blocks.txt');
if (!fs.existsSync(paletteBlocksDir)) {
log(LogStyle.Failure, 'Could not find /tools/new-palette-blocks.txt');
return;
}
log(LogStyle.Success, 'Found list of blocks to use in /tools/new-palette-blocks.txt');
let blocksToUse: string[] = fs.readFileSync(paletteBlocksDir, 'utf8').replace(/\r/g, '').split('\n');
blocksToUse = blocksToUse.filter((block) => {
return block.length !== 0;
@ -28,7 +30,7 @@ void async function main() {
return;
}
log(LogStyle.Info, `Found ${blocksToUse.length} blocks to use`);
const schema: prompt.Schema = {
properties: {
paletteName: {

View File

@ -26,6 +26,7 @@ export const headlessConfig: THeadlessConfig = {
exporter: 'obj', // 'schematic' / 'litematic',
},
debug: {
logging: true,
showLogs: true,
showWarnings: true,
},
};

View File

@ -1,4 +1,3 @@
import { headlessConfig } from './headless-config';
import { AssignParams, ExportParams, ImportParams, VoxeliseParams } from '../src/worker_types';
import { WorkerClient } from '../src/worker_client';
import { StatusHandler } from '../src/status';
@ -15,7 +14,7 @@ export type THeadlessConfig = {
}
}
export function runHeadless() {
export function runHeadless(headlessConfig: THeadlessConfig) {
if (headlessConfig.debug.showLogs) {
Logger.Get.enableLOGMAJOR();
}

View File

@ -1,11 +1,11 @@
import { log, LogStyle } from './logging';
import { TOOLS_DIR } from '../src/util';
import fs from 'fs';
import path from 'path';
import { PNG } from 'pngjs';
import prompt from 'prompt';
import { RGBA } from '../src/colour';
import { AppPaths } from '../src/util/path_util';
export const ASSERT = (condition: boolean, onFailMessage: string) => {
if (!condition) {
@ -15,7 +15,7 @@ export const ASSERT = (condition: boolean, onFailMessage: string) => {
};
export function isDirSetup(relativePath: string, jarAssetDir: string) {
const dir = path.join(TOOLS_DIR, relativePath);
const dir = path.join(AppPaths.Get.tools, relativePath);
if (fs.existsSync(dir)) {
if (fs.readdirSync(dir).length > 0) {
return true;

View File

@ -1,8 +1,12 @@
import { PathUtil, AppPaths } from '../src/util/path_util';
import { LOG_MAJOR } from '../src/util/log_util';
import { runHeadless } from './headless';
import { headlessConfig } from './headless-config';
void async function main() {
runHeadless();
AppPaths.Get.setBaseDir(PathUtil.join(__dirname, '../..'));
runHeadless(headlessConfig);
LOG_MAJOR('Finished!');
}();