Minor refactor to exporters

This commit is contained in:
Lucas Dower 2023-01-24 20:33:27 +00:00
parent 39693c2a42
commit 134914530b
No known key found for this signature in database
GPG Key ID: B3EE6B8499593605
11 changed files with 116 additions and 170 deletions

View File

@ -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) {

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

@ -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

@ -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

@ -216,7 +216,7 @@ export class WorkerClient {
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;
}

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;