OBJReader

Introduction

The vtkOBJReader aims to read a text file formatted as below and create
a bumpy plane based on the elevation defined in that file. If a line has 10
elevation values, that means the plane will have 10 points along the X axis.
If the file has 5 lines, that means the plane will have 5 points along the Y
axis.

1 2 3 4 5
5 4 3 2 1
1 2 3 4 5
5 4 3 2 1
1 2 3 4 5

Each number represents an elevation on a uniform grid where a line
(horizontal) define the elevations along the X axis. With that in mind, new
lines (vertical) define the elevations along the Y axis and the actual number
is the elevation along Z.

In order to properly represent that in world coordinates, you can provide an
origin which will define the coordinate of the first point without its
elevation. Then you need to describe how much you should move along X and Y
between two elevations definition. For that we use xSpacing and ySpacing.
Since the elevation is given to us as a number, we can scale it via
zScaling. Finally you may decide that your grid should move along positive
X and negative Y while reading the file. The xDirection and yDirection
are meant to give you control on that end.

Methods

extend

Method used to decorate a given object (publicAPI+model) with vtkOBJReader characteristics.

Argument Type Required Description
publicAPI Yes object on which methods will be bounds (public)
model Yes object on which data structure will be bounds (protected)
initialValues IOBJReaderInitialValues No (default: {})

getBaseURL

getDataAccessHelper

getPointDuplicateIds

Convenient method to get the id of all duplicated points.
Relies on a “Duplicates” array that contains the id of:
- the first duplicate id for the original point (or self Id if there is no duplicate)

  • the original id for the duplicate points
    Duplicated points must be adjacent to each other.
    Example:
    • Points: [pt0, pt1, pt2, pt3, pt1d0, pt1d1, pt1d3, pt3d1, pt3d2]
  • Duplicates: [0, 4, 2, 3, 1, 1, 1, 3, 3]
    Expected results:
  • getPointDuplicateIds(p, 1) => [1, 4, 5, 6]
  • getPointDuplicateIds(p, 0) => [0]
  • getPointDuplicateIds(p, 5) => [1, 4, 5, 6]
Argument Type Required Description
polydata Yes A polydata generated by the OBJReader
pointId Yes A point Id (duplicated or not)

getSplitMode

getTrackDuplicates

True if duplicates are tracked in output polydata.

getUrl

Get the url of the object to load.

invokeBusy

Argument Type Required Description
busy Boolean Yes

isBusy

loadData

Load the object data.

Argument Type Required Description
options IOBJReaderOptions No

newInstance

Method used to create a new instance of vtkOBJReader

Argument Type Required Description
initialValues IOBJReaderInitialValues No for pre-setting some of its content

onBusy

Argument Type Required Description
callback Yes

parseAsText

Parse data as text.

Argument Type Required Description
content String Yes The content to parse.

requestData

Argument Type Required Description
inData Yes
outData Yes

setDataAccessHelper

Argument Type Required Description
dataAccessHelper Yes

setSplitMode

String in OBJ file used to split in multiple output polydata.

Argument Type Required Description
splitMode Null or String Yes

setTrackDuplicates

Set to true to be able to use getPointDuplicateIds() on output polydata.
Requires splitMode to not be null.

Argument Type Required Description
trackDuplicates Yes true or false (false by default)

setUrl

Set the url of the object to load.

Argument Type Required Description
url String Yes the url of the object to load.
option IOBJReaderOptions No The OBJ reader options.

Source

index.d.ts
import { vtkAlgorithm, vtkObject, vtkSubscription } from '../../../interfaces';
import HtmlDataAccessHelper from '../../Core/DataAccessHelper/HtmlDataAccessHelper';
import HttpDataAccessHelper from '../../Core/DataAccessHelper/HttpDataAccessHelper';
import JSZipDataAccessHelper from '../../Core/DataAccessHelper/JSZipDataAccessHelper';
import LiteHttpDataAccessHelper from '../../Core/DataAccessHelper/LiteHttpDataAccessHelper';

interface IOBJReaderOptions {
binary?: boolean;
compression?: string;
progressCallback?: any;
}

/**
*
*/
export interface IOBJReaderInitialValues {
numberOfOutputs?: number;
requestCount?: number;
splitMode?: string;
}

type vtkOBJReaderBase = vtkObject &
Omit<
vtkAlgorithm,
| 'getInputData'
| 'setInputData'
| 'setInputConnection'
| 'getInputConnection'
| 'addInputConnection'
| 'addInputData'
>;

/**
* Convenient method to get the id of all duplicated points.
* Relies on a "Duplicates" array that contains the id of:
* - the first duplicate id for the original point (or self Id if there is no duplicate)
* - the original id for the duplicate points
* Duplicated points must be adjacent to each other.
* Example:
* - Points: [pt0, pt1, pt2, pt3, pt1d0, pt1d1, pt1d3, pt3d1, pt3d2]
* - Duplicates: [0, 4, 2, 3, 1, 1, 1, 3, 3]
* Expected results:
* - getPointDuplicateIds(p, 1) => [1, 4, 5, 6]
* - getPointDuplicateIds(p, 0) => [0]
* - getPointDuplicateIds(p, 5) => [1, 4, 5, 6]
* @param polydata A polydata generated by the OBJReader
* @param pointId A point Id (duplicated or not)
*/
export function getPointDuplicateIds(
polydata: any,
pointId: number
): Array<number>;

export interface vtkOBJReader extends vtkOBJReaderBase {
/**
*
*/
getBaseURL(): string;

/**
*
*/
getDataAccessHelper():
| HtmlDataAccessHelper
| HttpDataAccessHelper
| JSZipDataAccessHelper
| LiteHttpDataAccessHelper;

/**
* Get the url of the object to load.
*/
getUrl(): string;

/**
*
*/
getSplitMode(): number;

/**
* True if duplicates are tracked in output polydata.
*/
getTrackDuplicates(): boolean;

/**
*
* @param {Boolean} busy
*/
invokeBusy(busy: boolean): void;

/**
*
*/
isBusy(): number;

/**
* Load the object data.
* @param {IOBJReaderOptions} [options]
*/
loadData(options?: IOBJReaderOptions): Promise<any>;

/**
*
* @param callback
*/
onBusy(callback: (busy: boolean) => any): vtkSubscription;

/**
* Parse data as text.
* @param {String} content The content to parse.
*/
parseAsText(content: string): void;

/**
*
* @param inData
* @param outData
*/
requestData(inData: any, outData: any): void;

/**
*
* @param dataAccessHelper
*/
setDataAccessHelper(
dataAccessHelper:
| HtmlDataAccessHelper
| HttpDataAccessHelper
| JSZipDataAccessHelper
| LiteHttpDataAccessHelper
): boolean;

/**
* String in OBJ file used to split in multiple output polydata.
* @param {Null | String} splitMode
*/
setSplitMode(splitMode: string): boolean;

/**
* Set to true to be able to use getPointDuplicateIds() on output polydata.
* Requires splitMode to not be null.
* @param trackDuplicates true or false (false by default)
* @see getPointDuplicateIds(), setSplitMode()
*/
setTrackDuplicates(trackDuplicates: boolean): boolean;

/**
* Set the url of the object to load.
* @param {String} url the url of the object to load.
* @param {IOBJReaderOptions} [option] The OBJ reader options.
*/
setUrl(url: string, option?: IOBJReaderOptions): Promise<string>;
}

/**
* Method used to decorate a given object (publicAPI+model) with vtkOBJReader characteristics.
*
* @param publicAPI object on which methods will be bounds (public)
* @param model object on which data structure will be bounds (protected)
* @param {IOBJReaderInitialValues} [initialValues] (default: {})
*/
export function extend(
publicAPI: object,
model: object,
initialValues?: IOBJReaderInitialValues
): void;

/**
* Method used to create a new instance of vtkOBJReader
* @param {IOBJReaderInitialValues} [initialValues] for pre-setting some of its content
*/
export function newInstance(
initialValues?: IOBJReaderInitialValues
): vtkOBJReader;

/**
* The vtkOBJReader aims to read a text file formatted as below and create
* a bumpy plane based on the elevation defined in that file. If a line has 10
* elevation values, that means the plane will have 10 points along the X axis.
* If the file has 5 lines, that means the plane will have 5 points along the Y
* axis.
*
* ```
* 1 2 3 4 5
* 5 4 3 2 1
* 1 2 3 4 5
* 5 4 3 2 1
* 1 2 3 4 5
* ```
*
* Each number represents an elevation on a uniform grid where a line
* (horizontal) define the elevations along the X axis. With that in mind, new
* lines (vertical) define the elevations along the Y axis and the actual number
* is the elevation along Z.
*
* In order to properly represent that in world coordinates, you can provide an
* `origin` which will define the coordinate of the first point without its
* elevation. Then you need to describe how much you should move along X and Y
* between two elevations definition. For that we use `xSpacing` and `ySpacing`.
* Since the elevation is given to us as a number, we can scale it via
* `zScaling`. Finally you may decide that your grid should move along positive
* X and negative Y while reading the file. The `xDirection` and `yDirection`
* are meant to give you control on that end.
*/
export declare const vtkOBJReader: {
newInstance: typeof newInstance;
extend: typeof extend;
};
export default vtkOBJReader;
index.js
import macro from 'vtk.js/Sources/macros';
import DataAccessHelper from 'vtk.js/Sources/IO/Core/DataAccessHelper';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData';

// Enable several sources for DataAccessHelper
import 'vtk.js/Sources/IO/Core/DataAccessHelper/LiteHttpDataAccessHelper'; // Just need HTTP
// import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; // HTTP + gz
// import 'vtk.js/Sources/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; // html + base64 + zip
// import 'vtk.js/Sources/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; // zip

// ----------------------------------------------------------------------------

const data = {};

// ----------------------------------------------------------------------------

function copyVector(src, srcOffset, dst, dstOffset, vectorSize) {
for (let i = 0; i < vectorSize; i++) {
dst[dstOffset + i] = src[srcOffset + i];
}
}

// ----------------------------------------------------------------------------

function begin(splitMode) {
data.splitOn = splitMode;
data.pieces = [];
data.v = [];
data.vt = [];
data.vn = [];
data.f = [[]];
data.size = 0;
}

// ----------------------------------------------------------------------------

function faceMap(str) {
const idxs = str.split('/').map((i) => Number(i));
let vertexIdx = idxs[0] - 1;
vertexIdx = vertexIdx < 0 ? vertexIdx + 1 + data.v.length / 3 : vertexIdx;
let textCoordIdx = idxs[1] ? idxs[1] - 1 : vertexIdx;
textCoordIdx =
textCoordIdx < 0 ? textCoordIdx + 1 + data.vt.length / 2 : textCoordIdx;
let vertexNormal = idxs[2] ? idxs[2] - 1 : vertexIdx;
vertexNormal =
vertexNormal < 0 ? vertexNormal + 1 + data.vn.length / 3 : vertexNormal;
return [vertexIdx, textCoordIdx, vertexNormal];
}

// ----------------------------------------------------------------------------

function parseLine(line) {
if (line[0] === '#') {
return;
}
const tokens = line.split(/[ \t]+/);
if (tokens[0] === data.splitOn) {
tokens.shift();
data.pieces.push(tokens.join(' ').trim());
data.f.push([]);
data.size++;
} else if (tokens[0] === 'v') {
data.v.push(Number(tokens[1]));
data.v.push(Number(tokens[2]));
data.v.push(Number(tokens[3]));
} else if (tokens[0] === 'vt') {
data.vt.push(Number(tokens[1]));
data.vt.push(Number(tokens[2]));
} else if (tokens[0] === 'vn') {
data.vn.push(Number(tokens[1]));
data.vn.push(Number(tokens[2]));
data.vn.push(Number(tokens[3]));
} else if (tokens[0] === 'f') {
// Handle triangles for now
if (data.size === 0) {
data.size++;
}
const cells = data.f[data.size - 1];
tokens.shift();
const faces = tokens.filter((s) => s.length > 0 && s !== '\r');
const size = faces.length;
cells.push(size);
for (let i = 0; i < size; i++) {
cells.push(faceMap(faces[i]));
}
}
}

// ----------------------------------------------------------------------------

function end(model) {
const hasTcoords = !!data.vt.length;
const hasNormals = !!data.vn.length;
if (model.splitMode) {
model.numberOfOutputs = data.size;
for (let idx = 0; idx < data.size; idx++) {
const polyIn = data.f[idx];
const nbElems = polyIn.length;
const nbPoints = data.v.length / 3;
const keyPointId = {};
let pointDuplicatesReferences;

if (model.trackDuplicates) {
// In trackDuplicates mode, we want the following point layout:
// [pt0, pt1, pt2, ... ptN, pt0d1, pt0d2, pt1d1]
const pointKeys = [];
let duplicatesCount = 0;

for (let offset = 0; offset < nbElems; ) {
const cellSize = polyIn[offset++];
for (let pIdx = 0; pIdx < cellSize; pIdx++) {
const [vIdx, tcIdx, nIdx] = polyIn[offset++];
const key = `${vIdx}/${tcIdx}/${nIdx}`;
if (keyPointId[key] === undefined) {
if (pointKeys[vIdx] === undefined) {
pointKeys[vIdx] = [key];
} else {
pointKeys[vIdx].push(key);
++duplicatesCount;
}
// will be overwritten for duplicates
keyPointId[key] = vIdx;
}
}
}
pointDuplicatesReferences = new Uint16Array(nbPoints + duplicatesCount);
let duplicates = 0;
for (let pointId = 0; pointId < pointKeys.length; ++pointId) {
const usageCount = pointKeys[pointId] ? pointKeys[pointId].length : 0;
// Set the first duplicate index on the original point
pointDuplicatesReferences[pointId] =
usageCount > 1 ? nbPoints + duplicates : pointId;
// Set the original index on each duplicated point
for (let duplicateId = 1; duplicateId < usageCount; ++duplicateId) {
const finalDuplicateId = nbPoints + duplicates++;
pointDuplicatesReferences[finalDuplicateId] = pointId;
// Associate the duplicate index to the key
keyPointId[pointKeys[pointId][duplicateId]] = finalDuplicateId;
}
}
}

const ctMapping = {};
const polydata = vtkPolyData.newInstance({ name: data.pieces[idx] });
const pts = [];
const tc = [];
const normals = [];
const polys = [];

let offset = 0;
while (offset < nbElems) {
const cellSize = polyIn[offset];
polys.push(cellSize);
for (let pIdx = 0; pIdx < cellSize; pIdx++) {
const [vIdx, tcIdx, nIdx] = polyIn[offset + pIdx + 1];
const key = `${vIdx}/${tcIdx}/${nIdx}`;
if (ctMapping[key] === undefined) {
const dstOffset = model.trackDuplicates
? keyPointId[key]
: pts.length / 3;
ctMapping[key] = dstOffset;
copyVector(data.v, vIdx * 3, pts, dstOffset * 3, 3);
if (hasTcoords) {
copyVector(data.vt, tcIdx * 2, tc, dstOffset * 2, 2);
}
if (hasNormals) {
copyVector(data.vn, nIdx * 3, normals, dstOffset * 3, 3);
}
}
polys.push(ctMapping[key]);
}
offset += cellSize + 1;
}

polydata.getPoints().setData(Float32Array.from(pts), 3);
if (model.trackDuplicates) {
const duplicatesArray = vtkDataArray.newInstance({
name: 'Duplicates',
values: pointDuplicatesReferences,
});
polydata.getPointData().addArray(duplicatesArray);
}
polydata.getPolys().setData(Uint32Array.from(polys));

if (hasTcoords) {
const tcoords = vtkDataArray.newInstance({
numberOfComponents: 2,
values: Float32Array.from(tc),
name: 'TextureCoordinates',
});
polydata.getPointData().setTCoords(tcoords);
}

if (hasNormals) {
const normalsArray = vtkDataArray.newInstance({
numberOfComponents: 3,
values: Float32Array.from(normals),
name: 'Normals',
});
polydata.getPointData().setNormals(normalsArray);
}

// register in output
model.output[idx] = polydata;
}
} else {
model.numberOfOutputs = 1;
const polydata = vtkPolyData.newInstance();
polydata.getPoints().setData(Float32Array.from(data.v), 3);
if (hasTcoords && data.v.length / 3 === data.vt.length / 2) {
const tcoords = vtkDataArray.newInstance({
numberOfComponents: 2,
values: Float32Array.from(data.vt),
name: 'TextureCoordinates',
});
polydata.getPointData().setTCoords(tcoords);
}
if (hasNormals && data.v.length === data.vn.length) {
const normalsArray = vtkDataArray.newInstance({
numberOfComponents: 3,
values: Float32Array.from(data.vn),
name: 'Normals',
});
polydata.getPointData().setNormals(normalsArray);
}

const polys = [];
const polyIn = data.f[0];
const nbElems = polyIn.length;
let offset = 0;
while (offset < nbElems) {
const cellSize = polyIn[offset];
polys.push(cellSize);
for (let pIdx = 0; pIdx < cellSize; pIdx++) {
const [vIdx] = polyIn[offset + pIdx + 1];
polys.push(vIdx);
}
offset += cellSize + 1;
}
polydata.getPolys().setData(Uint32Array.from(polys));
model.output[0] = polydata;
}
}

// ----------------------------------------------------------------------------
// Static API
// ----------------------------------------------------------------------------

function getPointDuplicateIds(polyData, pointId) {
const res = [];
const duplicates = polyData.getPointData().getArrayByName('Duplicates');
if (duplicates == null) {
return res;
}
const duplicatesData = duplicates.getData();
const originalPointId = Math.min(pointId, duplicatesData[pointId]);
res.push(originalPointId);
let duplicateId = duplicatesData[originalPointId];
if (duplicateId !== originalPointId) {
// point has duplicates
while (
duplicateId < duplicatesData.length &&
duplicatesData[duplicateId] === originalPointId
) {
// Duplicated points must be next to each other and original point must
// reference first duplicate
res.push(duplicateId++);
}
}
return res;
}

export const STATIC = {
getPointDuplicateIds,
};

// ----------------------------------------------------------------------------
// vtkOBJReader methods
// ----------------------------------------------------------------------------

function vtkOBJReader(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkOBJReader');

// Create default dataAccessHelper if not available
if (!model.dataAccessHelper) {
model.dataAccessHelper = DataAccessHelper.get('http');
}

// Internal method to fetch Array
function fetchData(url, option = {}) {
return model.dataAccessHelper.fetchText(publicAPI, url, option);
}

// Set DataSet url
publicAPI.setUrl = (url, option = {}) => {
if (url.indexOf('.obj') === -1 && !option.fullpath) {
model.baseURL = url;
model.url = `${url}/index.obj`;
} else {
model.url = url;

// Remove the file in the URL
const path = url.split('/');
path.pop();
model.baseURL = path.join('/');
}

// Fetch metadata
return publicAPI.loadData(option);
};

// Fetch the actual data arrays
publicAPI.loadData = (option = {}) =>
fetchData(model.url, option).then((content) =>
publicAPI.isDeleted() ? false : publicAPI.parseAsText(content)
);

publicAPI.parseAsText = (content) => {
if (!content) {
return true;
}
if (content !== model.parseData) {
publicAPI.modified();
}
model.parseData = content;
model.numberOfOutputs = 0;
begin(model.splitMode);
content.split('\n').forEach(parseLine);
end(model);
return true;
};

publicAPI.requestData = (inData, outData) => {
publicAPI.parseAsText(model.parseData);
};

// return Busy state
publicAPI.isBusy = () => !!model.requestCount;

publicAPI.getNumberOfOutputPorts = () => model.numberOfOutputs;
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------

const DEFAULT_VALUES = {
numberOfOutputs: 1,
requestCount: 0,
splitMode: null,
trackDuplicates: false,
// baseURL: null,
// dataAccessHelper: null,
// url: null,
};

// ----------------------------------------------------------------------------

export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);

// Build VTK API
macro.obj(publicAPI, model);
macro.get(publicAPI, model, ['url', 'baseURL']);
macro.setGet(publicAPI, model, [
'dataAccessHelper',
'splitMode',
'trackDuplicates',
]);
macro.algo(publicAPI, model, 0, 1);
macro.event(publicAPI, model, 'busy');

// Object methods
vtkOBJReader(publicAPI, model);
}

// ----------------------------------------------------------------------------

export const newInstance = macro.newInstance(extend, 'vtkOBJReader');

// ----------------------------------------------------------------------------

export default { newInstance, extend, ...STATIC };