forked from mirror/ObjToSchematic
Compare commits
9 Commits
main
...
0.8-vox-im
Author | SHA1 | Date | |
---|---|---|---|
|
23bffa2dc4 | ||
|
ee5dd8b702 | ||
|
1b5d23ba75 | ||
|
fd31ca161f | ||
|
74b1d1222b | ||
|
6169d51cbb | ||
|
de67687d86 | ||
|
134914530b | ||
|
39693c2a42 |
17
package-lock.json
generated
17
package-lock.json
generated
@ -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",
|
||||
|
@ -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
12
res/static/bug.svg
Normal 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
4
res/static/github.svg
Normal 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 |
@ -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 };
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
38
src/importers/vox_importer.ts
Normal file
38
src/importers/vox_importer.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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,
|
||||
}],
|
||||
});
|
||||
|
140
src/ui/elements/header_element.ts
Normal file
140
src/ui/elements/header_element.ts
Normal 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';
|
||||
});
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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: {
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -27,9 +27,12 @@ export namespace ImportParams {
|
||||
}
|
||||
|
||||
export type Output = {
|
||||
type: 'Mesh',
|
||||
triangleCount: number,
|
||||
dimensions: Vector3,
|
||||
materials: MaterialMap
|
||||
} | {
|
||||
type: 'VoxelMesh',
|
||||
}
|
||||
}
|
||||
|
||||
|
46
styles.css
46
styles.css
@ -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)
|
||||
}
|
@ -40,7 +40,7 @@ const baseConfig: THeadlessConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
test('FULL Obj->Obj', () => {
|
||||
test('FULL Obj->Litematic', () => {
|
||||
TEST_PREAMBLE();
|
||||
|
||||
const config: THeadlessConfig = baseConfig;
|
||||
|
@ -40,7 +40,7 @@ const baseConfig: THeadlessConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
test('FULL Obj->Obj', () => {
|
||||
test('FULL Obj->Schem', () => {
|
||||
TEST_PREAMBLE();
|
||||
|
||||
const config: THeadlessConfig = baseConfig;
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user