forked from mirror/ObjToSchematic
Added proper path routing, added end-to-end tests
This commit is contained in:
parent
5b84c76a75
commit
edcb9ec4b0
@ -33,6 +33,7 @@
|
||||
"block-spacing": [2, "always"],
|
||||
"semi": "error",
|
||||
"spaced-comment": "off",
|
||||
"keyword-spacing": "off"
|
||||
"keyword-spacing": "off",
|
||||
"space-before-function-paren": "off"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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`);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
17
src/main.ts
17
src/main.ts
@ -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) {
|
||||
|
@ -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`);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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();
|
||||
|
14
src/util.ts
14
src/util.ts
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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/');
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
45
tests/full/objlitematic.test.ts
Normal file
45
tests/full/objlitematic.test.ts
Normal 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
45
tests/full/objobj.test.ts
Normal 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);
|
||||
});
|
45
tests/full/objschem.test.ts
Normal file
45
tests/full/objschem.test.ts
Normal 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);
|
||||
});
|
45
tests/full/objschematic.test.ts
Normal file
45
tests/full/objschematic.test.ts
Normal 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);
|
||||
});
|
@ -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 */
|
||||
|
@ -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: {
|
||||
|
@ -26,6 +26,7 @@ export const headlessConfig: THeadlessConfig = {
|
||||
exporter: 'obj', // 'schematic' / 'litematic',
|
||||
},
|
||||
debug: {
|
||||
logging: true,
|
||||
showLogs: true,
|
||||
showWarnings: true,
|
||||
},
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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!');
|
||||
}();
|
||||
|
Loading…
Reference in New Issue
Block a user