Compare commits

...

9 Commits

Author SHA1 Message Date
Lucas Dower
23bffa2dc4
Fixed enabled state flow for VoxelMesh importers
Added scroll-into-view for next recommended actions
2023-02-08 19:00:31 +00:00
Lucas Dower
ee5dd8b702
Minor UI updates 2023-02-07 21:15:01 +00:00
Lucas Dower
1b5d23ba75
Updated view modes to handle missing triangle mesh 2023-02-07 20:56:50 +00:00
Lucas Dower
fd31ca161f
Cull voxels/blocks with no visible faces from rendering 2023-02-07 20:41:33 +00:00
Lucas Dower
74b1d1222b
Added prototype .vox importer, refactored importers 2023-02-07 20:31:45 +00:00
Lucas Dower
6169d51cbb
Refactored worker layout to support async actions 2023-01-25 18:50:31 +00:00
Lucas Dower
de67687d86
Added UI header with update checker 2023-01-24 21:29:38 +00:00
Lucas Dower
134914530b
Minor refactor to exporters 2023-01-24 20:33:27 +00:00
Lucas Dower
39693c2a42
Initial 0.8 branch commit 2023-01-23 23:11:44 +00:00
35 changed files with 808 additions and 469 deletions

17
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "objtoschematic",
"version": "0.7.0",
"version": "0.8.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "objtoschematic",
"version": "0.7.0",
"version": "0.8.0",
"license": "BSD-3-Clause",
"dependencies": {
"bvh-tree": "^1.0.1",
@ -15,7 +15,8 @@
"prismarine-nbt": "^1.6.0",
"tga": "^1.0.7",
"twgl.js": "^4.19.1",
"varint-array": "^0.0.0"
"varint-array": "^0.0.0",
"vox-reader": "^2.1.2"
},
"devDependencies": {
"@types/adm-zip": "^0.5.0",
@ -7703,6 +7704,11 @@
"varint": "^5.0.0"
}
},
"node_modules/vox-reader": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/vox-reader/-/vox-reader-2.1.2.tgz",
"integrity": "sha512-33H2ZE1FedHg2xlQmEkID05jc1d05uwhH2JOURblLspTSZI7SLHNTN6jyq4cIIaglSwcC+o21Pss3uRSxtlN/Q=="
},
"node_modules/w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
@ -13814,6 +13820,11 @@
"varint": "^5.0.0"
}
},
"vox-reader": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/vox-reader/-/vox-reader-2.1.2.tgz",
"integrity": "sha512-33H2ZE1FedHg2xlQmEkID05jc1d05uwhH2JOURblLspTSZI7SLHNTN6jyq4cIIaglSwcC+o21Pss3uRSxtlN/Q=="
},
"w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",

View File

@ -1,7 +1,7 @@
{
"name": "objtoschematic",
"private": true,
"version": "0.7.0",
"version": "0.8.0",
"description": "A tool to convert .obj files into voxels and then into Minecraft Schematic files",
"main": "./dist/src/main.js",
"engines": {
@ -12,6 +12,7 @@
"build": "tsc",
"test": "jest --config jestconfig.json",
"start": "npm run build && electron ./dist/src/main.js --enable-logging --remote-debugging-port=9222",
"debug": "npm run build && electron ./dist/src/main.js --enable-logging --remote-debugging-port=9222 --OTS-ENABLE-DEBUG",
"atlas": "node ./dist/tools/build-atlas.js",
"palette": "node ./dist/tools/build-palette.js",
"headless": "tsc && node ./dist/tools/run-headless.js",
@ -64,6 +65,7 @@
"prismarine-nbt": "^1.6.0",
"tga": "^1.0.7",
"twgl.js": "^4.19.1",
"varint-array": "^0.0.0"
"varint-array": "^0.0.0",
"vox-reader": "^2.1.2"
}
}

12
res/static/bug.svg Normal file
View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-bug" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" id="bug-svg"/>
<path d="M9 9v-1a3 3 0 0 1 6 0v1" />
<path d="M8 9h8a6 6 0 0 1 1 3v3a5 5 0 0 1 -10 0v-3a6 6 0 0 1 1 -3" />
<line x1="3" y1="13" x2="7" y2="13" />
<line x1="17" y1="13" x2="21" y2="13" />
<line x1="12" y1="20" x2="12" y2="14" />
<line x1="4" y1="19" x2="7.35" y2="17" />
<line x1="20" y1="19" x2="16.65" y2="17" />
<line x1="4" y1="7" x2="7.75" y2="9.4" />
<line x1="20" y1="7" x2="16.25" y2="9.4" />
</svg>

After

Width:  |  Height:  |  Size: 711 B

4
res/static/github.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-github" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round" id="github-svg">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5" />
</svg>

After

Width:  |  Height:  |  Size: 613 B

View File

@ -87,24 +87,25 @@ export class AppContext {
}
public do(action: EAction) {
this._ui.cacheValues(action);
this._ui.disable(action);
this._ui.disableAll();
const workerJob = this._getWorkerJob(action);
if (workerJob === undefined) {
this._ui.enableTo(action);
return;
}
this._lastAction = action;
const uiOutput = this._ui.getActionOutput(action);
this._ui.disable(action + 1);
this._ui.cacheValues(action);
this._ui.cacheGroupEnableStates();
this._ui.disableAll();
const jobCallback = (payload: TFromWorkerMessage) => {
//this._ui.enableTo(action);
switch (payload.action) {
case 'KnownError':
case 'UnknownError': {
uiOutput.setTaskComplete(
this._workerController.addJob({
id: workerJob.id,
payload: workerJob.payload,
callback: (payload: TFromWorkerMessage) => {
this._ui.restoreGroupEnableStates();
if (payload.action === 'KnownError' || payload.action === 'UnknownError') {
this._ui.getActionOutput(action).setTaskComplete(
'action',
StatusHandler.Get.getDefaultFailureMessage(action),
[payload.action === 'KnownError' ? payload.error.message : 'Something unexpectedly went wrong'],
@ -116,29 +117,17 @@ export class AppContext {
.stopLoading()
.setProgress(0.0);
this._ui.enableTo(action);
break;
}
default: {
//this._ui.enableTo(action + 1);
} else {
ASSERT(payload.action !== 'Progress');
const { builder, style } = this._getActionMessageBuilder(action, payload.statusMessages);
uiOutput.setMessage(builder, style as OutputStyle);
this._ui.getActionOutput(action).setMessage(builder, style as OutputStyle);
if (workerJob.callback) {
workerJob.callback(payload);
}
}
}
};
this._lastAction = action;
this._workerController.addJob({
id: workerJob.id,
payload: workerJob.payload,
callback: jobCallback,
},
});
}
@ -177,7 +166,6 @@ export class AppContext {
}
ASSERT(false);
}
private _import(): TWorkerJob {
const uiElements = this._ui.layout.import.elements;
@ -198,21 +186,26 @@ export class AppContext {
ASSERT(payload.action === 'Import');
const outputElement = this._ui.getActionOutput(EAction.Import);
const dimensions = new Vector3(
payload.result.dimensions.x,
payload.result.dimensions.y,
payload.result.dimensions.z,
);
dimensions.mulScalar(AppConfig.Get.CONSTRAINT_MAXIMUM_HEIGHT / 8.0).floor();
this.maxConstraint = dimensions;
this._materialManager = new MaterialMapManager(payload.result.materials);
if (payload.result.type === 'Mesh') {
const dimensions = Vector3.copy(payload.result.dimensions);
dimensions.mulScalar(AppConfig.Get.CONSTRAINT_MAXIMUM_HEIGHT / 8.0).floor();
this.maxConstraint = dimensions;
this._materialManager = new MaterialMapManager(payload.result.materials);
if (payload.result.triangleCount < AppConfig.Get.RENDER_TRIANGLE_THRESHOLD) {
outputElement.setTaskInProgress('render', '[Renderer]: Processing...');
this._workerController.addJob(this._renderMesh());
if (payload.result.triangleCount < AppConfig.Get.RENDER_TRIANGLE_THRESHOLD) {
outputElement.setTaskInProgress('render', '[Renderer]: Processing...');
this._workerController.addJob(this._renderMesh());
} else {
const message = `Will not render mesh as its over ${AppConfig.Get.RENDER_TRIANGLE_THRESHOLD.toLocaleString()} triangles.`;
outputElement.setTaskComplete('render', '[Renderer]: Stopped', [message], 'warning');
}
} else {
const message = `Will not render mesh as its over ${AppConfig.Get.RENDER_TRIANGLE_THRESHOLD.toLocaleString()} triangles.`;
outputElement.setTaskComplete('render', '[Renderer]: Stopped', [message], 'warning');
ASSERT(payload.result.type === 'VoxelMesh');
this._materialManager.materials.clear();
outputElement.setTaskComplete('render', '[Renderer]: Stopped', [], 'success');
this._workerController.addJob(this._renderVoxelMesh());
}
this._updateMaterialsAction();
@ -255,7 +248,8 @@ export class AppContext {
Renderer.Get.setModelToUse(MeshType.TriangleMesh);
});
this._ui.enableTo(EAction.Voxelise);
this._ui.enable(EAction.Voxelise);
this._ui.scrollIntoView(EAction.Voxelise);
};
return { id: 'Import', payload: payload, callback: callback };
@ -300,7 +294,9 @@ export class AppContext {
const callback = (payload: TFromWorkerMessage) => {
// This callback is not managed through `AppContext::do`, therefore
// we need to check the payload is not an error
this._ui.enableTo(EAction.Voxelise);
this._ui.enable(EAction.Materials);
this._ui.enable(EAction.Voxelise);
this._ui.scrollIntoView(EAction.Materials);
switch (payload.action) {
case 'KnownError':
@ -388,7 +384,8 @@ export class AppContext {
);
LOG_ERROR(payload.error);
this._ui.enableTo(EAction.Assign);
this._ui.enable(EAction.Assign);
this._ui.scrollIntoView(EAction.Assign);
break;
}
default: {
@ -404,7 +401,8 @@ export class AppContext {
[],
'success',
);
this._ui.enableTo(EAction.Assign);
this._ui.enable(EAction.Assign);
this._ui.scrollIntoView(EAction.Assign);
}
}
}
@ -476,7 +474,8 @@ export class AppContext {
);
LOG_ERROR(payload.error);
this._ui.enableTo(EAction.Export);
this._ui.enable(EAction.Export);
this._ui.scrollIntoView(EAction.Export);
break;
}
default: {
@ -492,7 +491,8 @@ export class AppContext {
[],
'success',
);
this._ui.enableTo(EAction.Export);
this._ui.enable(EAction.Export);
this._ui.scrollIntoView(EAction.Export);
}
}
}
@ -508,7 +508,10 @@ export class AppContext {
const filepath = remote.dialog.showSaveDialogSync({
title: 'Save structure',
buttonLabel: 'Save',
filters: [exporter.getFormatFilter()],
filters: [{
name: exporter.getFormatFilter().name,
extensions: [exporter.getFormatFilter().extension],
}],
});
if (filepath === undefined) {
@ -529,7 +532,8 @@ export class AppContext {
const callback = (payload: TFromWorkerMessage) => {
// This callback is managed through `AppContext::do`, therefore
// this callback is only called if the job is successful.
this._ui.enableTo(EAction.Export);
this._ui.enable(EAction.Export);
this._ui.scrollIntoView(EAction.Export);
};
return { id: 'Export', payload: payload, callback: callback };

View File

@ -1,3 +1,4 @@
import { EFaceVisibility } from './block_assigner';
import { BlockMesh } from './block_mesh';
import { AppConfig } from './config';
import { AppConstants } from './constants';
@ -70,39 +71,49 @@ export class ChunkedBufferGenerator {
const cube: AttributeData = GeometryTemplates.getBoxBufferData(new Vector3(0, 0, 0));
const voxels = voxelMesh.getVoxels();
let bufferIndex = 0;
for (let i = 0; i < numBufferVoxels; ++i) {
const voxelIndex = i + voxelsStartIndex;
const voxel = voxels[voxelIndex];
if (voxelMesh.getFaceVisibility(voxel.position) === EFaceVisibility.None) {
// This voxel has no visible faces, cull it from the buffer.
// TODO Cull on a per-face basis, instead of per-voxel.
continue;
}
const voxelColourArray = [voxel.colour.r, voxel.colour.g, voxel.colour.b, voxel.colour.a];
const voxelPositionArray = voxel.position.toArray();
for (let j = 0; j < AppConstants.VoxelMeshBufferComponentOffsets.POSITION; ++j) {
newBuffer.position.data[i * AppConstants.VoxelMeshBufferComponentOffsets.POSITION + j] = cube.custom.position[j] + voxelPositionArray[j % 3];
newBuffer.position.data[bufferIndex * AppConstants.VoxelMeshBufferComponentOffsets.POSITION + j] = cube.custom.position[j] + voxelPositionArray[j % 3];
}
for (let j = 0; j < AppConstants.VoxelMeshBufferComponentOffsets.COLOUR; ++j) {
newBuffer.colour.data[i * AppConstants.VoxelMeshBufferComponentOffsets.COLOUR + j] = voxelColourArray[j % 4];
newBuffer.colour.data[bufferIndex * AppConstants.VoxelMeshBufferComponentOffsets.COLOUR + j] = voxelColourArray[j % 4];
}
for (let j = 0; j < AppConstants.VoxelMeshBufferComponentOffsets.NORMAL; ++j) {
newBuffer.normal.data[i * AppConstants.VoxelMeshBufferComponentOffsets.NORMAL + j] = cube.custom.normal[j];
newBuffer.normal.data[bufferIndex * AppConstants.VoxelMeshBufferComponentOffsets.NORMAL + j] = cube.custom.normal[j];
}
for (let j = 0; j < AppConstants.VoxelMeshBufferComponentOffsets.TEXCOORD; ++j) {
newBuffer.texcoord.data[i * AppConstants.VoxelMeshBufferComponentOffsets.TEXCOORD + j] = cube.custom.texcoord[j];
newBuffer.texcoord.data[bufferIndex * AppConstants.VoxelMeshBufferComponentOffsets.TEXCOORD + j] = cube.custom.texcoord[j];
}
for (let j = 0; j < AppConstants.VoxelMeshBufferComponentOffsets.INDICES; ++j) {
newBuffer.indices.data[i * AppConstants.VoxelMeshBufferComponentOffsets.INDICES + j] = cube.indices[j] + (i * AppConstants.INDICES_PER_VOXEL);
newBuffer.indices.data[bufferIndex * AppConstants.VoxelMeshBufferComponentOffsets.INDICES + j] = cube.indices[j] + (bufferIndex * AppConstants.INDICES_PER_VOXEL);
}
if (params.enableAmbientOcclusion) {
const voxelOcclusionArray = OcclusionManager.Get.getOcclusions(voxel.position, voxelMesh);
for (let j = 0; j < AppConstants.VoxelMeshBufferComponentOffsets.OCCLUSION; ++j) {
newBuffer.occlusion.data[i * AppConstants.VoxelMeshBufferComponentOffsets.OCCLUSION + j] = voxelOcclusionArray[j];
newBuffer.occlusion.data[bufferIndex * AppConstants.VoxelMeshBufferComponentOffsets.OCCLUSION + j] = voxelOcclusionArray[j];
}
}
++bufferIndex;
}
return {
@ -152,6 +163,12 @@ export class ChunkedBufferGenerator {
const blockIndex = i + blocksStartIndex;
const blockLighting = blockMesh.getBlockLighting(blocks[blockIndex].voxel.position);
if (blockMesh.getVoxelMesh().getFaceVisibility(blocks[blockIndex].voxel.position) === EFaceVisibility.None) {
// This voxel has no visible faces, cull it from the buffer.
// TODO Cull on a per-face basis, instead of per-voxel.
continue;
}
for (let f = 0; f < AppConstants.FACES_PER_VOXEL; ++f) {
const faceName = faceOrder[f];
const faceLighting = lightingRamp.get(blockLighting[f] ?? 15) ?? 1.0;

View File

@ -12,6 +12,10 @@ export class AppConfig {
}
public readonly RELEASE_MODE: boolean;
public readonly MAJOR_VERSION: number;
public readonly MINOR_VERSION: number;
public readonly HOTFIX_VERSION: number;
public readonly VERSION_TYPE: 'd' | 'a' | 'r'; // dev, alpha, or release build
public readonly RELEASE_VERSION: string;
public readonly VOXEL_BUFFER_CHUNK_SIZE: number;
@ -40,8 +44,12 @@ export class AppConfig {
public readonly FRESNEL_MIX: number;
private constructor() {
this.RELEASE_MODE = true;
this.RELEASE_VERSION = '0.7.0r';
this.RELEASE_MODE = false;
this.MAJOR_VERSION = 0;
this.MINOR_VERSION = 8;
this.HOTFIX_VERSION = 0;
this.VERSION_TYPE = 'd';
this.RELEASE_VERSION = `${this.MAJOR_VERSION}.${this.MINOR_VERSION}.${this.HOTFIX_VERSION}${this.VERSION_TYPE}`;
this.VOXEL_BUFFER_CHUNK_SIZE = 5_000;
const configFile = fs.readFileSync(PathUtil.join(AppPaths.Get.resources, 'config.json'), 'utf8');

View File

@ -1,23 +1,18 @@
import { BlockMesh } from '../block_mesh';
import { Vector3 } from '../vector';
export abstract class IExporter {
protected _sizeVector!: Vector3;
/** The display name of this exporter */
public abstract getFormatName(): string;
/** The file type extension of this exporter
/** The file type extension of this exporter.
* @note Do not include the dot prefix, e.g. 'obj' not '.obj'.
*/
public abstract getFileExtension(): string;
public abstract export(blockMesh: BlockMesh, filePath: string): boolean;
public getFormatFilter() {
return {
name: this.getFormatName(),
extensions: [this.getFileExtension()],
};
public abstract getFormatFilter(): {
name: string,
extension: string,
}
/**
* Export a block mesh to a file.
* @param blockMesh The block mesh to export.
* @param filePath The location to save the file to.
*/
public abstract export(blockMesh: BlockMesh, filePath: string): void;
}

View File

@ -1,12 +1,17 @@
import { ASSERT } from '../util/error_util';
import { IExporter } from './base_exporter';
import { Litematic } from './litematic_exporter';
import { NBTExporter } from './nbt_exporter';
import { ObjExporter } from './obj_exporter';
import { SchemExporter } from './schem_exporter';
import { Schematic } from './schematic_exporter';
//import { UncompressedJSONExporter } from './uncompressed_json_exporter';
export type TExporters = 'schematic' | 'litematic' | 'obj' | 'schem' | 'nbt';
export type TExporters =
'schematic' |
'litematic' |
'obj' |
'schem' |
'nbt';
export class ExporterFactory {
public static GetExporter(voxeliser: TExporters): IExporter {
@ -21,8 +26,6 @@ export class ExporterFactory {
return new SchemExporter();
case 'nbt':
return new NBTExporter();
default:
ASSERT(false);
}
}
}

View File

@ -3,47 +3,60 @@ import { NBT, TagType } from 'prismarine-nbt';
import { BlockMesh } from '../block_mesh';
import { AppConstants } from '../constants';
import { ceilToNearest } from '../math';
import { AppTypes } from '../util';
import { ASSERT } from '../util/error_util';
import { saveNBT } from '../util/nbt_util';
import { NBTUtil } from '../util/nbt_util';
import { Vector3 } from '../vector';
import { IExporter } from './base_exporter';
type BlockID = number;
type long = [number, number];
interface BlockMapping {
[name: string]: BlockID
}
type BlockMapping = Map<AppTypes.TNamespacedBlockName, BlockID>;
export class Litematic extends IExporter {
// XZY
private _getBufferIndex(vec: Vector3) {
return (this._sizeVector.z * this._sizeVector.x * vec.y) + (this._sizeVector.x * vec.z) + vec.x;
public override getFormatFilter() {
return {
name: 'Litematic',
extension: 'litematic',
};
}
public override export(blockMesh: BlockMesh, filePath: string) {
const nbt = this._convertToNBT(blockMesh);
NBTUtil.save(nbt, filePath);
}
/**
* Create a mapping from block names to their respecitve index in the block state palette
* Create a mapping from block names to their respecitve index in the block state palette.
*/
private _createBlockMapping(blockMesh: BlockMesh): BlockMapping {
const blockMapping: BlockMapping = { 'minecraft:air': 0 };
const blockMapping: BlockMapping = new Map();
blockMapping.set('minecraft:air', 0);
blockMesh.getBlockPalette().forEach((blockName, index) => {
blockMapping[blockName] = index + 1;
blockMapping.set(blockName, index + 1);
});
return blockMapping;
}
/**
* Pack the blocks into a buffer that's the dimensions of the block mesh.
*/
private _createBlockBuffer(blockMesh: BlockMesh, blockMapping: BlockMapping): Uint32Array {
const bufferSize = this._sizeVector.x * this._sizeVector.y * this._sizeVector.z;
const bounds = blockMesh.getVoxelMesh().getBounds();
const bounds = blockMesh.getVoxelMesh()?.getBounds();
const sizeVector = Vector3.sub(bounds.max, bounds.min).add(1);
const buffer = new Uint32Array(bufferSize);
const buffer = new Uint32Array(sizeVector.x * sizeVector.y * sizeVector.z);
blockMesh.getBlocks().forEach((block) => {
const indexVector = Vector3.sub(block.voxel.position, bounds.min);
const bufferIndex = this._getBufferIndex(indexVector);
buffer[bufferIndex] = blockMapping[block.blockInfo.name || 'minecraft:air'];
const bufferIndex = (sizeVector.z * sizeVector.x * indexVector.y) + (sizeVector.x * indexVector.z) + indexVector.x; // XZY ordering
const mappingIndex = blockMapping.get(block.blockInfo.name);
ASSERT(mappingIndex !== undefined, 'Invalid mapping index');
buffer[bufferIndex] = mappingIndex;
});
return buffer;
@ -86,9 +99,9 @@ export class Litematic extends IExporter {
private _encodeBlockBuffer(blockMesh: BlockMesh, blockMapping: BlockMapping) {
const blockBuffer = this._createBlockBuffer(blockMesh, blockMapping);
const paletteSize = Object.keys(blockMapping).length;
const paletteSize = blockMapping.size;
const stride = Math.ceil(Math.log2(paletteSize - 1));
ASSERT(stride >= 1, 'Stride too small');
ASSERT(stride >= 1, `Stride too small: ${stride}`);
const expectedLengthBits = blockBuffer.length * stride;
const requiredLengthBits = ceilToNearest(expectedLengthBits, 64);
@ -133,18 +146,20 @@ export class Litematic extends IExporter {
}
private _createBlockStatePalette(blockMapping: BlockMapping) {
const blockStatePalette = Array(Object.keys(blockMapping).length);
for (const blockName of Object.keys(blockMapping)) {
const index = blockMapping[blockName];
blockStatePalette[index] = { Name: { type: TagType.String, value: blockName } };
}
blockStatePalette[0] = { Name: { type: TagType.String, value: 'minecraft:air' } };
const blockStatePalette = Array(blockMapping.size);
blockMapping.forEach((mappingIndex, blockName) => {
blockStatePalette[mappingIndex] = { Name: { type: TagType.String, value: blockName } };
});
return blockStatePalette;
}
private _convertToNBT(blockMesh: BlockMesh) {
const bufferSize = this._sizeVector.x * this._sizeVector.y * this._sizeVector.z;
const bounds = blockMesh.getVoxelMesh()?.getBounds();
const sizeVector = Vector3.sub(bounds.max, bounds.min).add(1);
const bufferSize = sizeVector.x * sizeVector.y * sizeVector.z;
const blockMapping = this._createBlockMapping(blockMesh);
const blockStates = this._createBlockStates(blockMesh, blockMapping);
@ -161,9 +176,9 @@ export class Litematic extends IExporter {
Description: { type: TagType.String, value: '' },
Size: {
type: TagType.Compound, value: {
x: { type: TagType.Int, value: this._sizeVector.x },
y: { type: TagType.Int, value: this._sizeVector.y },
z: { type: TagType.Int, value: this._sizeVector.z },
x: { type: TagType.Int, value: sizeVector.x },
y: { type: TagType.Int, value: sizeVector.y },
z: { type: TagType.Int, value: sizeVector.z },
},
},
Name: { type: TagType.String, value: '' },
@ -190,9 +205,9 @@ export class Litematic extends IExporter {
BlockStatePalette: { type: TagType.List, value: { type: TagType.Compound, value: blockStatePalette } },
Size: {
type: TagType.Compound, value: {
x: { type: TagType.Int, value: this._sizeVector.x },
y: { type: TagType.Int, value: this._sizeVector.y },
z: { type: TagType.Int, value: this._sizeVector.z },
x: { type: TagType.Int, value: sizeVector.x },
y: { type: TagType.Int, value: sizeVector.y },
z: { type: TagType.Int, value: sizeVector.z },
},
},
PendingFluidTicks: { type: TagType.List, value: { type: TagType.Int, value: [] } },
@ -209,29 +224,4 @@ export class Litematic extends IExporter {
return nbt;
}
getFormatFilter() {
return {
name: this.getFormatName(),
extensions: ['litematic'],
};
}
getFormatName() {
return 'Litematic';
}
getFileExtension(): string {
return 'litematic';
}
public override export(blockMesh: BlockMesh, filePath: string): boolean {
const bounds = blockMesh.getVoxelMesh()?.getBounds();
this._sizeVector = Vector3.sub(bounds.max, bounds.min).add(1);
const nbt = this._convertToNBT(blockMesh);
saveNBT(nbt, filePath);
return false;
}
}

View File

@ -4,27 +4,19 @@ import { BlockMesh } from '../block_mesh';
import { AppConstants } from '../constants';
import { StatusHandler } from '../status';
import { AppUtil } from '../util';
import { saveNBT } from '../util/nbt_util';
import { NBTUtil } from '../util/nbt_util';
import { Vector3 } from '../vector';
import { IExporter } from './base_exporter';
export class NBTExporter extends IExporter {
public override getFormatFilter() {
return {
name: this.getFormatName(),
extensions: ['nbt'],
name: 'Structure Blocks',
extension: 'nbt',
};
}
public override getFormatName() {
return 'Structure Blocks';
}
public override getFileExtension(): string {
return 'nbt';
}
public override export(blockMesh: BlockMesh, filePath: string): boolean {
public override export(blockMesh: BlockMesh, filePath: string) {
const bounds = blockMesh.getVoxelMesh().getBounds();
const sizeVector = bounds.getDimensions().add(1);
@ -100,12 +92,6 @@ export class NBTExporter extends IExporter {
},
};
saveNBT(nbt, filePath);
return false;
}
private static _getBufferIndex(dimensions: Vector3, vec: Vector3) {
return vec.x + (vec.z * dimensions.x) + (vec.y * dimensions.x * dimensions.z);
NBTUtil.save(nbt, filePath);
}
}

View File

@ -7,21 +7,12 @@ import { ASSERT } from '../util/error_util';
import { IExporter } from './base_exporter';
export class ObjExporter extends IExporter {
public override getFormatFilter(): Electron.FileFilter {
public override getFormatFilter() {
return {
name: 'Wavefront Obj',
extensions: ['obj'],
extension: 'obj',
};
}
public override getFileExtension(): string {
return 'obj';
}
public override getFormatName(): string {
return 'Wavefront OBJ';
}
public override export(blockMesh: BlockMesh, filepath: string) {
ASSERT(path.isAbsolute(filepath));
const parsedPath = path.parse(filepath);
@ -32,8 +23,6 @@ export class ObjExporter extends IExporter {
this._exportOBJ(filepathOBJ, blockMesh, parsedPath.name + '.mtl');
this._exportMTL(filepathMTL, filepathTexture, blockMesh);
return true;
}
private _exportOBJ(filepath: string, blockMesh: BlockMesh, mtlRelativePath: string) {
@ -85,16 +74,16 @@ export class ObjExporter extends IExporter {
buffers.forEach(({ buffer }) => {
positionData.set(buffer.position.data, positionIndex);
positionIndex += buffer.position.data.length;
normalData.set(buffer.normal.data, normalIndex);
normalIndex += buffer.normal.data.length;
texcoordData.set(buffer.texcoord.data, texcoordIndex);
texcoordIndex += buffer.texcoord.data.length;
blockTexcoordData.set(buffer.blockTexcoord.data, blockTexcoordIndex);
blockTexcoordIndex += buffer.blockTexcoord.data.length;
indexData.set(buffer.indices.data, indicesIndex);
indicesIndex += buffer.indices.data.length;
});

View File

@ -5,29 +5,21 @@ import { AppConstants } from '../constants';
import { AppUtil } from '../util';
import { LOG } from '../util/log_util';
import { MathUtil } from '../util/math_util';
import { saveNBT } from '../util/nbt_util';
import { NBTUtil } from '../util/nbt_util';
import { Vector3 } from '../vector';
import { IExporter } from './base_exporter';
export class SchemExporter extends IExporter {
private static SCHEMA_VERSION = 2;
public override getFormatFilter() {
return {
name: this.getFormatName(),
extensions: ['schem'],
name: 'Sponge Schematic',
extension: 'schem',
};
}
public override getFormatName() {
return 'Sponge Schematic';
}
public override getFileExtension(): string {
return 'schem';
}
private static SCHEMA_VERSION = 2;
public override export(blockMesh: BlockMesh, filePath: string): boolean {
public override export(blockMesh: BlockMesh, filePath: string) {
const bounds = blockMesh.getVoxelMesh().getBounds();
const sizeVector = bounds.getDimensions().add(1);
@ -40,7 +32,7 @@ export class SchemExporter extends IExporter {
let blockIndex = 1;
for (const blockName of blockMesh.getBlockPalette()) {
const namespacedBlockName = AppUtil.Text.namespaceBlock(blockName);
blockMapping[namespacedBlockName] = { type: TagType.Int, value: blockIndex };
++blockIndex;
}
@ -54,11 +46,11 @@ export class SchemExporter extends IExporter {
const namespacedBlockName = AppUtil.Text.namespaceBlock(block.blockInfo.name);
blockData[bufferIndex] = blockMapping[namespacedBlockName].value;
}
const blockEncoding: number[] = [];
for (let i = 0; i < blockData.length; ++i) {
let id = blockData[i];
while ((id & -128) != 0) {
blockEncoding.push(id & 127 | 128);
id >>>= 7;
@ -69,7 +61,7 @@ export class SchemExporter extends IExporter {
for (let i = 0; i < blockEncoding.length; ++i) {
blockEncoding[i] = MathUtil.int8(blockEncoding[i]);
}
const nbt: NBT = {
type: TagType.Compound,
name: 'Schematic',
@ -85,7 +77,7 @@ export class SchemExporter extends IExporter {
},
};
saveNBT(nbt, filePath);
NBTUtil.save(nbt, filePath);
return false;
}

View File

@ -4,18 +4,31 @@ import { NBT, TagType } from 'prismarine-nbt';
import { BlockMesh } from '../block_mesh';
import { StatusHandler, StatusID } from '../status';
import { LOG_WARN } from '../util/log_util';
import { saveNBT } from '../util/nbt_util';
import { NBTUtil } from '../util/nbt_util';
import { AppPaths, PathUtil } from '../util/path_util';
import { Vector3 } from '../vector';
import { IExporter } from './base_exporter';
export class Schematic extends IExporter {
private _convertToNBT(blockMesh: BlockMesh) {
const bufferSize = this._sizeVector.x * this._sizeVector.y * this._sizeVector.z;
public override getFormatFilter() {
return {
name: 'Schematic',
extension: 'schematic',
};
}
public override export(blockMesh: BlockMesh, filePath: string) {
const nbt = this._convertToNBT(blockMesh);
NBTUtil.save(nbt, filePath);
}
private _convertToNBT(blockMesh: BlockMesh): NBT {
const bounds = blockMesh.getVoxelMesh().getBounds();
const sizeVector = Vector3.sub(bounds.max, bounds.min).add(1);
const bufferSize = sizeVector.x * sizeVector.y * sizeVector.z;
const blocksData = Array<number>(bufferSize);
const metaData = Array<number>(bufferSize);
const bounds = blockMesh.getVoxelMesh().getBounds();
const schematicBlocks: { [blockName: string]: { id: number, meta: number, name: string } } = JSON.parse(
fs.readFileSync(PathUtil.join(AppPaths.Get.resources, './block_ids.json'), 'utf8'),
@ -26,7 +39,7 @@ export class Schematic extends IExporter {
let numBlocksUnsupported = 0;
for (const block of blocks) {
const indexVector = Vector3.sub(block.voxel.position, bounds.min);
const index = this._getBufferIndex(indexVector, this._sizeVector);
const index = this._getBufferIndex(indexVector, sizeVector);
if (block.blockInfo.name in schematicBlocks) {
const schematicBlock = schematicBlocks[block.blockInfo.name];
blocksData[index] = new Int8Array([schematicBlock.id])[0];
@ -52,9 +65,9 @@ export class Schematic extends IExporter {
type: TagType.Compound,
name: 'Schematic',
value: {
Width: { type: TagType.Short, value: this._sizeVector.x },
Height: { type: TagType.Short, value: this._sizeVector.y },
Length: { type: TagType.Short, value: this._sizeVector.z },
Width: { type: TagType.Short, value: sizeVector.x },
Height: { type: TagType.Short, value: sizeVector.y },
Length: { type: TagType.Short, value: sizeVector.z },
Materials: { type: TagType.String, value: 'Alpha' },
Blocks: { type: TagType.ByteArray, value: blocksData },
Data: { type: TagType.ByteArray, value: metaData },
@ -66,32 +79,7 @@ export class Schematic extends IExporter {
return nbt;
}
_getBufferIndex(vec: Vector3, sizeVector: Vector3) {
private _getBufferIndex(vec: Vector3, sizeVector: Vector3) {
return (sizeVector.z * sizeVector.x * vec.y) + (sizeVector.x * vec.z) + vec.x;
}
getFormatFilter() {
return {
name: this.getFormatName(),
extensions: ['schematic'],
};
}
getFormatName() {
return 'Schematic';
}
getFileExtension(): string {
return 'schematic';
}
public override export(blockMesh: BlockMesh, filePath: string): boolean {
const bounds = blockMesh.getVoxelMesh().getBounds();
this._sizeVector = Vector3.sub(bounds.max, bounds.min).add(1);
const nbt = this._convertToNBT(blockMesh);
saveNBT(nbt, filePath);
return false;
}
}

View File

@ -1,6 +1,9 @@
import { Mesh } from '../mesh';
export abstract class IFileImporter<T> {
protected readonly _filepath: string;
export abstract class IImporter {
abstract parseFile(filePath: string): void;
abstract toMesh(): Mesh;
public constructor(filepath: string) {
this._filepath = filepath;
}
public abstract load(): T;
}

View File

@ -1,16 +1,24 @@
import { ASSERT } from '../util/error_util';
import { IImporter } from './base_importer';
import { Mesh } from '../mesh';
import { VoxelMesh } from '../voxel_mesh';
import { IFileImporter } from './base_importer';
import { ObjImporter } from './obj_importer';
import { VoxImporter } from './vox_importer';
export type TImporters = 'obj';
export type TImporters = 'obj' | 'vox';
export class ImporterFactor {
public static GetImporter(importer: TImporters): IImporter {
public static GetImporter(importer: TImporters, filename: string): { type: 'Mesh', importer: IFileImporter<Mesh> } | { type: 'VoxelMesh', importer: IFileImporter<VoxelMesh> } {
switch (importer) {
case 'obj':
return new ObjImporter();
default:
ASSERT(false);
return {
type: 'Mesh',
importer: new ObjImporter(filename),
};
case 'vox':
return {
type: 'VoxelMesh',
importer: new VoxImporter(filename),
};
}
}
}

View File

@ -14,9 +14,9 @@ import { RegExpBuilder } from '../util/regex_util';
import { REGEX_NZ_ANY } from '../util/regex_util';
import { REGEX_NUMBER } from '../util/regex_util';
import { Vector3 } from '../vector';
import { IImporter } from './base_importer';
import { IFileImporter } from './base_importer';
export class ObjImporter extends IImporter {
export class ObjImporter extends IFileImporter<Mesh> {
private _vertices: Vector3[] = [];
private _normals: Vector3[] = [];
private _uvs: UV[] = [];
@ -24,8 +24,9 @@ export class ObjImporter extends IImporter {
private _materials: Map<string, SolidMaterial | TexturedMaterial>;
public constructor() {
super();
public constructor(filepath: string) {
super(filepath);
this._materials = new Map();
this._materials.set('DEFAULT_UNASSIGNED', {
type: MaterialType.solid,
@ -286,10 +287,10 @@ export class ObjImporter extends IImporter {
},
];
override parseFile(filePath: string) {
this._objPath = path.parse(filePath);
public override load(): Mesh {
this._objPath = path.parse(this._filepath);
this._parseOBJ(filePath);
this._parseOBJ(this._filepath);
if (this._mtlLibs.length === 0) {
StatusHandler.Get.add('warning', 'Could not find associated .mtl file');
@ -304,9 +305,7 @@ export class ObjImporter extends IImporter {
this._parseMTL();
LOG('Materials', this._materials);
}
override toMesh(): Mesh {
return new Mesh(this._vertices, this._normals, this._uvs, this._tris, this._materials);
}

View File

@ -0,0 +1,38 @@
import fs from 'fs';
import readVox from 'vox-reader';
import { RGBA_255, RGBAUtil } from '../colour';
import { Vector3 } from '../vector';
import { VoxelMesh } from '../voxel_mesh';
import { IFileImporter } from './base_importer';
export class VoxImporter extends IFileImporter<VoxelMesh> {
public override load(): VoxelMesh {
const fileBuffer = fs.readFileSync(this._filepath);
const voxelMesh = new VoxelMesh({
enableAmbientOcclusion: true,
voxelOverlapRule: 'first',
});
const voxObject = readVox(fileBuffer);
const colourPalette = voxObject.rgba.values as RGBA_255[];
colourPalette.map((x) => RGBAUtil.fromRGBA255(x));
const size = voxObject.size as { x: number, y: number, z: number };
const halfSizeVector = new Vector3(size.x, size.z, size.y) // Swap y and z axis
.divScalar(2)
.floor();
const voxels = voxObject.xyzi.values as { x: number, y: number, z: number, i: number }[];
voxels.forEach((voxel) => {
const position = new Vector3(voxel.x, voxel.z, voxel.y) // Swap y and z axis
.sub(halfSizeVector);
const colour = RGBAUtil.fromRGBA255(colourPalette[voxel.i]);
voxelMesh.addVoxel(position, colour);
});
return voxelMesh;
}
}

View File

@ -63,7 +63,11 @@ export class Renderer {
private _voxelSize: number = 1.0;
private _gridOffset: Vector3 = new Vector3(0, 0, 0);
private _modelsAvailable: number;
private _modelsAvailable = {
mesh: false,
voxelMesh: false,
blockMesh: false,
};
private _materialBuffers: Map<string, {
material: InternalSolidMaterial | InternalTextureMaterial,
@ -100,7 +104,6 @@ export class Renderer {
this._backgroundColour = AppConfig.Get.VIEWPORT_BACKGROUND_COLOUR;
this._modelsAvailable = 0;
this._materialBuffers = new Map();
this._gridBuffers = { x: {}, y: {}, z: {} };
@ -206,7 +209,9 @@ export class Renderer {
public clearMesh() {
this._materialBuffers = new Map();
this._modelsAvailable = 0;
this._modelsAvailable.mesh = false;
this._modelsAvailable.voxelMesh = false;
this._modelsAvailable.blockMesh = false;
this.setModelToUse(MeshType.None);
}
@ -287,7 +292,9 @@ export class Renderer {
this._gridBuffers.y[MeshType.TriangleMesh] = DebugGeometryTemplates.gridY(params.dimensions);
this._gridBuffers.z[MeshType.TriangleMesh] = DebugGeometryTemplates.gridZ(params.dimensions);
this._modelsAvailable = 1;
this._modelsAvailable.mesh = true;
this._modelsAvailable.voxelMesh = false;
this._modelsAvailable.blockMesh = false;
this.setModelToUse(MeshType.TriangleMesh);
}
@ -318,7 +325,8 @@ export class Renderer {
this._gridBuffers.y[MeshType.VoxelMesh] = DebugGeometryTemplates.gridY(Vector3.mulScalar(dimensions, voxelSize), voxelSize);
this._gridBuffers.z[MeshType.VoxelMesh] = DebugGeometryTemplates.gridZ(Vector3.mulScalar(dimensions, voxelSize), voxelSize);
this._modelsAvailable = 2;
this._modelsAvailable.voxelMesh = true;
this._modelsAvailable.blockMesh = false;
this.setModelToUse(MeshType.VoxelMesh);
}
}
@ -365,7 +373,7 @@ export class Renderer {
this._gridBuffers.y[MeshType.BlockMesh] = this._gridBuffers.y[MeshType.VoxelMesh];
this._modelsAvailable = 3;
this._modelsAvailable.blockMesh = true;
this.setModelToUse(MeshType.BlockMesh);
}
}
@ -502,7 +510,10 @@ export class Renderer {
}
public setModelToUse(meshType: MeshType) {
const isModelAvailable = this._modelsAvailable >= meshType;
const isModelAvailable =
(meshType === MeshType.TriangleMesh && this._modelsAvailable.mesh) ||
(meshType === MeshType.VoxelMesh && this._modelsAvailable.voxelMesh) ||
(meshType === MeshType.BlockMesh && this._modelsAvailable.blockMesh);
if (isModelAvailable) {
this._meshToUse = meshType;
}

View File

@ -65,6 +65,15 @@ export abstract class ConfigUIElement<T, F> extends BaseUIElement<F> {
return this._value;
}
/**
* Add a delegate that will be called when the value/enabled state changes.
*/
public addStateChangedListener(delegate: () => void) {
this._onValueChangedListeners.push(delegate);
this._onEnabledChangedListeners.push(delegate);
return this;
}
/**
* Add a delegate that will be called when the value changes.
*/

View File

@ -51,7 +51,7 @@ export class FileInputElement extends ConfigUIElement<string, HTMLDivElement> {
title: 'Load file',
buttonLabel: 'Load',
filters: [{
name: 'Model file',
name: `Model file (${this._fileExtensions.map((x) => `.${x}`).join(', ')})`,
extensions: this._fileExtensions,
}],
});

View File

@ -0,0 +1,140 @@
import { shell } from 'electron';
import { AppConfig } from '../../config';
import { ASSERT } from '../../util/error_util';
import { LOG, LOG_ERROR } from '../../util/log_util';
import { AppPaths, PathUtil } from '../../util/path_util';
import { UIUtil } from '../../util/ui_util';
import { BaseUIElement } from './base_element';
import { ToolbarItemElement } from './toolbar_item';
export class HeaderUIElement extends BaseUIElement<HTMLDivElement> {
private static _instance: HeaderUIElement;
public static get Get() {
return this._instance || (this._instance = new this());
}
private _githubButton: ToolbarItemElement;
private _bugButton: ToolbarItemElement;
private constructor() {
super();
this._githubButton = new ToolbarItemElement({ icon: 'github' })
.onClick(() => {
shell.openExternal('https://github.com/LucasDower/ObjToSchematic');
});
this._bugButton = new ToolbarItemElement({ icon: 'bug' })
.onClick(() => {
shell.openExternal('https://github.com/LucasDower/ObjToSchematic/issues');
});
}
// Header element shouldn't be
protected override _onEnabledChanged(): void {
return;
}
public override generateHTML(): string {
return `
<div class="property">
<div class="col-container header-cols">
<div class="col-container">
<div class="col-item">
<img class="logo" src="${PathUtil.join(AppPaths.Get.static, 'icon.png')}">
</div>
<div class="col-item">
<div class="row-container">
<div class="row-item title">
ObjToSchematic
</div>
<div class="row-item subtitle" id="update-checker">
Up-to-date
</div>
</div>
</div>
</div>
<div class="col-container">
<div class="col-item">
${this._githubButton.generateHTML()}
</div>
<div class="col-item">
${this._bugButton.generateHTML()}
</div>
</div>
</div>
</div>
`;
}
public override registerEvents(): void {
this._githubButton.registerEvents();
this._bugButton.registerEvents();
}
public override finalise(): void {
const updateElement = UIUtil.getElementById('update-checker') as HTMLDivElement;
updateElement.style.animation = 'pulse-opacity 1.5s infinite';
updateElement.innerHTML = '<i style="animation: pulse-opacity 1.5s infinite;">Checking for updates...</i>';
fetch('https://api.github.com/repos/LucasDower/ObjToSchematic/releases/latest')
.then((response) => response.json())
.then((data) => {
const latest: string = data.tag_name; // e.g. v0.7.0
const versionString = latest.substring(1); // e.g. 0.7.0
const versionValues = versionString.split('.').map((x) => parseInt(x));
// Is the local version older than the latest release on GitHub?
let isGitHubVersionNewer = false;
if (versionValues[0] > AppConfig.Get.MAJOR_VERSION) {
isGitHubVersionNewer = true;
} else {
if (versionValues[1] > AppConfig.Get.MINOR_VERSION) {
isGitHubVersionNewer = true;
} else {
if (versionValues[2] > AppConfig.Get.HOTFIX_VERSION) {
isGitHubVersionNewer = true;
}
}
}
/*
let isLocalVersionNewer = false;
if (versionValues[0] < AppConfig.Get.MAJOR_VERSION) {
isLocalVersionNewer = true;
} else {
if (versionValues[1] < AppConfig.Get.MINOR_VERSION) {
isLocalVersionNewer = true;
} else {
if (versionValues[2] > AppConfig.Get.HOTFIX_VERSION) {
isLocalVersionNewer = true;
}
}
}
*/
LOG(`[VERSION]: Current: ${[AppConfig.Get.MAJOR_VERSION, AppConfig.Get.MINOR_VERSION, AppConfig.Get.HOTFIX_VERSION]}, Latest: ${versionValues}`);
updateElement.style.animation = '';
if (isGitHubVersionNewer) {
updateElement.innerHTML = `<a href="#" id="update-link">New ${versionString} update available!</a>`;
const linkElement = UIUtil.getElementById('update-link') as HTMLLinkElement;
linkElement.onclick = () => {
shell.openExternal('https://github.com/LucasDower/ObjToSchematic/releases/latest');
};
} else {
// Either using most up-to-date version or local version is newer (using unreleased dev or alpha build)
updateElement.innerHTML = `Version up-to-date!`;
}
})
.catch((error) => {
LOG_ERROR(error);
updateElement.style.animation = '';
updateElement.innerHTML = 'Could not check for updates';
});
}
}

View File

@ -1,4 +1,5 @@
import fs from 'fs';
import path from 'path';
import { AppContext } from '../app_context';
import { FallableBehaviour } from '../block_mesh';
@ -19,10 +20,12 @@ import { CheckboxElement } from './elements/checkbox';
import { ComboBoxElement, ComboBoxItem } from './elements/combobox';
import { ConfigUIElement } from './elements/config_element';
import { FileInputElement } from './elements/file_input';
import { HeaderUIElement } from './elements/header_element';
import { OutputElement } from './elements/output';
import { SliderElement } from './elements/slider';
import { ToolbarItemElement } from './elements/toolbar_item';
import { VectorSpinboxElement } from './elements/vector_spinbox';
import { UIUtil } from '../util/ui_util';
export interface Group {
label: string;
@ -44,19 +47,20 @@ export class UI {
label: 'Import',
elements: {
'input': new FileInputElement()
.setFileExtensions(['obj'])
.setLabel('Wavefront .obj file'),
.setFileExtensions(['obj', 'vox'])
.setLabel('Model file'),
'rotation': new VectorSpinboxElement()
.setLabel('Rotation')
.setWrap(360)
.setUnits('°'),
.setUnits('°')
.setShouldObeyGroupEnables(false),
},
elementsOrder: ['input', 'rotation'],
submitButton: new ButtonElement()
.setOnClick(() => {
this._appContext.do(EAction.Import);
})
.setLabel('Load mesh'),
.setLabel('Load model'),
output: new OutputElement(),
},
'materials': {
@ -293,7 +297,7 @@ export class UI {
return Renderer.Get.getActiveMeshType() === MeshType.TriangleMesh;
})
.isEnabled(() => {
return Renderer.Get.getModelsAvailable() >= MeshType.TriangleMesh;
return Renderer.Get.getModelsAvailable().mesh;
}),
'voxelMesh': new ToolbarItemElement({ icon: 'voxel' })
.onClick(() => {
@ -303,7 +307,7 @@ export class UI {
return Renderer.Get.getActiveMeshType() === MeshType.VoxelMesh;
})
.isEnabled(() => {
return Renderer.Get.getModelsAvailable() >= MeshType.VoxelMesh;
return Renderer.Get.getModelsAvailable().voxelMesh;
}),
'blockMesh': new ToolbarItemElement({ icon: 'block' })
.onClick(() => {
@ -313,7 +317,7 @@ export class UI {
return Renderer.Get.getActiveMeshType() === MeshType.BlockMesh;
})
.isEnabled(() => {
return Renderer.Get.getModelsAvailable() >= MeshType.BlockMesh;
return Renderer.Get.getModelsAvailable().blockMesh;
}),
},
elementsOrder: ['mesh', 'voxelMesh', 'blockMesh'],
@ -416,6 +420,10 @@ export class UI {
public constructor(appContext: AppContext) {
this._appContext = appContext;
for (let i = 0; i < EAction.MAX; ++i) {
this._groupEnableStates.set(i, true);
}
}
public tick(isBusy: boolean) {
@ -450,7 +458,7 @@ export class UI {
<div class="h-div">
</div>
</div>
<div class="group-heading">
<div class="group-heading" id="group-heading-${group.label}">
${group.label.toUpperCase()}
</div>
<div style="flex-grow: 1">
@ -468,7 +476,7 @@ export class UI {
}
document.getElementById('properties')!.innerHTML = `<div class="container">
` + itemHTML + `</div>`;
` + HeaderUIElement.Get.generateHTML() + itemHTML + `</div>`;
// Build toolbar
let toolbarHTML = '';
@ -498,6 +506,11 @@ export class UI {
toolbarHTML += '</div>';
document.getElementById('toolbar')!.innerHTML = toolbarHTML;
this._ui.import.elements.input.addStateChangedListener(() => {
const parsed = path.parse(this._ui.import.elements.input.getValue());
this._ui.import.elements.rotation.setEnabled(parsed.ext === '.obj', false);
});
}
public cacheValues(action: EAction) {
@ -556,6 +569,13 @@ export class UI {
`;
}
public scrollIntoView(action: EAction) {
const group = this._getEActionGroup(action);
const elementId = `group-heading-${group.label}`;
UIUtil.getElementById(elementId).scrollIntoView({ behavior: 'smooth' });
}
public getActionOutput(action: EAction) {
const group = this._getEActionGroup(action);
return group.output;
@ -567,6 +587,9 @@ export class UI {
}
public registerEvents() {
HeaderUIElement.Get.registerEvents();
HeaderUIElement.Get.finalise();
for (const groupName in this._ui) {
const group = this._uiDull[groupName];
for (const elementName in group.elements) {
@ -601,17 +624,43 @@ export class UI {
return this._uiDull;
}
/*
public enableTo(action: EAction) {
for (let i = 0; i <= action; ++i) {
this.enable(i);
}
}
*/
private _groupEnableStates = new Map<EAction, boolean>();
private _cachedGroupEnableStates = new Map<EAction, boolean>();
public cacheGroupEnableStates() {
this._cachedGroupEnableStates.clear();
this._groupEnableStates.forEach((state, action) => {
this._cachedGroupEnableStates.set(action, state);
});
}
public restoreGroupEnableStates() {
this._groupEnableStates.clear();
this._cachedGroupEnableStates.forEach((state, action) => {
this._groupEnableStates.set(action, state);
if (state) {
this.enable(action);
} else {
this.disable(action);
}
});
}
public enable(action: EAction) {
if (action >= EAction.MAX) {
return;
}
this._groupEnableStates.set(action, true);
LOG('[UI]: Enabling', action);
const group = this._getEActionGroup(action);
for (const compName in group.elements) {
@ -630,6 +679,7 @@ export class UI {
}
for (let i = action; i < EAction.MAX; ++i) {
this._groupEnableStates.set(i, false);
const group = this._getEActionGroup(i);
//LOG('[UI]: Disabling', group.label);
for (const compName in group.elements) {

View File

@ -5,10 +5,13 @@ import zlib from 'zlib';
import { ASSERT } from './error_util';
export function saveNBT(nbt: NBT, filepath: string) {
ASSERT(path.isAbsolute(filepath), '[saveNBT]: filepath is not absolute');
export namespace NBTUtil {
export function save(nbt: NBT, filepath: string) {
ASSERT(path.isAbsolute(filepath), '[saveNBT]: filepath is not absolute');
const uncompressedBuffer = writeUncompressed(nbt, 'big');
const compressedBuffer = zlib.gzipSync(uncompressedBuffer);
fs.writeFileSync(filepath, compressedBuffer);
}
const uncompressedBuffer = writeUncompressed(nbt, 'big');
const compressedBuffer = zlib.gzipSync(uncompressedBuffer);
fs.writeFileSync(filepath, compressedBuffer);
}

View File

@ -4,7 +4,7 @@ import { AppError } from './util/error_util';
import { WorkerClient } from './worker_client';
import { TFromWorkerMessage, TToWorkerMessage } from './worker_types';
export function doWork(message: TToWorkerMessage): TFromWorkerMessage {
export function doWork(message: TToWorkerMessage, onFinish: (result: TFromWorkerMessage) => void): void {
StatusHandler.Get.clear();
if (message.action !== 'RenderNextVoxelMeshChunk' && message.action !== 'RenderNextBlockMeshChunk') {
@ -14,78 +14,35 @@ export function doWork(message: TToWorkerMessage): TFromWorkerMessage {
try {
switch (message.action) {
case 'Init':
return {
action: 'Init',
result: WorkerClient.Get.init(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
WorkerClient.Get.init(message.params, onFinish);
break;
case 'Import':
return {
action: 'Import',
result: WorkerClient.Get.import(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
WorkerClient.Get.import(message.params, onFinish);
break;
case 'SetMaterials':
return {
action: 'SetMaterials',
result: WorkerClient.Get.setMaterials(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
WorkerClient.Get.setMaterials(message.params, onFinish);
break;
case 'RenderMesh':
return {
action: 'RenderMesh',
result: WorkerClient.Get.renderMesh(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
WorkerClient.Get.renderMesh(message.params, onFinish);
break;
case 'Voxelise':
return {
action: 'Voxelise',
result: WorkerClient.Get.voxelise(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
/*
case 'RenderVoxelMesh':
return {
action: 'RenderVoxelMesh',
result: WorkerClient.Get.renderVoxelMesh(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
*/
WorkerClient.Get.voxelise(message.params, onFinish);
break;
case 'RenderNextVoxelMeshChunk':
return {
action: 'RenderNextVoxelMeshChunk',
result: WorkerClient.Get.renderChunkedVoxelMesh(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
WorkerClient.Get.renderChunkedVoxelMesh(message.params, onFinish);
break;
case 'Assign':
return {
action: 'Assign',
result: WorkerClient.Get.assign(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
/*
case 'RenderBlockMesh':
return {
action: 'RenderBlockMesh',
result: WorkerClient.Get.renderBlockMesh(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
*/
WorkerClient.Get.assign(message.params, onFinish);
break;
case 'RenderNextBlockMeshChunk':
return {
action: 'RenderNextBlockMeshChunk',
result: WorkerClient.Get.renderChunkedBlockMesh(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
WorkerClient.Get.renderChunkedBlockMesh(message.params, onFinish);
break;
case 'Export':
return {
action: 'Export',
result: WorkerClient.Get.export(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
result: WorkerClient.Get.export(message.params, onFinish);
break;
}
} catch (e: any) {
return { action: e instanceof AppError ? 'KnownError' : 'UnknownError', error: e as Error };
onFinish({ action: e instanceof AppError ? 'KnownError' : 'UnknownError', error: e as Error });
}
}

View File

@ -1,3 +1,5 @@
import path from 'path';
import { Atlas } from './atlas';
import { BlockMesh } from './block_mesh';
import { BufferGenerator } from './buffer';
@ -5,8 +7,10 @@ import { EAppEvent, EventManager } from './event';
import { IExporter } from './exporters/base_exporter';
import { ExporterFactory } from './exporters/exporters';
import { ObjImporter } from './importers/obj_importer';
import { VoxImporter } from './importers/vox_importer';
import { Mesh } from './mesh';
import { ProgressManager, TTaskHandle } from './progress';
import { StatusHandler } from './status';
import { ASSERT } from './util/error_util';
import { Logger } from './util/log_util';
import { VoxelMesh } from './voxel_mesh';
@ -32,7 +36,7 @@ export class WorkerClient {
/**
* This function should only be called if the client is using the worker.
*/
public init(params: InitParams.Input): InitParams.Output {
public init(params: InitParams.Input, onFinish: (result: TFromWorkerMessage) => void): void {
EventManager.Get.add(EAppEvent.onTaskStart, (e: any) => {
const message: TFromWorkerMessage = {
action: 'Progress',
@ -67,44 +71,86 @@ export class WorkerClient {
postMessage(message);
});
return {};
onFinish({
action: 'Init',
statusMessages: StatusHandler.Get.getAllStatusMessages(),
result: {},
});
}
public import(params: ImportParams.Input): ImportParams.Output {
const importer = new ObjImporter();
importer.parseFile(params.filepath);
this._loadedMesh = importer.toMesh();
this._loadedMesh.processMesh(params.rotation.y, params.rotation.x, params.rotation.z);
public import(params: ImportParams.Input, onFinish: (result: TFromWorkerMessage) => void): void {
const parsedPath = path.parse(params.filepath);
return {
triangleCount: this._loadedMesh.getTriangleCount(),
dimensions: this._loadedMesh.getBounds().getDimensions(),
materials: this._loadedMesh.getMaterials(),
};
this._voxelMeshChunkIndex = 0;
switch (parsedPath.ext) {
case '.obj': {
const importer = new ObjImporter(params.filepath);
this._loadedMesh = importer.load();
this._loadedMesh.processMesh(params.rotation.y, params.rotation.x, params.rotation.z);
onFinish({
action: 'Import',
statusMessages: StatusHandler.Get.getAllStatusMessages(),
result: {
type: 'Mesh',
triangleCount: this._loadedMesh.getTriangleCount(),
dimensions: this._loadedMesh.getBounds().getDimensions(),
materials: this._loadedMesh.getMaterials(),
},
});
break;
}
case '.vox': {
const importer = new VoxImporter(params.filepath);
this._loadedVoxelMesh = importer.load();
onFinish({
action: 'Import',
statusMessages: StatusHandler.Get.getAllStatusMessages(),
result: {
type: 'VoxelMesh',
},
});
break;
}
}
}
public setMaterials(params: SetMaterialsParams.Input): SetMaterialsParams.Output {
public setMaterials(params: SetMaterialsParams.Input, onFinish: (result: TFromWorkerMessage) => void): void {
ASSERT(this._loadedMesh !== undefined);
this._loadedMesh.setMaterials(params.materials);
this._voxelMeshChunkIndex = 0;
return {
materials: this._loadedMesh.getMaterials(),
materialsChanged: Array.from(params.materials.keys()), // TODO: Change to actual materials changed
};
onFinish({
action: 'SetMaterials',
statusMessages: StatusHandler.Get.getAllStatusMessages(),
result: {
materials: this._loadedMesh.getMaterials(),
materialsChanged: Array.from(params.materials.keys()), // TODO: Change to actual materials changed
},
});
}
public renderMesh(params: RenderMeshParams.Input): RenderMeshParams.Output {
public renderMesh(params: RenderMeshParams.Input, onFinish: (result: TFromWorkerMessage) => void): void {
ASSERT(this._loadedMesh !== undefined);
return {
buffers: BufferGenerator.fromMesh(this._loadedMesh),
dimensions: this._loadedMesh.getBounds().getDimensions(),
};
this._voxelMeshChunkIndex = 0;
onFinish({
action: 'RenderMesh',
statusMessages: StatusHandler.Get.getAllStatusMessages(),
result: {
buffers: BufferGenerator.fromMesh(this._loadedMesh),
dimensions: this._loadedMesh.getBounds().getDimensions(),
},
});
}
public voxelise(params: VoxeliseParams.Input): VoxeliseParams.Output {
public voxelise(params: VoxeliseParams.Input, onFinish: (result: TFromWorkerMessage) => void): void {
ASSERT(this._loadedMesh !== undefined);
const voxeliser: IVoxeliser = VoxeliserFactory.GetVoxeliser(params.voxeliser);
@ -112,13 +158,17 @@ export class WorkerClient {
this._voxelMeshChunkIndex = 0;
return {
};
onFinish({
action: 'Voxelise',
statusMessages: StatusHandler.Get.getAllStatusMessages(),
result: {
},
});
}
private _voxelMeshChunkIndex = 0;
private _voxelMeshProgressHandle?: TTaskHandle;
public renderChunkedVoxelMesh(params: RenderNextVoxelMeshChunkParams.Input): RenderNextVoxelMeshChunkParams.Output {
public renderChunkedVoxelMesh(params: RenderNextVoxelMeshChunkParams.Input, onFinish: (result: TFromWorkerMessage) => void): void {
ASSERT(this._loadedVoxelMesh !== undefined);
const isFirstChunk = this._voxelMeshChunkIndex === 0;
@ -139,29 +189,37 @@ export class WorkerClient {
}
}
return {
buffer: buffer,
dimensions: this._loadedVoxelMesh.getBounds().getDimensions(),
voxelSize: 8.0 / params.desiredHeight,
moreVoxelsToBuffer: buffer.moreVoxelsToBuffer,
isFirstChunk: isFirstChunk,
};
onFinish({
action: 'RenderNextVoxelMeshChunk',
statusMessages: StatusHandler.Get.getAllStatusMessages(),
result: {
buffer: buffer,
dimensions: this._loadedVoxelMesh.getBounds().getDimensions(),
voxelSize: 8.0 / params.desiredHeight,
moreVoxelsToBuffer: buffer.moreVoxelsToBuffer,
isFirstChunk: isFirstChunk,
},
});
}
public assign(params: AssignParams.Input): AssignParams.Output {
public assign(params: AssignParams.Input, onFinish: (result: TFromWorkerMessage) => void): void {
ASSERT(this._loadedVoxelMesh !== undefined);
this._loadedBlockMesh = BlockMesh.createFromVoxelMesh(this._loadedVoxelMesh, params);
this._blockMeshChunkIndex = 0;
return {
};
onFinish({
action: 'Assign',
statusMessages: StatusHandler.Get.getAllStatusMessages(),
result: {
},
});
}
private _blockMeshChunkIndex = 0;
//private _blockMeshProgressHandle?: TTaskHandle;
public renderChunkedBlockMesh(params: RenderNextBlockMeshChunkParams.Input): RenderNextBlockMeshChunkParams.Output {
public renderChunkedBlockMesh(params: RenderNextBlockMeshChunkParams.Input, onFinish: (result: TFromWorkerMessage) => void): void {
ASSERT(this._loadedBlockMesh !== undefined);
const isFirstChunk = this._blockMeshChunkIndex === 0;
@ -172,57 +230,38 @@ export class WorkerClient {
const buffer = this._loadedBlockMesh.getChunkedBuffer(this._blockMeshChunkIndex);
++this._blockMeshChunkIndex;
/*
if (this._blockMeshProgressHandle !== undefined) {
if (buffer.moreBlocksToBuffer) {
ProgressManager.Get.progress(this._blockMeshProgressHandle, buffer.progress);
} else {
ProgressManager.Get.end(this._blockMeshProgressHandle);
this._blockMeshProgressHandle = undefined;
}
}
*/
const atlas = Atlas.load(params.textureAtlas);
ASSERT(atlas !== undefined);
return {
buffer: buffer,
dimensions: this._loadedBlockMesh.getVoxelMesh().getBounds().getDimensions(),
atlasTexturePath: atlas.getAtlasTexturePath(),
atlasSize: atlas.getAtlasSize(),
moreBlocksToBuffer: buffer.moreBlocksToBuffer,
isFirstChunk: isFirstChunk,
};
onFinish({
action: 'RenderNextBlockMeshChunk',
statusMessages: StatusHandler.Get.getAllStatusMessages(),
result: {
buffer: buffer,
dimensions: this._loadedBlockMesh.getVoxelMesh().getBounds().getDimensions(),
atlasTexturePath: atlas.getAtlasTexturePath(),
atlasSize: atlas.getAtlasSize(),
moreBlocksToBuffer: buffer.moreBlocksToBuffer,
isFirstChunk: isFirstChunk,
},
});
}
/*
public renderBlockMesh(params: RenderBlockMeshParams.Input): RenderBlockMeshParams.Output {
ASSERT(this._loadedBlockMesh !== undefined);
const atlas = Atlas.load(params.textureAtlas);
ASSERT(atlas !== undefined);
return {
buffer: this._loadedBlockMesh.getBuffer(),
dimensions: this._loadedBlockMesh.getVoxelMesh().getBounds().getDimensions(),
atlasTexturePath: atlas.getAtlasTexturePath(),
atlasSize: atlas.getAtlasSize(),
};
}
*/
public export(params: ExportParams.Input): ExportParams.Output {
public export(params: ExportParams.Input, onFinish: (result: TFromWorkerMessage) => void): void {
ASSERT(this._loadedBlockMesh !== undefined);
const exporter: IExporter = ExporterFactory.GetExporter(params.exporter);
const fileExtension = '.' + exporter.getFileExtension();
const fileExtension = '.' + exporter.getFormatFilter().extension;
if (!params.filepath.endsWith(fileExtension)) {
params.filepath += fileExtension;
}
exporter.export(this._loadedBlockMesh, params.filepath);
return {
};
onFinish({
action: 'Export',
statusMessages: StatusHandler.Get.getAllStatusMessages(),
result: {
},
});
}
}

View File

@ -107,10 +107,13 @@ export class WorkerController {
if (AppConfig.Get.USE_WORKER_THREAD) {
this._worker.postMessage(this._jobPending.payload);
} else {
const result = doWork(this._jobPending.payload);
if (this._jobPending.callback) {
this._jobPending.callback(result);
}
doWork(this._jobPending.payload, (result: TFromWorkerMessage) => {
ASSERT(this._jobPending !== undefined);
if (this._jobPending.callback) {
this._jobPending.callback(result);
}
});
}
}
}

View File

@ -1,5 +1,7 @@
const workerInstance = require('./worker');
addEventListener('message', (e) => {
postMessage(workerInstance.doWork(e.data));
workerInstance.doWork(e.data, (result: any) => { // result: TFromWorkerMessage
postMessage(result);
});
});

View File

@ -27,9 +27,12 @@ export namespace ImportParams {
}
export type Output = {
type: 'Mesh',
triangleCount: number,
dimensions: Vector3,
materials: MaterialMap
} | {
type: 'VoxelMesh',
}
}

View File

@ -18,6 +18,7 @@
--prop-sunken: hsl(0, 0%, 8%);
--text-standard: hsl(0, 0%, 66%);
--text-muted: hsl(0, 0%, 45%);
--text-disabled: hsl(0, 0%, 33%);
--vertical-divider: hsl(0, 0%, 14%);
@ -797,10 +798,55 @@ svg {
display: flex;
flex-direction: row;
gap: 2px;
align-items: center;
}
.col-item {
display: flex;
flex-direction: column;
align-items: center;
}
.logo {
width: 32px;
margin-right: 10px;
}
.title {
font-size: 110%;
}
.subtitle {
font-size: 90%;
font-weight: 300;
color: var(--text-muted);
}
.header-cols {
justify-content: space-between;
width: 100%;
padding-top: 4px;
}
.button-loading {
box-shadow: 0 0 0 0 rgba(0, 0, 0, 1);
}
@keyframes pulse-opacity {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
a {
font-weight: 400;
color: var(--prop-accent-hovered)
}

View File

@ -40,7 +40,7 @@ const baseConfig: THeadlessConfig = {
},
};
test('FULL Obj->Obj', () => {
test('FULL Obj->Litematic', () => {
TEST_PREAMBLE();
const config: THeadlessConfig = baseConfig;

View File

@ -40,7 +40,7 @@ const baseConfig: THeadlessConfig = {
},
};
test('FULL Obj->Obj', () => {
test('FULL Obj->Schem', () => {
TEST_PREAMBLE();
const config: THeadlessConfig = baseConfig;

View File

@ -13,9 +13,11 @@ test('Random-dither', () => {
config.assign.dithering = 'random';
const worker = WorkerClient.Get;
worker.import(headlessConfig.import);
worker.voxelise(headlessConfig.voxelise);
worker.assign(headlessConfig.assign);
expect(StatusHandler.Get.hasId(StatusID.SchematicUnsupportedBlocks)).toBe(false);
worker.import(headlessConfig.import, () => {
worker.voxelise(headlessConfig.voxelise, () => {
worker.assign(headlessConfig.assign, () => {
expect(StatusHandler.Get.hasId(StatusID.SchematicUnsupportedBlocks)).toBe(false);
});
});
});
});

View File

@ -15,10 +15,13 @@ test('Schematic-friendly Palette', () => {
config.export.filepath = PathUtil.join(AppPaths.Get.testData, '../out/friendly.schematic');
const worker = WorkerClient.Get;
worker.import(headlessConfig.import);
worker.voxelise(headlessConfig.voxelise);
worker.assign(headlessConfig.assign);
worker.export(headlessConfig.export);
expect(StatusHandler.Get.hasId(StatusID.SchematicUnsupportedBlocks)).toBe(false);
worker.import(headlessConfig.import, () => {
worker.voxelise(headlessConfig.voxelise, () => {
worker.assign(headlessConfig.assign, () => {
worker.export(headlessConfig.export, () => {
expect(StatusHandler.Get.hasId(StatusID.SchematicUnsupportedBlocks)).toBe(false);
});
});
});
});
});

View File

@ -1,7 +1,8 @@
import { StatusHandler } from '../src/status';
import { ASSERT } from '../src/util/error_util';
import { LOG_MAJOR, Logger, TIME_END, TIME_START } from '../src/util/log_util';
import { WorkerClient } from '../src/worker_client';
import { AssignParams, ExportParams, ImportParams, VoxeliseParams } from '../src/worker_types';
import { AssignParams, ExportParams, ImportParams, TFromWorkerMessage, VoxeliseParams } from '../src/worker_types';
export type THeadlessConfig = {
import: ImportParams.Input,
@ -26,49 +27,70 @@ export function runHeadless(headlessConfig: THeadlessConfig) {
Logger.Get.enableLOGTIME();
}
const worker = WorkerClient.Get;
{
TIME_START('[TIMER] Importer');
LOG_MAJOR('\nImporting...');
worker.import(headlessConfig.import);
StatusHandler.Get.dump().clear();
TIME_END('[TIMER] Importer');
}
{
TIME_START('[TIMER] Voxeliser');
LOG_MAJOR('\nVoxelising...');
worker.voxelise(headlessConfig.voxelise);
StatusHandler.Get.dump().clear();
TIME_END('[TIMER] Voxeliser');
}
{
TIME_START('[TIMER] Assigner');
LOG_MAJOR('\nAssigning...');
worker.assign(headlessConfig.assign);
StatusHandler.Get.dump().clear();
TIME_END('[TIMER] Assigner');
}
{
TIME_START('[TIMER] Exporter');
LOG_MAJOR('\nExporting...');
/**
* The OBJExporter is unique in that it uses the actual render buffer used by WebGL
* to create its data, in headless mode this render buffer is not created so we must
* generate it manually
*/
{
let result;
do {
result = worker.renderChunkedVoxelMesh({
enableAmbientOcclusion: headlessConfig.voxelise.enableAmbientOcclusion,
desiredHeight: headlessConfig.voxelise.size,
});
} while (result.moreVoxelsToBuffer);
}
worker.export(headlessConfig.export);
StatusHandler.Get.dump().clear();
TIME_END('[TIMER] Exporter');
WorkerClient.Get.import(headlessConfig.import, () => {
StatusHandler.Get.dump().clear();
TIME_END('[TIMER] Importer');
onImporterFinished(headlessConfig);
});
}
}
function onImporterFinished(headlessConfig: THeadlessConfig) {
TIME_START('[TIMER] Voxeliser');
LOG_MAJOR('\nVoxelising...');
WorkerClient.Get.voxelise(headlessConfig.voxelise, () => {
StatusHandler.Get.dump().clear();
TIME_END('[TIMER] Voxeliser');
onVoxeliserFinished(headlessConfig);
});
}
function onVoxeliserFinished(headlessConfig: THeadlessConfig) {
TIME_START('[TIMER] Assigner');
LOG_MAJOR('\nAssigning...');
WorkerClient.Get.assign(headlessConfig.assign, () => {
StatusHandler.Get.dump().clear();
TIME_END('[TIMER] Assigner');
onAssignerFinished(headlessConfig);
});
}
function onAssignerFinished(headlessConfig: THeadlessConfig) {
TIME_START('[TIMER] Exporter');
LOG_MAJOR('\nExporting...');
const onReadyToExport = () => {
WorkerClient.Get.export(headlessConfig.export, () => {
StatusHandler.Get.dump().clear();
TIME_END('[TIMER] Exporter');
});
};
/**
* The OBJExporter is unique in that it uses the actual render buffer used by WebGL
* to create its data, in headless mode this render buffer is not created so we must
* generate it manually
*/
if (headlessConfig.export.exporter === 'obj') {
const gatherChunks = () => {
WorkerClient.Get.renderChunkedVoxelMesh({
enableAmbientOcclusion: headlessConfig.voxelise.enableAmbientOcclusion,
desiredHeight: headlessConfig.voxelise.size,
}, (result: TFromWorkerMessage) => {
ASSERT(result.action === 'RenderNextVoxelMeshChunk', `Expected RenderNextVoxelMeshChunk, Got ${result.action}`);
if (result.result.moreVoxelsToBuffer) {
gatherChunks();
} else {
onReadyToExport();
}
});
};
gatherChunks();
} else {
onReadyToExport();
}
}