TubeFilter

Introduction

vtkTubeFilter - A filter that generates tubes around lines

vtkTubeFilter is a filter that generates a tube around each input line. The
tubes are made up of triangle strips and rotate around the tube with the
rotation of the line normals. (If no normals are present, they are computed
automatically.) The radius of the tube can be set to vary with scalar or
vector value. If the radius varies with scalar value the radius is linearly
adjusted. If the radius varies with vector value, a mass flux preserving
variation is used. The number of sides for the tube also can be specified.
You can also specify which of the sides are visible. This is useful for
generating interesting striping effects. Other options include the ability to
cap the tube and generate texture coordinates. Texture coordinates can be
used with an associated texture map to create interesting effects such as
marking the tube with stripes corresponding to length or time.

This filter is typically used to create thick or dramatic lines. Another
common use is to combine this filter with vtkStreamTracer to generate
streamtubes.

!!! warning
The number of tube sides must be greater than 3.

!!! warning
The input line must not have duplicate points, or normals at points that are
parallel to the incoming/outgoing line segments. If a line does not meet this
criteria, then that line is not tubed.

Methods

extend

Method used to decorate a given object (publicAPI+model) with vtkTubeFilter 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 ITubeFilterInitialValues No (default: {})

getCapping

Get whether the capping is enabled or not.

getDefaultNormal

Get the default normal value.

getGenerateTCoords

Get generateTCoords value.

getNumberOfSides

Get the number of sides for the tube.

getOffset

Get offset value.

getOnRatio

Get onRatio value.

getOutputPointsPrecision

Get the desired precision for the output types.

getRadius

Get the minimum tube radius.

getRadiusFactor

Get the maximum tube radius in terms of a multiple of the minimum radius.

getSidesShareVertices

Get sidesShareVertices value.

getTextureLength

Get textureLength value.

getUseDefaultNormal

Get useDefaultNormal value.

getVaryRadius

Get variation of tube radius with scalar or vector values.

newInstance

Method used to create a new instance of vtkTubeFilter

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

requestData

Argument Type Required Description
inData Yes
outData Yes

setCapping

Enable / disable capping the ends of the tube with polygons.

Argument Type Required Description
capping Yes

setDefaultNormal

Set the default normal to use if no normals are supplied. Requires that
useDefaultNormal is set.

Argument Type Required Description
defaultNormal Yes

setGenerateTCoords

Control whether and how texture coordinates are produced. This is useful
for stripping the tube with length textures, etc. If you use scalars to
create the texture, the scalars are assumed to be monotonically
increasing (or decreasing).

Argument Type Required Description
generateTCoords Yes

setNumberOfSides

Set the number of sides for the tube. At a minimum, number of sides is 3.

Argument Type Required Description
numberOfSides Yes

setOffset

Control the stripping of tubes. The offset sets the first tube side that
is visible. Offset is generally used with onRatio to create nifty
stripping effects.

Argument Type Required Description
offset Yes

setOnRatio

Control the stripping of tubes. If OnRatio is greater than 1, then every
nth tube side is turned on, beginning with the offset side.

Argument Type Required Description
onRatio Yes

setOutputPointsPrecision

Set the desired precision for the output types.

Argument Type Required Description
outputPointsPrecision Yes

setRadius

Set the minimum tube radius (minimum because the tube radius may vary).

Argument Type Required Description
radius Number Yes

setRadiusFactor

Set the maximum tube radius in terms of a multiple of the minimum radius.

Argument Type Required Description
radiusFactor Number Yes

setSidesShareVertices

Control whether the tube sides should share vertices. This creates
independent strips, with constant normals so the tube is always faceted
in appearance.

Argument Type Required Description
sidesShareVertices Yes

setTextureLength

Control the conversion of units during texture coordinates calculation.
The texture length indicates what length (whether calculated from scalars
or length) is mapped to [0, 1) texture space.

Argument Type Required Description
textureLength Yes

setUseDefaultNormal

Control whether to use the defaultNormal.

Argument Type Required Description
useDefaultNormal Yes

setVaryRadius

Enable or disable variation of tube radius with scalar or vector values.

Argument Type Required Description
varyRadius Yes

Source

Constants.js
export const VaryRadius = {
VARY_RADIUS_OFF: 0, // default
VARY_RADIUS_BY_SCALAR: 1,
VARY_RADIUS_BY_VECTOR: 2,
VARY_RADIUS_BY_ABSOLUTE_SCALAR: 3,
};

export const GenerateTCoords = {
TCOORDS_OFF: 0, // default
TCOORDS_FROM_NORMALIZED_LENGTH: 1,
TCOORDS_FROM_LENGTH: 2,
TCOORDS_FROM_SCALARS: 3,
};

export default {
VaryRadius,
GenerateTCoords,
};
index.d.ts
import { vtkAlgorithm, vtkObject } from "vtk.js/Sources/interfaces";
import { DesiredOutputPrecision } from 'vtk.js/Sources/Common/DataModel/DataSetAttributes/Constants';

export enum VaryRadius {
VARY_RADIUS_OFF,
VARY_RADIUS_BY_SCALAR,
VARY_RADIUS_BY_VECTOR,
VARY_RADIUS_BY_ABSOLUTE_SCALAR
}

export enum GenerateTCoords {
TCOORDS_OFF,
TCOORDS_FROM_NORMALIZED_LENGTH,
TCOORDS_FROM_LENGTH,
TCOORDS_FROM_SCALARS
}

/**
*
*/
interface ITubeFilterInitialValues {
outputPointsPrecision?: DesiredOutputPrecision,
radius?: number;
varyRadius?: VaryRadius,
numberOfSides?: number;
radiusFactor?: number;
defaultNormal?: number[];
useDefaultNormal?: boolean;
sidesShareVertices?: boolean;
capping?: boolean;
onRatio?: number;
offset?: number;
generateTCoords?: GenerateTCoords,
textureLength?: number;
}

type vtkTubeFilterBase = vtkObject & vtkAlgorithm;

export interface vtkTubeFilter extends vtkTubeFilterBase {

/**
* Get the desired precision for the output types.
*/
getOutputPointsPrecision(): DesiredOutputPrecision;

/**
* Get the minimum tube radius.
*/
getRadius(): number;

/**
* Get variation of tube radius with scalar or vector values.
*/
getVaryRadius(): VaryRadius;

/**
* Get the number of sides for the tube.
*/
getNumberOfSides(): number;

/**
* Get the maximum tube radius in terms of a multiple of the minimum radius.
*/
getRadiusFactor(): number;

/**
* Get the default normal value.
*/
getDefaultNormal(): number[];

/**
* Get useDefaultNormal value.
*/
getUseDefaultNormal(): boolean;

/**
* Get sidesShareVertices value.
*/
getSidesShareVertices(): boolean;

/**
* Get whether the capping is enabled or not.
*/
getCapping(): boolean;

/**
* Get onRatio value.
*/
getOnRatio(): number;

/**
* Get offset value.
*/
getOffset(): number;

/**
* Get generateTCoords value.
*/
getGenerateTCoords(): GenerateTCoords;

/**
* Get textureLength value.
*/
getTextureLength(): number;

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

/**
* Set the desired precision for the output types.
* @param outputPointsPrecision
*/
setOutputPointsPrecision(outputPointsPrecision: DesiredOutputPrecision): boolean;

/**
* Set the minimum tube radius (minimum because the tube radius may vary).
* @param {Number} radius
*/
setRadius(radius: number): boolean;

/**
* Enable or disable variation of tube radius with scalar or vector values.
* @param varyRadius
*/
setVaryRadius(varyRadius: VaryRadius): boolean;

/**
* Set the number of sides for the tube. At a minimum, number of sides is 3.
* @param numberOfSides
*/
setNumberOfSides(numberOfSides: number): boolean;

/**
* Set the maximum tube radius in terms of a multiple of the minimum radius.
* @param {Number} radiusFactor
*/
setRadiusFactor(radiusFactor: number): boolean;

/**
* Set the default normal to use if no normals are supplied. Requires that
* useDefaultNormal is set.
* @param defaultNormal
*/
setDefaultNormal(defaultNormal: number[]): boolean;

/**
* Control whether to use the defaultNormal.
* @param useDefaultNormal
*/
setUseDefaultNormal(useDefaultNormal: boolean): boolean;

/**
* Control whether the tube sides should share vertices. This creates
* independent strips, with constant normals so the tube is always faceted
* in appearance.
* @param sidesShareVertices
*/
setSidesShareVertices(sidesShareVertices: boolean): boolean;

/**
* Enable / disable capping the ends of the tube with polygons.
* @param capping
*/
setCapping(capping: boolean): boolean;

/**
* Control the stripping of tubes. If OnRatio is greater than 1, then every
* nth tube side is turned on, beginning with the offset side.
* @param onRatio
*/
setOnRatio(onRatio: number): boolean;

/**
* Control the stripping of tubes. The offset sets the first tube side that
* is visible. Offset is generally used with onRatio to create nifty
* stripping effects.
* @param offset
*/
setOffset(offset: number): boolean;

/**
* Control whether and how texture coordinates are produced. This is useful
* for stripping the tube with length textures, etc. If you use scalars to
* create the texture, the scalars are assumed to be monotonically
* increasing (or decreasing).
* @param generateTCoords
*/
setGenerateTCoords(generateTCoords: GenerateTCoords): boolean;

/**
* Control the conversion of units during texture coordinates calculation.
* The texture length indicates what length (whether calculated from scalars
* or length) is mapped to [0, 1) texture space.
* @param textureLength
*/
setTextureLength(textureLength: number): boolean;
}

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

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


/**
* vtkTubeFilter - A filter that generates tubes around lines
*
* vtkTubeFilter is a filter that generates a tube around each input line. The
* tubes are made up of triangle strips and rotate around the tube with the
* rotation of the line normals. (If no normals are present, they are computed
* automatically.) The radius of the tube can be set to vary with scalar or
* vector value. If the radius varies with scalar value the radius is linearly
* adjusted. If the radius varies with vector value, a mass flux preserving
* variation is used. The number of sides for the tube also can be specified.
* You can also specify which of the sides are visible. This is useful for
* generating interesting striping effects. Other options include the ability to
* cap the tube and generate texture coordinates. Texture coordinates can be
* used with an associated texture map to create interesting effects such as
* marking the tube with stripes corresponding to length or time.
*
* This filter is typically used to create thick or dramatic lines. Another
* common use is to combine this filter with vtkStreamTracer to generate
* streamtubes.
*
* !!! warning
* The number of tube sides must be greater than 3.
*
* !!! warning
* The input line must not have duplicate points, or normals at points that are
* parallel to the incoming/outgoing line segments. If a line does not meet this
* criteria, then that line is not tubed.
*/
export declare const vtkTubeFilter: {
newInstance: typeof newInstance;
extend: typeof extend;
}
export default vtkTubeFilter;
index.js
import macro from 'vtk.js/Sources/macro';
import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';
import vtkPoints from 'vtk.js/Sources/Common/Core/Points';
import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData';

import { DesiredOutputPrecision } from 'vtk.js/Sources/Common/DataModel/DataSetAttributes/Constants';
import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants';

import Constants from './Constants';

const { VaryRadius, GenerateTCoords } = Constants;
const { vtkDebugMacro, vtkErrorMacro, vtkWarningMacro } = macro;

// ----------------------------------------------------------------------------
// vtkTubeFilter methods
// ----------------------------------------------------------------------------

function vtkTubeFilter(publicAPI, model) {
// Set our classname
model.classHierarchy.push('vtkTubeFilter');

function computeOffset(offset, npts) {
let newOffset = offset;
if (model.sidesShareVertices) {
newOffset += model.numberOfSides * npts;
} else {
// points are duplicated
newOffset += 2 * model.numberOfSides * npts;
}
if (model.capping) {
// cap points are duplicated
newOffset += 2 * model.numberOfSides;
}
return newOffset;
}

function findNextValidSegment(points, pointIds, start) {
const ptId = pointIds[start];
const ps = points.slice(3 * ptId, 3 * (ptId + 1));
let end = start + 1;
while (end < pointIds.length) {
const endPtId = pointIds[end];
const pe = points.slice(3 * endPtId, 3 * (endPtId + 1));
if (ps !== pe) {
return end - 1;
}
++end;
}
return pointIds.length;
}

function generateSlidingNormals(pts, lines, normals, firstNormal = null) {
let normal = [0.0, 0.0, 1.0];
const lineData = lines;
// lid = 0;
let npts = lineData[0];
for (let i = 0; i < lineData.length; i += npts + 1) {
npts = lineData[i];
if (npts === 1) {
// return arbitrary
normals.setTuple(lineData[i + 1], normal);
} else if (npts > 1) {
let sNextId = 0;
let sPrev = [0, 0, 0];
const sNext = [0, 0, 0];

const linePts = lineData.slice(i + 1, i + 1 + npts);
sNextId = findNextValidSegment(pts, linePts, 0);
if (sNextId !== npts) {
// at least one valid segment
let pt1Id = linePts[sNextId];
let pt1 = pts.slice(3 * pt1Id, 3 * (pt1Id + 1));
let pt2Id = linePts[sNextId + 1];
let pt2 = pts.slice(3 * pt2Id, 3 * (pt2Id + 1));
sPrev = pt2.map((elem, idx) => elem - pt1[idx]);
vtkMath.normalize(sPrev);

// compute first normal
if (firstNormal) {
normal = firstNormal;
} else {
// find the next valid, non-parallel segment
while (++sNextId < npts) {
sNextId = findNextValidSegment(pts, linePts, sNextId);
if (sNextId !== npts) {
pt1Id = linePts[sNextId];
pt1 = pts.slice(3 * pt1Id, 3 * (pt1Id + 1));
pt2Id = linePts[sNextId + 1];
pt2 = pts.slice(3 * pt2Id, 3 * (pt2Id + 1));
for (let j = 0; j < 3; ++j) {
sNext[j] = pt2[j] - pt1[j];
}
vtkMath.normalize(sNext);

// now the starting normal should simply be the cross product.
// In the following if statement, we check for the case where
// the two segments are parallel, in which case, continue
// searching for the next valid segment
const n = [0.0, 0.0, 0.0];
vtkMath.cross(sPrev, sNext, n);
if (vtkMath.norm(n) > 1.0e-3) {
normal = n;
sPrev = sNext;
break;
}
}
}

if (sNextId >= npts) {
// only one valid segment
// a little trick to find orthogonal normal
for (let j = 0; j < 3; ++j) {
if (sPrev[j] !== 0.0) {
normal[(j + 2) % 3] = 0.0;
normal[(j + 1) % 3] = 1.0;
normal[j] = -sPrev[(j + 1) % 3] / sPrev[j];
break;
}
}
}
}

vtkMath.normalize(normal);

// compute remaining normals
let lastNormalId = 0;
while (++sNextId < npts) {
sNextId = findNextValidSegment(pts, linePts, sNextId);
if (sNextId === npts) {
break;
}

pt1Id = linePts[sNextId];
pt1 = pts.slice(3 * pt1Id, 3 * (pt1Id + 1));
pt2Id = linePts[sNextId + 1];
pt2 = pts.slice(3 * pt2Id, 3 * (pt2Id + 1));
for (let j = 0; j < 3; ++j) {
sNext[j] = pt2[j] - pt1[j];
}
vtkMath.normalize(sNext);

// compute rotation vector
const w = [0.0, 0.0, 0.0];
vtkMath.cross(sPrev, normal, w);
if (vtkMath.normalize(w) !== 0.0) {
// can't use this segment otherwise
const q = [0.0, 0.0, 0.0];
vtkMath.cross(sNext, sPrev, q);
if (vtkMath.normalize(q) !== 0.0) {
// can't use this segment otherwise
const f1 = vtkMath.dot(q, normal);
let f2 = 1.0 - f1 * f1;
if (f2 > 0.0) {
f2 = Math.sqrt(f2);
} else {
f2 = 0.0;
}
const c = [0, 0, 0];
for (let j = 0; j < 3; ++j) {
c[j] = sNext[j] + sPrev[j];
}
vtkMath.normalize(c);
vtkMath.cross(c, q, w);
vtkMath.cross(sPrev, q, c);
if (vtkMath.dot(normal, c) * vtkMath.dot(w, c) < 0.0) {
f2 *= -1.0;
}

// insert current normal before updating
for (let j = lastNormalId; j < sNextId; ++j) {
normals.setTuple(linePts[j], normal);
}
lastNormalId = sNextId;
sPrev = sNext;

// compute next normal
normal = f1 * q + f2 * w;
}
}
}

// insert last normal for the remaining points
for (let j = lastNormalId; j < npts; ++j) {
normals.setTuple(linePts[j], normal);
}
} else {
// no valid segments
for (let j = 0; j < npts; ++j) {
normals.setTuple(linePts[j], normal);
}
}
}
}
return 1;
}

function generatePoints(
offset,
npts,
pts,
inPts,
newPts,
pd,
outPD,
newNormals,
inScalars,
range,
inVectors,
maxSpeed,
inNormals,
theta
) {
// Use averaged segment to create beveled effect.
const sNext = [0.0, 0.0, 0.0];
const sPrev = [0.0, 0.0, 0.0];
const startCapNorm = [0.0, 0.0, 0.0];
const endCapNorm = [0.0, 0.0, 0.0];
let p = [0.0, 0.0, 0.0];
let pNext = [0.0, 0.0, 0.0];
let s = [0.0, 0.0, 0.0];
let n = [0.0, 0.0, 0.0];
const w = [0.0, 0.0, 0.0];
const nP = [0.0, 0.0, 0.0];
const normal = [0.0, 0.0, 0.0];
let sFactor = 1.0;
let ptId = offset;
for (let j = 0; j < npts; ++j) {
// First point
if (j === 0) {
p = inPts.slice(3 * pts[0], 3 * (pts[0] + 1));
pNext = inPts.slice(3 * pts[1], 3 * (pts[1] + 1));
for (let i = 0; i < 3; ++i) {
sNext[i] = pNext[i] - p[i];
sPrev[i] = sNext[i];
startCapNorm[i] = -sPrev[i];
}
vtkMath.normalize(startCapNorm);
} else if (j === npts - 1) {
for (let i = 0; i < 3; ++i) {
sPrev[i] = sNext[i];
p[i] = pNext[i];
endCapNorm[i] = sNext[i];
}
vtkMath.normalize(endCapNorm);
} else {
for (let i = 0; i < 3; ++i) {
p[i] = pNext[i];
}
pNext = inPts.slice(3 * pts[j + 1], 3 * (pts[j + 1] + 1));
for (let i = 0; i < 3; ++i) {
sPrev[i] = sNext[i];
sNext[i] = pNext[i] - p[i];
}
}

if (vtkMath.normalize(sNext) === 0.0) {
vtkWarningMacro('Coincident points!');
return 0;
}

for (let i = 0; i < 3; ++i) {
s[i] = (sPrev[i] + sNext[i]) / 2.0; // average vector
}

n = inNormals.slice(3 * pts[j], 3 * (pts[j] + 1));
// if s is zero then just use sPrev cross n
if (vtkMath.normalize(s) === 0.0) {
vtkMath.cross(sPrev, n, s);
if (vtkMath.normalize(s) === 0.0) {
vtkDebugMacro('Using alternate bevel vector');
}
}

vtkMath.cross(s, n, w);
if (vtkMath.normalize(w) === 0.0) {
let msg = 'Bad normal: s = ';
msg += `${s[0]}, ${s[1]}, ${s[2]}`;
msg += ` n = ${n[0]}, ${n[1]}, ${n[2]}`;
vtkWarningMacro(msg);
return 0;
}

vtkMath.cross(w, s, nP); // create orthogonal coordinate system
vtkMath.normalize(nP);

// Compute a scalar factor based on scalars or vectors
if (inScalars && model.varyRadius === VaryRadius.VARY_RADIUS_BY_SCALAR) {
sFactor =
1.0 +
((model.radiusFactor - 1.0) *
(inScalars.getComponent(pts[j], 0) - range[0])) /
(range[1] - range[0]);
} else if (
inVectors &&
model.varyRadius === VaryRadius.VARY_RADIUS_BY_VECTOR
) {
sFactor = Math.sqrt(
maxSpeed / vtkMath.norm(inVectors.getTuple(pts[j]))
);
if (sFactor > model.radiusFactor) {
sFactor = model.radiusFactor;
}
} else if (
inScalars &&
model.varyRadius === VaryRadius.VARY_RADIUS_BY_ABSOLUTE_SCALAR
) {
sFactor = inScalars.getComponent(pts[j], 0);
if (sFactor < 0.0) {
vtkWarningMacro('Scalar value less than zero, skipping line');
return 0;
}
}

// create points around line
if (model.sidesShareVertices) {
for (let k = 0; k < model.numberOfSides; ++k) {
for (let i = 0; i < 3; ++i) {
normal[i] =
w[i] * Math.cos(k * theta) + nP[i] * Math.sin(k * theta);
s[i] = p[i] + model.radius * sFactor * normal[i];
newPts[3 * ptId + i] = s[i];
newNormals[3 * ptId + i] = normal[i];
}
outPD.passData(pd, pts[j], ptId);
ptId++;
} // for each side
} else {
const nRight = [0, 0, 0];
const nLeft = [0, 0, 0];
for (let k = 0; k < model.numberOfSides; ++k) {
for (let i = 0; i < 3; ++i) {
// Create duplicate vertices at each point
// and adjust the associated normals so that they are
// oriented with the facets. This preserves the tube's
// polygonal appearance, as if by flat-shading around the tube,
// while still allowing smooth (gouraud) shading along the
// tube as it bends.
normal[i] =
w[i] * Math.cos(k * theta) + nP[i] * Math.sin(k * theta);
nRight[i] =
w[i] * Math.cos((k - 0.5) * theta) +
nP[i] * Math.sin((k - 0.5) * theta);
nLeft[i] =
w[i] * Math.cos((k + 0.5) * theta) +
nP[i] * Math.sin((k + 0.5) * theta);
s[i] = p[i] + model.radius * sFactor * normal[i];
newPts[3 * ptId + i] = s[i];
newNormals[3 * ptId + i] = nRight[i];
newPts[3 * (ptId + 1) + i] = s[i];
newNormals[3 * (ptId + 1) + i] = nLeft[i];
}
outPD.passData(pd, pts[j], ptId + 1);
ptId += 2;
} // for each side
} // else separate vertices
} // for all points in the polyline

// Produce end points for cap. They are placed at tail end of points.
if (model.capping) {
let numCapSides = model.numberOfSides;
let capIncr = 1;
if (!model.sidesShareVertices) {
numCapSides = 2 * model.numberOfSides;
capIncr = 2;
}

// the start cap
for (let k = 0; k < numCapSides; k += capIncr) {
s = newPts.slice(3 * (offset + k), 3 * (offset + k + 1));
for (let i = 0; i < 3; ++i) {
newPts[3 * ptId + i] = s[i];
newNormals[3 * ptId + i] = startCapNorm[i];
}
outPD.passData(pd, pts[0], ptId);
ptId++;
}

// the end cap
let endOffset = offset + (npts - 1) * model.numberOfSides;
if (!model.sidesShareVertices) {
endOffset = offset + 2 * (npts - 1) * model.numberOfSides;
}
for (let k = 0; k < numCapSides; k += capIncr) {
s = newPts.slice(3 * (endOffset + k), 3 * (endOffset + k + 1));
for (let i = 0; i < 3; ++i) {
newPts[3 * ptId + i] = s[i];
newNormals[3 * ptId + i] = endCapNorm[i];
}
outPD.passData(pd, pts[npts - 1], ptId);
ptId++;
}
} // if capping

return 1;
}

function generateStrips(
offset,
npts,
inCellId,
outCellId,
inCD,
outCD,
newStrips
) {
let i1 = 0;
let i2 = 0;
let i3 = 0;
let newOutCellId = outCellId;
let outCellIdx = 0;
const newStripsData = newStrips.getData();
let cellId = 0;
while (outCellIdx < newStripsData.length) {
if (cellId === outCellId) {
break;
}
outCellIdx += newStripsData[outCellIdx] + 1;
cellId++;
}
if (model.sidesShareVertices) {
for (
let k = offset;
k < model.numberOfSides + offset;
k += model.onRatio
) {
i1 = k % model.numberOfSides;
i2 = (k + 1) % model.numberOfSides;
newStripsData[outCellIdx++] = npts * 2;
for (let i = 0; i < npts; ++i) {
i3 = i * model.numberOfSides;
newStripsData[outCellIdx++] = offset + i2 + i3;
newStripsData[outCellIdx++] = offset + i1 + i3;
}
outCD.passData(inCD, inCellId, newOutCellId++);
} // for each side of the tube
} else {
for (
let k = offset;
k < model.numberOfSides + offset;
k += model.onRatio
) {
i1 = 2 * (k % model.numberOfSides) + 1;
i2 = 2 * ((k + 1) % model.numberOfSides);
// outCellId = newStrips.getNumberOfCells(true);
newStripsData[outCellIdx] = npts * 2;
outCellIdx++;
for (let i = 0; i < npts; ++i) {
i3 = i * 2 * model.numberOfSides;
newStripsData[outCellIdx++] = offset + i2 + i3;
newStripsData[outCellIdx++] = offset + i1 + i3;
}
outCD.passData(inCD, inCellId, newOutCellId++);
} // for each side of the tube
}

// Take care of capping. The caps are n-sided polygons that can be easily
// triangle stripped.
if (model.capping) {
let startIdx = offset + npts * model.numberOfSides;
let idx = 0;

if (!model.sidesShareVertices) {
startIdx = offset + 2 * npts * model.numberOfSides;
}

// The start cap
newStripsData[outCellIdx++] = model.numberOfSides;
newStripsData[outCellIdx++] = startIdx;
newStripsData[outCellIdx++] = startIdx + 1;
let k = 0;
for (
i1 = model.numberOfSides - 1, i2 = 2, k = 0;
k < model.numberOfSides - 2;
++k
) {
if (k % 2) {
idx = startIdx + i2;
newStripsData[outCellIdx++] = idx;
i2++;
} else {
idx = startIdx + i1;
newStripsData[outCellIdx++] = idx;
i1--;
}
}
outCD.passData(inCD, inCellId, newOutCellId++);

// The end cap - reversed order to be consistent with normal
startIdx += model.numberOfSides;
newStripsData[outCellIdx++] = model.numberOfSides;
newStripsData[outCellIdx++] = startIdx;
newStripsData[outCellIdx++] = startIdx + model.numberOfSides - 1;
for (
i1 = model.numberOfSides - 2, i2 = 1, k = 0;
k < model.numberOfSides - 2;
++k
) {
if (k % 2) {
idx = startIdx + i1;
newStripsData[outCellIdx++] = idx;
i1--;
} else {
idx = startIdx + i2;
newStripsData[outCellIdx++] = idx;
i2++;
}
}
outCD.passData(inCD, inCellId, newOutCellId++);
}
return newOutCellId;
}

function generateTCoords(offset, npts, pts, inPts, inScalars, newTCoords) {
let numSides = model.numberOfSides;
if (!model.sidesShareVertices) {
numSides = 2 * model.numberOfSides;
}

let tc = 0.0;
let s0 = 0.0;
let s = 0.0;
const inScalarsData = inScalars.getData();
if (model.generateTCoords === GenerateTCoords.TCOORDS_FROM_SCALARS) {
s0 = inScalarsData[pts[0]];
for (let i = 0; i < npts; ++i) {
s = inScalarsData[pts[i]];
tc = (s - s0) / model.textureLength;
for (let k = 0; k < numSides; ++k) {
const tcy = k / (numSides - 1);
const tcId = 2 * (offset + i * numSides + k);
newTCoords[tcId] = tc;
newTCoords[tcId + 1] = tcy;
}
}
} else if (model.generateTCoords === GenerateTCoords.TCOORDS_FROM_LENGTH) {
let len = 0.0;
const xPrev = inPts.slice(3 * pts[0], 3 * (pts[0] + 1));
for (let i = 0; i < npts; ++i) {
const x = inPts.slice(3 * pts[i], 3 * (pts[i] + 1));
len += Math.sqrt(vtkMath.distance2BetweenPoints(x, xPrev));
tc = len / model.textureLength;
for (let k = 0; k < numSides; ++k) {
const tcy = k / (numSides - 1);
const tcId = 2 * (offset + i * numSides + k);
newTCoords[tcId] = tc;
newTCoords[tcId + 1] = tcy;
}
for (let k = 0; k < 3; ++k) {
xPrev[k] = x[k];
}
}
} else if (
model.generateTCoords === GenerateTCoords.TCOORDS_FROM_NORMALIZED_LENGTH
) {
let len = 0.0;
let len1 = 0.0;
let xPrev = inPts.slice(3 * pts[0], 3 * (pts[0] + 1));
for (let i = 0; i < npts; ++i) {
const x = inPts.slice(3 * pts[i], 3 * (pts[i] + 1));
len1 += Math.sqrt(vtkMath.distance2BetweenPoints(x, xPrev));
for (let k = 0; k < 3; ++k) {
xPrev[k] = x[k];
}
}
xPrev = inPts.slice(3 * pts[0], 3 * (pts[0] + 1));
for (let i = 0; i < npts; ++i) {
const x = inPts.slice(3 * pts[i], 3 * (pts[i] + 1));
len += Math.sqrt(vtkMath.distance2BetweenPoints(x, xPrev));
tc = len / len1;
for (let k = 0; k < numSides; ++k) {
const tcy = k / (numSides - 1);
const tcId = 2 * (offset + i * numSides + k);
newTCoords[tcId] = tc;
newTCoords[tcId + 1] = tcy;
}
for (let k = 0; k < 3; ++k) {
xPrev[k] = x[k];
}
}
}

// Capping, set the endpoints as appropriate
if (model.capping) {
const startIdx = offset + npts * numSides;

// start cap
for (let ik = 0; ik < model.numberOfSides; ++ik) {
const tcId = 2 * (startIdx + ik);
newTCoords[tcId] = 0.0;
newTCoords[tcId + 1] = 0.0;
}

// end cap
for (let ik = 0; ik < model.numberOfSides; ++ik) {
const tcId = 2 * (startIdx + model.numberOfSides + ik);
newTCoords[tcId] = 0.0;
newTCoords[tcId + 1] = 0.0;
}
}
}

publicAPI.requestData = (inData, outData) => {
// implement requestData
// pass through for now
const output = vtkPolyData.newInstance();
outData[0] = output;

const input = inData[0];
if (!input) {
vtkErrorMacro('Invalid or missing input');
return;
}

// Allocate output
const inPts = input.getPoints();
if (!inPts) {
return;
}
const numPts = inPts.getNumberOfPoints();
if (numPts < 1) {
return;
}
const inLines = input.getLines();
if (!inLines) {
return;
}
const numLines = inLines.getNumberOfCells();
if (numLines < 1) {
return;
}

let numNewPts = 0;
let numStrips = 0;
const inLinesData = inLines.getData();
let npts = inLinesData[0];
const sidesShareVerticesMultiplier = model.sidesShareVertices ? 1 : 2;
for (let i = 0; i < inLinesData.length; i += npts + 1) {
numNewPts += sidesShareVerticesMultiplier * npts * model.numberOfSides;
if (model.capping) {
numNewPts += 2 * model.numberOfSides;
}

npts = inLinesData[i];
numStrips +=
(2 * npts + 1) * Math.ceil(model.numberOfSides / model.onRatio);
if (model.capping) {
numStrips += 2 * (model.numberOfSides + 1);
}
}

let pointType = inPts.getDataType();
if (model.outputPointsPrecision === DesiredOutputPrecision.SINGLE) {
pointType = VtkDataTypes.FLOAT;
} else if (model.outputPointsPrecision === DesiredOutputPrecision.DOUBLE) {
pointType = VtkDataTypes.DOUBLE;
}
const newPts = vtkPoints.newInstance({
dataType: pointType,
size: numNewPts * 3,
numberOfComponents: 3,
});
let numNormals = 3 * numNewPts;
if (model.capping) {
numNormals = 3 * (numNewPts + 2 * model.numberOfSides);
}
const newNormalsData = new Float32Array(numNormals);
const newNormals = vtkDataArray.newInstance({
numberOfComponents: 3,
values: newNormalsData,
name: 'TubeNormals',
});
const newStripsData = new Uint32Array(numStrips);
const newStrips = vtkCellArray.newInstance({ values: newStripsData });
let newStripId = 0;

let inNormals = input.getPointData().getNormals();
let inNormalsData = null;
let generateNormals = false;
if (!inNormals || model.useDefaultNormal) {
inNormalsData = new Float32Array(3 * numPts);
inNormals = vtkDataArray.newInstance({
numberOfComponents: 3,
values: inNormalsData,
name: 'Normals',
});
if (model.useDefaultNormal) {
inNormalsData = inNormalsData.map((elem, index) => {
const i = index % 3;
return model.defaultNormal[i];
});
} else {
generateNormals = true;
}
}

// loop over pointData arrays and resize based on numNewPts
const numArrays = input.getPointData().getNumberOfArrays();
let oldArray = null;
let newArray = null;
for (let i = 0; i < numArrays; i++) {
oldArray = input.getPointData().getArrayByIndex(i);
newArray = vtkDataArray.newInstance({
name: oldArray.getName(),
dataType: oldArray.getDataType(),
numberOfComponents: oldArray.getNumberOfComponents(),
size: numNewPts * oldArray.getNumberOfComponents(),
});
output.getPointData().removeArrayByIndex(0); // remove oldArray from beginning
output.getPointData().addArray(newArray); // concat newArray to end
}

// loop over cellData arrays and resize based on numNewCells
let numNewCells = inLines.getNumberOfCells() * model.numberOfSides;
if (model.capping) {
numNewCells += 2;
}
const numCellArrays = input.getCellData().getNumberOfArrays();
for (let i = 0; i < numCellArrays; i++) {
oldArray = input.getCellData().getArrayByIndex(i);
newArray = vtkDataArray.newInstance({
name: oldArray.getName(),
dataType: oldArray.getDataType(),
numberOfComponents: oldArray.getNumberOfComponents(),
size: numNewCells * oldArray.getNumberOfComponents(),
});
output.getCellData().removeArrayByIndex(0); // remove oldArray from beginning
output.getCellData().addArray(newArray); // concat newArray to end
}

const inScalars = publicAPI.getInputArrayToProcess(0);
let outScalars = null;
let range = [];
if (inScalars) {
// allocate output scalar array
// assuming point scalars for now
outScalars = vtkDataArray.newInstance({
name: inScalars.getName(),
dataType: inScalars.getDataType(),
numberOfComponents: inScalars.getNumberOfComponents(),
size: numNewPts * inScalars.getNumberOfComponents(),
});
range = inScalars.getRange();
if (range[1] - range[0] === 0.0) {
if (model.varyRadius === VaryRadius.VARY_RADIUS_BY_SCALAR) {
vtkWarningMacro('Scalar range is zero!');
}
range[1] = range[0] + 1.0;
}
}

const inVectors = publicAPI.getInputArrayToProcess(1);
let maxSpeed = 0;
if (inVectors) {
maxSpeed = inVectors.getMaxNorm();
}

const outCD = output.getCellData();
outCD.copyNormalsOff();
outCD.passData(input.getCellData());

const outPD = output.getPointData();
if (outPD.getNormals() !== null) {
outPD.copyNormalsOff();
}
if (inScalars && outScalars) {
outPD.setScalars(outScalars);
}

// TCoords
let newTCoords = null;
if (
(model.generateTCoords === GenerateTCoords.TCOORDS_FROM_SCALARS &&
inScalars) ||
model.generateTCoords === GenerateTCoords.TCOORDS_FROM_LENGTH ||
model.generateTCoords === GenerateTCoords.TCOORDS_FROM_NORMALIZED_LENGTH
) {
const newTCoordsData = new Float32Array(2 * numNewPts);
newTCoords = vtkDataArray.newInstance({
numberOfComponents: 2,
values: newTCoordsData,
name: 'TCoords',
});
outPD.copyTCoordsOff();
}

outPD.passData(input.getPointData());

// Create points along each polyline that are connected into numberOfSides
// triangle strips.
const theta = (2.0 * Math.PI) / model.numberOfSides;
npts = inLinesData[0];
let offset = 0;
let inCellId = input.getVerts().getNumberOfCells();
for (let i = 0; i < inLinesData.length; i += npts + 1) {
npts = inLinesData[i];
const pts = inLinesData.slice(i + 1, i + 1 + npts);
if (npts > 1) {
// if not, skip tubing this line
if (generateNormals) {
const polyLine = inLinesData.slice(i, i + npts + 1);
generateSlidingNormals(inPts.getData(), polyLine, inNormals);
}
}
// generate points
if (
generatePoints(
offset,
npts,
pts,
inPts.getData(),
newPts.getData(),
input.getPointData(),
outPD,
newNormalsData,
inScalars,
range,
inVectors,
maxSpeed,
inNormalsData,
theta
)
) {
// generate strips for the polyline
newStripId = generateStrips(
offset,
npts,
inCellId,
newStripId,
input.getCellData(),
outCD,
newStrips
);
// generate texture coordinates for the polyline
if (newTCoords) {
generateTCoords(
offset,
npts,
pts,
inPts.getData(),
inScalars,
newTCoords.getData()
);
}
} else {
// skip tubing this line
vtkWarningMacro('Could not generate points');
}
// lineIdx += npts;
// Compute the new offset for the next polyline
offset = computeOffset(offset, npts);
inCellId++;
}

output.setPoints(newPts);
output.setStrips(newStrips);
output.setPointData(outPD);
outPD.setNormals(newNormals);
outData[0] = output;
};
}

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

const DEFAULT_VALUES = {
outputPointsPrecision: DesiredOutputPrecision.DEFAULT,
radius: 0.5,
varyRadius: VaryRadius.VARY_RADIUS_OFF,
numberOfSides: 3,
radiusFactor: 10,
defaultNormal: [0, 0, 1],
useDefaultNormal: false,
sidesShareVertices: true,
capping: false,
onRatio: 1,
offset: 0,
generateTCoords: GenerateTCoords.TCOORDS_OFF,
textureLength: 1.0,
};

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

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

// Build VTK API
macro.setGet(publicAPI, model, [
'outputPointsPrecision',
'radius',
'varyRadius',
'numberOfSides',
'radiusFactor',
'defaultNormal',
'useDefaultNormal',
'sidesShareVertices',
'capping',
'onRatio',
'offset',
'generateTCoords',
'textureLength',
]);

// Make this a VTK object
macro.obj(publicAPI, model);

// Also make it an algorithm with one input and one output
macro.algo(publicAPI, model, 1, 1);

// Object specific methods
vtkTubeFilter(publicAPI, model);
}

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

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

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

export default { newInstance, extend };