Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 65 additions & 40 deletions Sources/IO/Geometry/PLYWriter/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { vec3 } from 'gl-matrix';
import macro from 'vtk.js/Sources/macros';

import {
Expand Down Expand Up @@ -30,11 +31,12 @@ const writeHeader = (
format = fileType ? 'binary_little_endian' : 'binary_big_endian';
} else format = 'ascii';

headerComments.unshift('VTK.js generated PLY File');
// Make a new list for each write, so the headerComments option stays the same.
const comments = ['VTK.js generated PLY File', ...(headerComments || [])];
if (textureFileName) {
headerComments.push(`TextureFile ${textureFileName}`);
comments.push(`TextureFile ${textureFileName}`);
}
const commentElements = headerComments
const commentElements = comments
.map((comment) => `comment ${comment}`)
.join('\n');

Expand Down Expand Up @@ -106,11 +108,15 @@ const binaryWriter = () => {
) => {
const vertexCount = polyData.getPoints().getNumberOfPoints();
ft = fileType;
// 1 byte shape descriptor
// 3 vertex indices at ${indexByteCount} bytes
const faceListLength = withIndices
? numPolys * (indexByteCount * 3 + 1)
: 0;
// Each face has 1 byte for the point count and indexByteCount bytes
// for each point index. The cell array has the count and the indices
// of each face.
let faceListLength = 0;
if (withIndices) {
const cellArrayLength = polyData.getPolys().getData().length;
faceListLength =
numPolys + (cellArrayLength - numPolys) * indexByteCount;
}

// 3 position values at 4 bytes
// 3 normal values at 4 bytes
Expand Down Expand Up @@ -179,15 +185,13 @@ const binaryWriter = () => {
vOffset += 1;
}
},
writeFace: (n, x, y, z) => {
writeFace: (cellArray, offset, n) => {
output.setUint8(fOffset, n);
fOffset += 1;
output.setUint32(fOffset, x, ft);
fOffset += indexByteCount;
output.setUint32(fOffset, y, ft);
fOffset += indexByteCount;
output.setUint32(fOffset, z, ft);
fOffset += indexByteCount;
for (let j = 1; j <= n; j++) {
output.setUint32(fOffset, cellArray[offset + j], ft);
fOffset += indexByteCount;
}
},
writeFooter: (polyData) => {},
getOutputData: () => output,
Expand Down Expand Up @@ -240,8 +244,9 @@ const asciiWriter = () => {
}
fileContent += '\n';
},
writeFace: (n, x, y, z) => {
fileContent += `${n} ${x} ${y} ${z}\n`;
writeFace: (cellArray, offset, n) => {
const ids = cellArray.subarray(offset + 1, offset + 1 + n);
fileContent += `${n} ${ids.join(' ')}\n`;
},
writeFooter: (polyData) => {},
getOutputData: () => fileContent,
Expand Down Expand Up @@ -290,15 +295,36 @@ function writePLY(
const numPts = inPts.getNumberOfPoints();
const numPolys = polys.getNumberOfCells();

// textureCoords / uvs
const textureCoords = polyData.getPointData().getTCoords();
// eslint-disable-next-line no-param-reassign
withUVs = !(textureCoords === null);
// The writer writes an attribute only when the option is true and the data
// has the attribute.
let textureCoords = null;
if (withUVs) {
textureCoords = polyData.getPointData().getTCoords();
}
const writeUVs = !!textureCoords;

// scalars / colors
const scalars = polyData.getPointData().getScalars();
// eslint-disable-next-line no-param-reassign
withColors = !(scalars === null);
// PLY colors are 8 bit RGB values. Other scalars are not colors.
let scalars = null;
let scalarsComps = 0;
if (withColors) {
const pointScalars = polyData.getPointData().getScalars();
if (pointScalars) {
const dataType = pointScalars.getDataType();
scalarsComps = pointScalars.getNumberOfComponents();
const is8Bit =
dataType === 'Uint8Array' || dataType === 'Uint8ClampedArray';
if (is8Bit && scalarsComps >= 3) {
scalars = pointScalars;
}
}
}
const writeColors = !!scalars;

let normals = null;
if (withNormals) {
normals = polyData.getPointData().getNormals();
}
const writeNormals = !!normals;

const fileType = dataByteOrder ? 0 : 1;

Expand All @@ -311,22 +337,17 @@ function writePLY(
tCoordsName,
numPts,
numPolys,
withNormals,
withUVs,
withColors,
writeNormals,
writeUVs,
writeColors,
withIndices
);

const normals = polyData.getPointData().getNormals();

// points / vertices
for (let i = 0; i < numPts; i++) {
// eslint-disable-next-line prefer-const
let p = inPts.getPoint(i);

// TODO: apply transform matrix
const p = inPts.getPoint(i);
if (transform) {
// vec3.transformMat4(p, p, transform);
vec3.transformMat4(p, p, transform);
}

// coords
Expand Down Expand Up @@ -355,9 +376,9 @@ function writePLY(
}

if (scalars) {
r = scalars.getData()[i * 3];
g = scalars.getData()[i * 3 + 1];
b = scalars.getData()[i * 3 + 2];
r = scalars.getData()[i * scalarsComps];
g = scalars.getData()[i * scalarsComps + 1];
b = scalars.getData()[i * scalarsComps + 2];
}

if (normals) {
Expand All @@ -370,9 +391,13 @@ function writePLY(
}

// polys / indices
// Each cell in the cell array is the point count, then the point ids.
const pd = polys.getData();
for (let i = 0, l = pd.length; i < l; i += 4) {
writer.writeFace(pd[i + 0], pd[i + 1], pd[i + 2], pd[i + 3]);
let offset = 0;
while (offset < pd.length) {
const n = pd[offset];
writer.writeFace(pd, offset, n);
offset += n + 1;
}

writer.writeFooter(polyData);
Expand Down
180 changes: 180 additions & 0 deletions Sources/IO/Geometry/PLYWriter/test/testPLYWriter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { it, expect } from 'vitest';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData';
import vtkPLYWriter from 'vtk.js/Sources/IO/Geometry/PLYWriter';

function makeTriangle() {
const polyData = vtkPolyData.newInstance();
polyData
.getPoints()
.setData(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), 3);
polyData.getPolys().setData(new Uint32Array([3, 0, 1, 2]));
return polyData;
}

function getHeader(text) {
return text.split('end_header')[0].split('\n');
}

function getBody(text) {
return text.split('end_header\n')[1].trim().split('\n');
}

it('vtkPLYWriter does not change the header comments option', () => {
const polyData = makeTriangle();
const writer = vtkPLYWriter.newInstance({ headerComments: ['my comment'] });
const output = [];
writer.requestData([polyData], output);
writer.requestData([polyData], output);

expect(writer.getHeaderComments()).toEqual(['my comment']);
const comments = getHeader(output[0]).filter((l) => l.startsWith('comment'));
expect(comments).toEqual([
'comment VTK.js generated PLY File',
'comment my comment',
]);
});

it('vtkPLYWriter does not write UVs and colors when the options are false', () => {
const polyData = makeTriangle();
polyData.getPointData().setTCoords(
vtkDataArray.newInstance({
numberOfComponents: 2,
values: new Float32Array([0, 0, 1, 0, 0, 1]),
})
);
polyData.getPointData().setScalars(
vtkDataArray.newInstance({
numberOfComponents: 3,
values: new Uint8Array([255, 0, 0, 0, 255, 0, 0, 0, 255]),
})
);

const writer = vtkPLYWriter.newInstance({
withNormals: false,
withUVs: false,
withColors: false,
});
writer.setInputData(polyData);
const text = writer.getOutputData();
const header = getHeader(text);
['u', 'v', 'red', 'green', 'blue'].forEach((name) => {
expect(header).not.toContain(`property float ${name}`);
expect(header).not.toContain(`property uchar ${name}`);
});
expect(getBody(text)[0]).toBe('0 0 0');
});

it('vtkPLYWriter declares normals only when the polydata has normals', () => {
const polyData = makeTriangle();
const writer = vtkPLYWriter.newInstance();
writer.setInputData(polyData);
let text = writer.getOutputData();
expect(getHeader(text)).not.toContain('property float nx');
expect(getBody(text)[0]).toBe('0 0 0');

polyData.getPointData().setNormals(
vtkDataArray.newInstance({
numberOfComponents: 3,
values: new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1]),
})
);
const writerWithNormals = vtkPLYWriter.newInstance();
writerWithNormals.setInputData(polyData);
text = writerWithNormals.getOutputData();
expect(getHeader(text)).toContain('property float nx');
expect(getBody(text)[0]).toBe('0 0 0 0 0 1');

const binaryWriter = vtkPLYWriter.newInstance({ format: 'binary' });
binaryWriter.setInputData(makeTriangle());
const view = binaryWriter.getOutputData();
const header = new TextDecoder().decode(view.buffer).split('end_header\n')[0];
const headerLength = header.length + 'end_header\n'.length;
// 3 points with 3 floats, and 1 face with 1 count byte and 3 indices.
expect(view.byteLength - headerLength).toBe(3 * 3 * 4 + 1 + 3 * 4);
});

it('vtkPLYWriter reads colors with the number of components of the scalars', () => {
const polyData = makeTriangle();
polyData.getPointData().setScalars(
vtkDataArray.newInstance({
numberOfComponents: 4,
values: new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]),
})
);
const writer = vtkPLYWriter.newInstance({ withNormals: false });
writer.setInputData(polyData);
expect(getBody(writer.getOutputData()).slice(0, 3)).toEqual([
'0 0 0 255 0 0',
'1 0 0 0 255 0',
'0 1 0 0 0 255',
]);
});

it('vtkPLYWriter does not write float scalars as colors', () => {
const polyData = makeTriangle();
polyData.getPointData().setScalars(
vtkDataArray.newInstance({
numberOfComponents: 3,
values: new Float32Array([0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5]),
})
);
const writer = vtkPLYWriter.newInstance({ withNormals: false });
writer.setInputData(polyData);
const text = writer.getOutputData();
expect(getHeader(text)).not.toContain('property uchar red');
expect(getBody(text)[0]).toBe('0 0 0');
});

it('vtkPLYWriter applies the transform to the points', () => {
// Translation of 10 along x (column major matrix).
const transform = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 0, 0, 1];
const writer = vtkPLYWriter.newInstance({ transform });
writer.setInputData(makeTriangle());
expect(getBody(writer.getOutputData())).toEqual([
'10 0 0',
'11 0 0',
'10 1 0',
'3 0 1 2',
]);
});

function makeQuadAndTriangle() {
const polyData = vtkPolyData.newInstance();
polyData
.getPoints()
.setData(
new Float32Array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 2, 0, 0]),
3
);
// One quad, then one triangle.
polyData.getPolys().setData(new Uint32Array([4, 0, 1, 2, 3, 3, 1, 4, 2]));
return polyData;
}

it('vtkPLYWriter writes each face with its own point count', () => {
const writer = vtkPLYWriter.newInstance();
writer.setInputData(makeQuadAndTriangle());
const body = getBody(writer.getOutputData());
expect(body.slice(5)).toEqual(['4 0 1 2 3', '3 1 4 2']);
});

it('vtkPLYWriter writes binary faces with their own point count', () => {
const writer = vtkPLYWriter.newInstance({ format: 'binary' });
writer.setInputData(makeQuadAndTriangle());
const view = writer.getOutputData();
// The faces are at the end of the file: 1 + 4 * 4 bytes, then 1 + 3 * 4 bytes.
const faceStart = view.byteLength - (1 + 4 * 4) - (1 + 3 * 4);
const littleEndian = true;
expect(view.getUint8(faceStart)).toBe(4);
const quad = [0, 1, 2, 3].map((j) =>
view.getUint32(faceStart + 1 + j * 4, littleEndian)
);
expect(quad).toEqual([0, 1, 2, 3]);
const triStart = faceStart + 1 + 4 * 4;
expect(view.getUint8(triStart)).toBe(3);
const tri = [0, 1, 2].map((j) =>
view.getUint32(triStart + 1 + j * 4, littleEndian)
);
expect(tri).toEqual([1, 4, 2]);
});
Loading