UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

602 lines (460 loc) 17.7 kB
import macro from '../../macros.js'; var vtkErrorMacro = macro.vtkErrorMacro; // ---------------------------------------------------------------------------- // vtkPiecewiseFunction methods // ---------------------------------------------------------------------------- function vtkPiecewiseFunction(publicAPI, model) { // Set our className model.classHierarchy.push('vtkPiecewiseFunction'); // Return the number of points which specify this function publicAPI.getSize = function () { return model.nodes.length; }; // Return the type of function stored in object: // Function Types: // 0 : Constant (No change in slope between end points) // 1 : NonDecreasing (Always increasing or zero slope) // 2 : NonIncreasing (Always decreasing or zero slope) // 3 : Varied (Contains both decreasing and increasing slopes) // 4 : Unknown (Error condition) // publicAPI.getType = function () { var value; var prevValue = 0.0; var functionType = 0; if (model.nodes.length > 0) { prevValue = model.nodes[0].y; } for (var i = 1; i < model.nodes.length; i++) { value = model.nodes[i].y; // Do not change the function type if equal if (value !== prevValue) { if (value > prevValue) { switch (functionType) { case 0: case 1: // NonDecreasing functionType = 1; break; case 2: default: // Varied functionType = 3; break; } } else { // value < prev_value switch (functionType) { case 0: case 2: // NonIncreasing functionType = 2; break; case 1: default: // Varied functionType = 3; break; } } } prevValue = value; // Exit loop if we find a Varied function if (functionType === 3) { break; } } switch (functionType) { case 0: return 'Constant'; case 1: return 'NonDecreasing'; case 2: return 'NonIncreasing'; case 3: default: return 'Varied'; } }; // Since we no longer store the data in an array, we must // copy out of the vector into an array. No modified check - // could be added if performance is a problem publicAPI.getDataPointer = function () { var size = model.nodes.length; model.function = null; if (size > 0) { model.function = []; for (var i = 0; i < size; i++) { model.function[2 * i] = model.nodes[i].x; model.function[2 * i + 1] = model.nodes[i].y; } } return model.function; }; // Returns the first point location which starts a non-zero segment of the // function. Note that the value at this point may be zero. publicAPI.getFirstNonZeroValue = function () { // Check if no points specified if (model.nodes.length === 0) { return 0; } var allZero = 1; var x = 0.0; var i = 0; for (; i < model.nodes.length; i++) { if (model.nodes[i].y !== 0.0) { allZero = 0; break; } } // If every specified point has a zero value then return // a large value if (allZero) { x = Number.MAX_VALUE; } else if (i > 0) { // A point was found with a non-zero value // Return the value of the point that precedes this one x = model.nodes[i - 1].x; } else if (model.clamping) { // If this is the first point in the function, return its // value is clamping is off, otherwise VTK_DOUBLE_MIN if // clamping is on. x = -Number.MAX_VALUE; } else { x = model.nodes[0].x; } return x; }; // For a specified index value, get the node parameters publicAPI.getNodeValue = function (index, val) { var size = model.nodes.length; if (index < 0 || index >= size) { vtkErrorMacro('Index out of range!'); return -1; } val[0] = model.nodes[index].x; val[1] = model.nodes[index].y; val[2] = model.nodes[index].midpoint; val[3] = model.nodes[index].sharpness; return 1; }; // For a specified index value, get the node parameters publicAPI.setNodeValue = function (index, val) { var size = model.nodes.length; if (index < 0 || index >= size) { vtkErrorMacro('Index out of range!'); return -1; } var oldX = model.nodes[index].x; model.nodes[index].x = val[0]; model.nodes[index].y = val[1]; model.nodes[index].midpoint = val[2]; model.nodes[index].sharpness = val[3]; if (oldX !== val[0]) { // The point has been moved, the order of points or the range might have // been modified. publicAPI.sortAndUpdateRange(); // No need to call Modified() here because SortAndUpdateRange() has done it // already. } else { publicAPI.modified(); } return 1; }; // Adds a point to the function. If a duplicate point is inserted // then the function value at that location is set to the new value. // This is the legacy version that assumes midpoint = 0.5 and // sharpness = 0.0 publicAPI.addPoint = function (x, y) { return publicAPI.addPointLong(x, y, 0.5, 0.0); }; // Adds a point to the function and returns the array index of the point. publicAPI.addPointLong = function (x, y, midpoint, sharpness) { // Error check if (midpoint < 0.0 || midpoint > 1.0) { vtkErrorMacro('Midpoint outside range [0.0, 1.0]'); return -1; } if (sharpness < 0.0 || sharpness > 1.0) { vtkErrorMacro('Sharpness outside range [0.0, 1.0]'); return -1; } // remove any node already at this X location if (!model.allowDuplicateScalars) { publicAPI.removePoint(x); } // Create the new node var node = { x: x, y: y, midpoint: midpoint, sharpness: sharpness }; // Add it, then sort to get everything in order model.nodes.push(node); publicAPI.sortAndUpdateRange(); // Now find this node so we can return the index var i; for (i = 0; i < model.nodes.length; i++) { if (model.nodes[i].x === x) { break; } } // If we didn't find it, something went horribly wrong so // return -1 if (i < model.nodes.length) { return i; } return -1; }; publicAPI.setNodes = function (nodes) { if (model.nodes !== nodes) { model.nodes = nodes; publicAPI.sortAndUpdateRange(); } }; // Sort the vector in increasing order, then fill in // the Range publicAPI.sortAndUpdateRange = function () { model.nodes.sort(function (a, b) { return a.x - b.x; }); var modifiedInvoked = publicAPI.updateRange(); // If range is updated, Modified() has been called, don't call it again. if (!modifiedInvoked) { publicAPI.modified(); } }; //---------------------------------------------------------------------------- publicAPI.updateRange = function () { var oldRange = model.range.slice(); var size = model.nodes.length; if (size) { model.range[0] = model.nodes[0].x; model.range[1] = model.nodes[size - 1].x; } else { model.range[0] = 0; model.range[1] = 0; } // If the rage is the same, then no need to call Modified() if (oldRange[0] === model.range[0] && oldRange[1] === model.range[1]) { return false; } publicAPI.modified(); return true; }; // Removes a point from the function. If no point is found then function // remains the same. publicAPI.removePoint = function (x) { // First find the node since we need to know its // index as our return value var i; for (i = 0; i < model.nodes.length; i++) { if (model.nodes[i].x === x) { break; } } // If the node doesn't exist, we return -1 if (i >= model.nodes.length) { return -1; } var retVal = i; // If the first or last point has been removed, then we update the range // No need to sort here as the order of points hasn't changed. var modifiedInvoked = false; model.nodes.splice(i, 1); if (i === 0 || i === model.nodes.length) { modifiedInvoked = publicAPI.updateRange(); } if (!modifiedInvoked) { publicAPI.modified(); } return retVal; }; // Removes all points from the function. publicAPI.removeAllPoints = function () { model.nodes = []; publicAPI.sortAndUpdateRange(); }; // Add in end points of line and remove any points between them // Legacy method with no way to specify midpoint and sharpness publicAPI.addSegment = function (x1, y1, x2, y2) { // First, find all points in this range and remove them publicAPI.sortAndUpdateRange(); for (var i = 0; i < model.nodes.length;) { if (model.nodes[i].x >= x1 && model.nodes[i].x <= x2) { model.nodes.splice(i, 1); } else { i++; } } // Now add the points publicAPI.addPoint(x1, y1, 0.5, 0.0); publicAPI.addPoint(x2, y2, 0.5, 0.0); }; // Return the value of the function at a position publicAPI.getValue = function (x) { var table = []; publicAPI.getTable(x, x, 1, table); return table[0]; }; // Remove all points outside the range, and make sure a point // exists at each end of the range. Used as a convenience method // for transfer function editors publicAPI.adjustRange = function (range) { if (range.length < 2) { return 0; } var functionRange = publicAPI.getRange(); // Make sure we have points at each end of the range if (functionRange[0] < range[0]) { publicAPI.addPoint(range[0], publicAPI.getValue(range[0])); } else { publicAPI.addPoint(range[0], publicAPI.getValue(functionRange[0])); } if (functionRange[1] > range[1]) { publicAPI.addPoint(range[1], publicAPI.getValue(range[1])); } else { publicAPI.addPoint(range[1], publicAPI.getValue(functionRange[1])); } // Remove all points out-of-range publicAPI.sortAndUpdateRange(); for (var i = 0; i < model.nodes.length;) { if (model.nodes[i].x >= range[0] && model.nodes[i].x <= range[1]) { model.nodes.splice(i, 1); } else { ++i; } } publicAPI.sortAndUpdateRange(); return 1; }; //-------------------------------------------------------------------------- publicAPI.estimateMinNumberOfSamples = function (x1, x2) { var d = publicAPI.findMinimumXDistance(); return Math.ceil((x2 - x1) / d); }; //---------------------------------------------------------------------------- publicAPI.findMinimumXDistance = function () { var size = model.nodes.length; if (size < 2) { return -1.0; } var distance = model.nodes[1].x - model.nodes[0].x; for (var i = 0; i < size - 1; i++) { var currentDist = model.nodes[i + 1].x - model.nodes[i].x; if (currentDist < distance) { distance = currentDist; } } return distance; }; // Returns a table of function values evaluated at regular intervals /* eslint-disable prefer-destructuring */ /* eslint-disable no-continue */ publicAPI.getTable = function (xStart, xEnd, size, table) { var stride = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; var i; var idx = 0; var numNodes = model.nodes.length; // Need to keep track of the last value so that // we can fill in table locations past this with // this value if Clamping is On. var lastValue = 0.0; if (numNodes !== 0) { lastValue = model.nodes[numNodes - 1].y; } var x = 0.0; var x1 = 0.0; var x2 = 0.0; var y1 = 0.0; var y2 = 0.0; var midpoint = 0.0; var sharpness = 0.0; // For each table entry for (i = 0; i < size; i++) { // Find our location in the table var tidx = stride * i; // Find our X location. If we are taking only 1 sample, make // it halfway between start and end (usually start and end will // be the same in this case) if (size > 1) { x = xStart + i / (size - 1.0) * (xEnd - xStart); } else { x = 0.5 * (xStart + xEnd); } // Do we need to move to the next node? while (idx < numNodes && x > model.nodes[idx].x) { idx++; // If we are at a valid point index, fill in // the value at this node, and the one before (the // two that surround our current sample location) // idx cannot be 0 since we just incremented it. if (idx < numNodes) { x1 = model.nodes[idx - 1].x; x2 = model.nodes[idx].x; y1 = model.nodes[idx - 1].y; y2 = model.nodes[idx].y; // We only need the previous midpoint and sharpness // since these control this region midpoint = model.nodes[idx - 1].midpoint; sharpness = model.nodes[idx - 1].sharpness; // Move midpoint away from extreme ends of range to avoid // degenerate math if (midpoint < 0.00001) { midpoint = 0.00001; } if (midpoint > 0.99999) { midpoint = 0.99999; } } } // Are we at the end? If so, just use the last value if (idx >= numNodes) { table[tidx] = model.clamping ? lastValue : 0.0; } else if (idx === 0) { // Are we before the first node? If so, duplicate this nodes values table[tidx] = model.clamping ? model.nodes[0].y : 0.0; } else { // Otherwise, we are between two nodes - interpolate // Our first attempt at a normalized location [0,1] - // we will be modifying this based on midpoint and // sharpness to get the curve shape we want and to have // it pass through (y1+y2)/2 at the midpoint. var s = (x - x1) / (x2 - x1); // Readjust based on the midpoint - linear adjustment if (s < midpoint) { s = 0.5 * s / midpoint; } else { s = 0.5 + 0.5 * (s - midpoint) / (1.0 - midpoint); } // override for sharpness > 0.99 // In this case we just want piecewise constant if (sharpness > 0.99) { // Use the first value since we are below the midpoint if (s < 0.5) { table[tidx] = y1; continue; } else { // Use the second value at or above the midpoint table[tidx] = y2; continue; } } // Override for sharpness < 0.01 // In this case we want piecewise linear if (sharpness < 0.01) { // Simple linear interpolation table[tidx] = (1 - s) * y1 + s * y2; continue; } // We have a sharpness between [0.01, 0.99] - we will // used a modified hermite curve interpolation where we // derive the slope based on the sharpness, and we compress // the curve non-linearly based on the sharpness // First, we will adjust our position based on sharpness in // order to make the curve sharper (closer to piecewise constant) if (s < 0.5) { s = 0.5 * Math.pow(s * 2, 1.0 + 10 * sharpness); } else if (s > 0.5) { s = 1.0 - 0.5 * Math.pow((1.0 - s) * 2, 1 + 10 * sharpness); } // Compute some coefficients we will need for the hermite curve var ss = s * s; var sss = ss * s; var h1 = 2 * sss - 3 * ss + 1; var h2 = -2 * sss + 3 * ss; var h3 = sss - 2 * ss + s; var h4 = sss - ss; // Use one slope for both end points var slope = y2 - y1; var t = (1.0 - sharpness) * slope; // Compute the value table[tidx] = h1 * y1 + h2 * y2 + h3 * t + h4 * t; // Final error check to make sure we don't go outside // the Y range var min = y1 < y2 ? y1 : y2; var max = y1 > y2 ? y1 : y2; table[tidx] = table[tidx] < min ? min : table[tidx]; table[tidx] = table[tidx] > max ? max : table[tidx]; } } }; } /* eslint-enable prefer-destructuring */ /* eslint-enable no-continue */ // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- var DEFAULT_VALUES = { // model.function = NULL; range: [0, 0], clamping: true, allowDuplicateScalars: false }; // ---------------------------------------------------------------------------- function extend(publicAPI, model) { var initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; Object.assign(model, DEFAULT_VALUES, initialValues); // Inheritance macro.obj(publicAPI, model); // Internal objects initialization model.nodes = []; // Create get-set macros macro.setGet(publicAPI, model, ['allowDuplicateScalars', 'clamping']); macro.setArray(publicAPI, model, ['range'], 2); // Create get macros for array macro.getArray(publicAPI, model, ['range']); // For more macro methods, see "Sources/macros.js" // Object specific methods vtkPiecewiseFunction(publicAPI, model); } // ---------------------------------------------------------------------------- var newInstance = macro.newInstance(extend, 'vtkPiecewiseFunction'); // ---------------------------------------------------------------------------- var vtkPiecewiseFunction$1 = { newInstance: newInstance, extend: extend }; export { vtkPiecewiseFunction$1 as default, extend, newInstance };