172 lines
5.6 KiB
JavaScript
172 lines
5.6 KiB
JavaScript
/*
|
|
* .vox file exporter for 3D canvas
|
|
* .vox is the Magica Voxel file format that is also compatible with
|
|
* other vodel editors like Goxel.
|
|
* A object in a .vox file can have a max dimension of 128x128 and 256 height
|
|
* As of the latest release with 0.99.5 4/5/2020 this limit is now 256x256x256,
|
|
* however, lets keep with the old restreints for support of other software.
|
|
* In .vox, 0,0 is the corner of the model and coordinates are unsigned int,
|
|
* the z dimension is the height, contrarian to pixelplanet with y.
|
|
*
|
|
* Reference:
|
|
* https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox.txt
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
/*
|
|
* THIS IS JUST THE START, I WONT CONTINUE IT ANYTIME SOON SO ITS ALREADY
|
|
* COMMITED AS A REFERENCE AND IDEA
|
|
* The idea is to export a 128x128 area around the focal point of where the user
|
|
* is currently looking at as .vox. The file should just have one model.
|
|
*/
|
|
|
|
import { THREE_TILE_SIZE, THREE_CANVAS_HEIGHT } from './constants';
|
|
|
|
const VOX_OBJECT_SIZE = 128;
|
|
const VOX_OBJECT_CSIZE = VOX_OBJECT_SIZE / THREE_TILE_SIZE;
|
|
|
|
|
|
function calcChunkSize(buffer) {
|
|
let cnt = 0;
|
|
let u = buffer.length;
|
|
while (u >= 0) {
|
|
u -= 1;
|
|
if (buffer[u] !== 0) {
|
|
cnt += 1;
|
|
}
|
|
}
|
|
return cnt;
|
|
}
|
|
|
|
async function exportVox(
|
|
chunks,
|
|
canvas,
|
|
centerPoint,
|
|
) {
|
|
// TODO deactivated cause its not finished
|
|
// eslint-disable-next-line no-unused-vars
|
|
const { size: canvasSize, colors } = canvas;
|
|
// round to chunk with its -x and -y corner closest to centerPoint
|
|
const [xc,, yc] = centerPoint.map((z) => {
|
|
const zabs = z + (canvasSize / 2);
|
|
return Math.round(zabs / THREE_TILE_SIZE);
|
|
});
|
|
|
|
const [xcMin, ycMin] = [xc, yc].map((zc) => {
|
|
const zcMin = zc - VOX_OBJECT_CSIZE / 2;
|
|
return (zcMin >= 0) ? zcMin : zc;
|
|
});
|
|
|
|
const posMax = canvasSize / THREE_TILE_SIZE - 1;
|
|
const [xcMax, ycMax] = [xc, yc].map((zc) => {
|
|
if (zc > posMax) {
|
|
return zc - 1;
|
|
}
|
|
const zcMax = zc + VOX_OBJECT_CSIZE / 2 + 1;
|
|
return (zcMax <= posMax) ? zcMax : zc;
|
|
});
|
|
|
|
// Size Chunk
|
|
// 4 bytes SIZE
|
|
// 4 bytes size
|
|
// 4 bytes size children chunks (0)
|
|
// 4 * 3 (x, y, z) size
|
|
const sizeChunkSize = 4 * 3;
|
|
const sizeChunkLength = 4 + 4 + 4 + sizeChunkSize;
|
|
// Voxel Chunk
|
|
// 4 bytes XYZI
|
|
// 4 bytes size
|
|
// 4 bytes size children chunks (0)
|
|
// 4 bytes numVoxels
|
|
// 4 bytes (x, y, z, clr) for every voxel
|
|
let numVoxels = 0;
|
|
for (let j = ycMin; j <= ycMax; j += 1) {
|
|
for (let i = xcMin; i <= xcMax; i += 1) {
|
|
const key = `${i}:${j}`;
|
|
const { buffer } = chunks.get(key);
|
|
numVoxels += calcChunkSize(buffer);
|
|
}
|
|
}
|
|
const xyziChunkSize = 4 + 4 * numVoxels;
|
|
const xyziChunkLength = 4 + 4 + 4 + xyziChunkSize;
|
|
// 4 bytes 'RGBA'
|
|
// 4 bytes size
|
|
// 4 bytes children chunks (0)
|
|
// 4 * 256 palette content (rgba * 256 colors)
|
|
const rgbaChunkSize = 4 * 256;
|
|
const rgbaChunkLength = 4 + 4 + 4 + rgbaChunkSize;
|
|
// TODO deactivated cause its not finished
|
|
// eslint-disable-next-line no-unused-vars
|
|
const rgbaPadding = [256, 256, 256, 256];
|
|
// Main Chunk
|
|
// 4 bytes MAIN
|
|
// 4 bytes size (0)
|
|
// 4 bythes size children chnks
|
|
const mainChildrenChunkSize = sizeChunkLength
|
|
+ xyziChunkLength + rgbaChunkLength;
|
|
const mainChunkLength = 4 + 4 + 4 + mainChildrenChunkSize;
|
|
// 4 bytes 'VOX '
|
|
// 4 bythes version number
|
|
const fileLength = 4 + 4 + mainChunkLength;
|
|
|
|
const voxFile = new ArrayBuffer(fileLength);
|
|
const voxFileView = new DataView(voxFile);
|
|
// TODO how to ascii and how to make less ugly
|
|
const charEncoder = new TextEncoder('utf-8');
|
|
let offset = 0;
|
|
voxFileView.setUint8(charEncoder('V')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('O')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('X')[0], offset++);
|
|
voxFileView.setUint8(charEncoder(' ')[0], offset++);
|
|
voxFileView.setUint32(150, offset);
|
|
offset += 4;
|
|
voxFileView.setUint8(charEncoder('M')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('A')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('I')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('N')[0], offset++);
|
|
voxFileView.setUint32(0, offset);
|
|
offset += 4;
|
|
voxFileView.setUint32(mainChildrenChunkSize, offset);
|
|
offset += 4;
|
|
voxFileView.setUint8(charEncoder('S')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('I')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('Z')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('E')[0], offset++);
|
|
voxFileView.setUint32(sizeChunkSize, offset);
|
|
offset += 4;
|
|
voxFileView.setUint32(0, offset);
|
|
offset += 4;
|
|
voxFileView.setUint32(VOX_OBJECT_SIZE, offset);
|
|
offset += 4;
|
|
voxFileView.setUint32(VOX_OBJECT_SIZE, offset);
|
|
offset += 4;
|
|
voxFileView.setUint32(THREE_CANVAS_HEIGHT, offset);
|
|
offset += 4;
|
|
voxFileView.setUint8(charEncoder('X')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('Y')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('Z')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('I')[0], offset++);
|
|
voxFileView.setUint32(xyziChunkSize, offset);
|
|
offset += 4;
|
|
voxFileView.setUint32(0, offset);
|
|
offset += 4;
|
|
// TODO load voxels here
|
|
offset += xyziChunkSize;
|
|
voxFileView.setUint8(charEncoder('R')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('G')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('B')[0], offset++);
|
|
voxFileView.setUint8(charEncoder('A')[0], offset++);
|
|
voxFileView.setUint32(rgbaChunkSize, offset);
|
|
offset += 4;
|
|
voxFileView.setUint32(0, offset);
|
|
offset += 4;
|
|
// TODO load palette here, unused indices set to rgbaPadding
|
|
|
|
return voxFile;
|
|
// can then be saved from the UI with react-file-downloader
|
|
// aka js-file-doanloader
|
|
}
|
|
|
|
export default exportVox;
|