diff --git a/Documentation/content/docs/gallery/EllipseArcSource.jpg b/Documentation/content/docs/gallery/EllipseArcSource.jpg new file mode 100644 index 00000000000..a4834a8254b Binary files /dev/null and b/Documentation/content/docs/gallery/EllipseArcSource.jpg differ diff --git a/Documentation/content/examples/index.md b/Documentation/content/examples/index.md index 4e81c621d59..3ed32e48575 100644 --- a/Documentation/content/examples/index.md +++ b/Documentation/content/examples/index.md @@ -141,6 +141,7 @@ This will allow you to see the some live code running in your browser. Just pick [![CubeSource Example][CubeSource]](./CubeSource.html "CubeSource") [![Cursor3D Example][Cursor3D]](./Cursor3D.html "Cursor3D") [![CylinderSource Example][CylinderSource]](./CylinderSource.html "CylinderSource") +[![EllipseArcSource Example][EllipseArcSource]](./EllipseArcSource.html "EllipseArcSource") [![LineSource Example][LineSource]](./LineSource.html "LineSource") [![PlaneSource Example][PlaneSource]](./PlaneSource.html "PlaneSource") [![PointSource Example][PointSource]](./PointSource.html "PointSource") @@ -159,6 +160,7 @@ This will allow you to see the some live code running in your browser. Just pick [CubeSource]: ../docs/gallery/CubeSource.jpg [Cursor3D]: ../docs/gallery/Cursor3D.gif [CylinderSource]: ../docs/gallery/CylinderSource.jpg +[EllipseArcSource]: ../docs/gallery/EllipseArcSource.jpg [LineSource]: ../docs/gallery/LineSource.jpg [PlaneSource]: ../docs/gallery/PlaneSource.jpg [PointSource]: ../docs/gallery/PointSource.jpg diff --git a/Sources/Filters/Sources/EllipseArcSource/example/controlPanel.html b/Sources/Filters/Sources/EllipseArcSource/example/controlPanel.html new file mode 100644 index 00000000000..9aeb49a9ad7 --- /dev/null +++ b/Sources/Filters/Sources/EllipseArcSource/example/controlPanel.html @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + +
StartAngle + +
SegmentAngle + +
Resolution + +
Ratio + +
Close + +
diff --git a/Sources/Filters/Sources/EllipseArcSource/example/index.js b/Sources/Filters/Sources/EllipseArcSource/example/index.js new file mode 100644 index 00000000000..5d96bf85668 --- /dev/null +++ b/Sources/Filters/Sources/EllipseArcSource/example/index.js @@ -0,0 +1,60 @@ +import '@kitware/vtk.js/favicon'; + +// Load the rendering pieces we want to use (for both WebGL and WebGPU) +import '@kitware/vtk.js/Rendering/Profiles/Geometry'; + +import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkEllipseArcSource from '@kitware/vtk.js/Filters/Sources/EllipseArcSource'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; + +import controlPanel from './controlPanel.html'; + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- + +const arcSource = vtkEllipseArcSource.newInstance(); +const actor = vtkActor.newInstance(); +const mapper = vtkMapper.newInstance(); + +mapper.setInputConnection(arcSource.getOutputPort()); +actor.setMapper(mapper); + +renderer.addActor(actor); +renderer.resetCamera(); +renderWindow.render(); + +// ----------------------------------------------------------- +// UI control handling +// ----------------------------------------------------------- + +fullScreenRenderer.addController(controlPanel); + +['startAngle', 'segmentAngle', 'resolution', 'ratio'].forEach( + (propertyName) => { + document + .querySelector(`.${propertyName}`) + .addEventListener('input', (e) => { + const value = Number(e.target.value); + arcSource.set({ [propertyName]: value }); + renderer.resetCamera(); + renderWindow.render(); + }); + } +); + +document.querySelector('.close').addEventListener('change', (e) => { + const value = e.target.checked; + arcSource.set({ close: value }); + renderer.resetCamera(); + renderWindow.render(); +}); diff --git a/Sources/Filters/Sources/EllipseArcSource/index.d.ts b/Sources/Filters/Sources/EllipseArcSource/index.d.ts new file mode 100644 index 00000000000..ade53e1a22a --- /dev/null +++ b/Sources/Filters/Sources/EllipseArcSource/index.d.ts @@ -0,0 +1,212 @@ +import { DesiredOutputPrecision } from '../../../Common/DataModel/DataSetAttributes'; +import { vtkAlgorithm, vtkObject } from '../../../interfaces'; +import { Vector3 } from '../../../types'; + +/** + * + */ +export interface IEllipseArcSourceInitialValues { + center?: Vector3; + normal?: Vector3; + majorRadiusVector?: Vector3; + startAngle?: number; + segmentAngle?: number; + resolution?: number; + close?: boolean; + outputPointsPrecision?: DesiredOutputPrecision; + ratio?: number; +} + +type vtkEllipseArcSourceBase = vtkObject & + Omit< + vtkAlgorithm, + | 'getInputData' + | 'setInputData' + | 'setInputConnection' + | 'getInputConnection' + | 'addInputConnection' + | 'addInputData' + >; + +export interface vtkEllipseArcSource extends vtkEllipseArcSourceBase { + /** + * Get whether the arc is closed. + */ + getClose(): boolean; + + /** + * Get the center of the arc. + */ + getCenter(): Vector3; + + /** + * Get the center of the arc by reference. + */ + getCenterByReference(): Vector3; + + /** + * Get the major radius vector of the arc. + */ + getMajorRadiusVector(): Vector3; + + /** + * Get the major radius vector of the arc by reference. + */ + getMajorRadiusVectorByReference(): Vector3; + + /** + * Get the normal vector of the arc. + */ + getNormal(): Vector3; + + /** + * Get the normal vector of the arc by reference. + */ + getNormalByReference(): Vector3; + + /** + * Get the output points precision. + */ + getOutputPointsPrecision(): DesiredOutputPrecision; + + /** + * Get the ratio of the arc. + */ + getRatio(): number; + + /** + * Get the resolution of the arc. + */ + getResolution(): number; + + /** + * Get the segment angle of the arc. + */ + getSegmentAngle(): number; + + /** + * Get the start angle of the arc. + */ + getStartAngle(): number; + + /** + * + * @param inData + * @param outData + */ + requestData(inData: any, outData: any): void; + + /** + * Set whether the arc is closed. + * @param {Boolean} close Whether the arc is closed. + */ + setClose(close: boolean): boolean; + + /** + * Set the center of the arc. + * @param {Vector3} center The center's coordinates. + */ + setCenter(center: Vector3): boolean; + + /** + * Set the center of the arc by reference. + * @param {Vector3} center The center's coordinates. + */ + setCenterFrom(center: Vector3): boolean; + + /** + * Set the major radius vector of the arc. + * @param {Vector3} majorRadiusVector The major radius vector's coordinates. + */ + setMajorRadiusVector(majorRadiusVector: Vector3): boolean; + + /** + * Set the major radius vector of the arc by reference. + * @param {Vector3} majorRadiusVector The major radius vector's coordinates. + */ + setMajorRadiusVectorFrom(majorRadiusVector: Vector3): boolean; + + /** + * Set the normal vector of the arc. + * @param {Vector3} normal The normal vector's coordinates. + */ + setNormal(normal: Vector3): boolean; + + /** + * Set the normal vector of the arc by reference. + * @param {Vector3} normal The normal vector's coordinates. + */ + setNormalFrom(normal: Vector3): boolean; + + /** + * Set the output points precision. + * @param {DesiredOutputPrecision} precision The desired output precision. + */ + setOutputPointsPrecision(precision: DesiredOutputPrecision): boolean; + + /** + * Set the ratio of the arc. + * @param {Number} ratio The ratio of the arc. + */ + setRatio(ratio: number): boolean; + + /** + * Set the resolution of the arc. + * @param {Number} resolution The number of points in the arc. + */ + setResolution(resolution: number): boolean; + + /** + * Set the segment angle of the arc. + * @param {Number} segmentAngle The segment angle in degrees. + */ + setSegmentAngle(segmentAngle: number): boolean; + + /** + * Set the start angle of the arc. + * @param {Number} startAngle The start angle in degrees. + */ + setStartAngle(startAngle: number): boolean; +} + +/** + * Method used to decorate a given object (publicAPI+model) with + * vtkEllipseArcSource characteristics. + * + * @param publicAPI object on which methods will be bounds (public) + * @param model object on which data structure will be bounds (protected) + * @param {IEllipseArcSourceInitialValues} [initialValues] (default: {}) + */ +export function extend( + publicAPI: object, + model: object, + initialValues?: IEllipseArcSourceInitialValues +): void; + +/** + * Method used to create a new instance of vtkEllipseArcSource. + * @param {IEllipseArcSourceInitialValues} [initialValues] for pre-setting some of its content + */ +export function newInstance( + initialValues?: IEllipseArcSourceInitialValues +): vtkEllipseArcSource; + +/** + * vtkEllipseArcSource is a source object that creates an elliptical arc defined + * by a normal, a center and the major radius vector. You can define an angle to + * draw only a section of the ellipse. The number of segments composing the + * polyline is controlled by setting the object resolution. + * + * @example + * ```js + * import vtkEllipseArcSource from '@kitware/vtk.js/Filters/Sources/EllipseArcSource'; + * + * const arc = vtkEllipseArcSource.newInstance(); + * const polydata = arc.getOutputData(); + * ``` + */ +export declare const vtkEllipseArcSource: { + newInstance: typeof newInstance; + extend: typeof extend; +}; +export default vtkEllipseArcSource; diff --git a/Sources/Filters/Sources/EllipseArcSource/index.js b/Sources/Filters/Sources/EllipseArcSource/index.js new file mode 100644 index 00000000000..686b8afd09a --- /dev/null +++ b/Sources/Filters/Sources/EllipseArcSource/index.js @@ -0,0 +1,209 @@ +import macro from 'vtk.js/Sources/macros'; +import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray'; +import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; +import 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'; + +const { vtkErrorMacro } = macro; +const { VtkDataTypes } = vtkDataArray; + +// ---------------------------------------------------------------------------- +// vtkEllipseArcSource methods +// ---------------------------------------------------------------------------- + +function vtkEllipseArcSource(publicAPI, model) { + // Set our className + model.classHierarchy.push('vtkEllipseArcSource'); + + publicAPI.requestData = (inData, outData) => { + const isClosedShape = Math.abs(model.segmentAngle - 360.0) < 1e-5; + const resolution = + model.close && !isClosedShape ? model.resolution + 1 : model.resolution; + + const numLines = resolution; + const numPts = resolution + 1; + const tc = [0.0, 0.0]; + + const output = outData[0] || vtkPolyData.newInstance(); + + // Make sure the normal vector is normalized + const normal = [...model.normal]; + vtkMath.normalize(normal); + + // Get orthogonal vector between user-defined major radius and normal + const orthogonalVect = [0, 0, 0]; + vtkMath.cross(normal, model.majorRadiusVector, orthogonalVect); + + if (vtkMath.norm(orthogonalVect) < 1e-10) { + vtkErrorMacro( + 'Ellipse normal vector and major radius axis are collinear' + ); + return; + } + + vtkMath.normalize(orthogonalVect); + + const majorRadiusVect = [0, 0, 0]; + vtkMath.cross(orthogonalVect, normal, majorRadiusVect); + vtkMath.normalize(majorRadiusVect); + + const a = vtkMath.norm(model.majorRadiusVector); + const b = a * model.ratio; + + let startAngleRad = vtkMath.radiansFromDegrees(model.startAngle); + if (startAngleRad < 0) { + startAngleRad += 2.0 * Math.PI; + } + + const segmentAngleRad = vtkMath.radiansFromDegrees(model.segmentAngle); + + const angleIncRad = segmentAngleRad / model.resolution; + + let pointType = VtkDataTypes.FLOAT; + if (model.outputPointsPrecision === DesiredOutputPrecision.SINGLE) { + pointType = VtkDataTypes.FLOAT; + } else if (model.outputPointsPrecision === DesiredOutputPrecision.DOUBLE) { + pointType = VtkDataTypes.DOUBLE; + } + + const points = vtkPoints.newInstance({ + dataType: pointType, + }); + points.setNumberOfPoints(numPts); + + const tcoords = vtkDataArray.newInstance({ + numberOfComponents: 2, + size: numPts * 2, + dataType: VtkDataTypes.FLOAT, + name: 'TextureCoordinates', + }); + + const lines = vtkCellArray.newInstance(); + lines.allocate(numLines); + + const skipLastPoint = model.close && isClosedShape; + + let theta = startAngleRad; + let pointIndex = 0; + + for (let i = 0; i <= resolution; ++i, theta += angleIncRad) { + const quotient = Math.floor(theta / (2.0 * Math.PI)); + const normalizedTheta = theta - quotient * 2.0 * Math.PI; + + let thetaEllipse = Math.atan(Math.tan(normalizedTheta) * model.ratio); + + if (normalizedTheta > Math.PI / 2 && normalizedTheta <= Math.PI) { + thetaEllipse += Math.PI; + } else if ( + normalizedTheta > Math.PI && + normalizedTheta <= 1.5 * Math.PI + ) { + thetaEllipse -= Math.PI; + } + + const cosTheta = Math.cos(thetaEllipse); + const sinTheta = Math.sin(thetaEllipse); + + const p = [ + model.center[0] + + a * cosTheta * majorRadiusVect[0] + + b * sinTheta * orthogonalVect[0], + model.center[1] + + a * cosTheta * majorRadiusVect[1] + + b * sinTheta * orthogonalVect[1], + model.center[2] + + a * cosTheta * majorRadiusVect[2] + + b * sinTheta * orthogonalVect[2], + ]; + + tc[0] = i / resolution; + tc[1] = 0.0; + + if (i !== resolution || !skipLastPoint) { + points.setPoint(pointIndex, ...p); + tcoords.setTuple(pointIndex, tc); + pointIndex++; + } + } + + const actualNumPts = pointIndex; + const pointIds = []; + + for (let k = 0; k < actualNumPts - 1; ++k) { + pointIds.push(k); + } + + if (model.close) { + pointIds.push(0); + } else { + pointIds.push(actualNumPts - 1); + } + + lines.insertNextCell(pointIds); + + output.setPoints(points); + output.getPointData().setTCoords(tcoords); + output.setLines(lines); + + outData[0] = output; + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + center: [0.0, 0.0, 0.0], + normal: [0.0, 0.0, 1.0], + majorRadiusVector: [1.0, 0.0, 0.0], + startAngle: 0.0, + segmentAngle: 90.0, + resolution: 100, + close: false, + outputPointsPrecision: DesiredOutputPrecision.SINGLE, + ratio: 1.0, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + // Ensure resolution is at least 1 + if (model.resolution < 1) { + model.resolution = 1; + } + + // Build VTK API + macro.obj(publicAPI, model); + macro.algo(publicAPI, model, 0, 1); + + macro.setGet(publicAPI, model, [ + 'resolution', + 'startAngle', + 'segmentAngle', + 'close', + 'outputPointsPrecision', + 'ratio', + ]); + + macro.setGetArray( + publicAPI, + model, + ['center', 'normal', 'majorRadiusVector'], + 3 + ); + + vtkEllipseArcSource(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkEllipseArcSource'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; diff --git a/Sources/Filters/Sources/EllipseArcSource/test/testEllipseArc.js b/Sources/Filters/Sources/EllipseArcSource/test/testEllipseArc.js new file mode 100644 index 00000000000..29dcbcbca15 --- /dev/null +++ b/Sources/Filters/Sources/EllipseArcSource/test/testEllipseArc.js @@ -0,0 +1,65 @@ +import test from 'tape'; +import testUtils from 'vtk.js/Sources/Testing/testUtils'; + +import 'vtk.js/Sources/Rendering/Misc/RenderingAPIs'; +import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow'; +import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; +import vtkEllipseArcSource from 'vtk.js/Sources/Filters/Sources/EllipseArcSource'; +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; + +import baseline from './testEllipseArc.png'; + +test.onlyIfWebGL('Test testArc Rendering', (t) => { + const gc = testUtils.createGarbageCollector(); + t.ok('rendering', 'testArc Rendering'); + + // Create some control UI + const container = document.querySelector('body'); + const renderWindowContainer = gc.registerDOMElement( + document.createElement('div') + ); + container.appendChild(renderWindowContainer); + + // create what we will view + const renderWindow = gc.registerResource(vtkRenderWindow.newInstance()); + const renderer = gc.registerResource(vtkRenderer.newInstance()); + renderWindow.addRenderer(renderer); + renderer.setBackground(0.32, 0.34, 0.43); + + const actor = gc.registerResource(vtkActor.newInstance()); + renderer.addActor(actor); + + const mapper = gc.registerResource(vtkMapper.newInstance()); + actor.setMapper(mapper); + + const ellipseArcSource = gc.registerResource( + vtkEllipseArcSource.newInstance({ + startAngle: 90, + segmentAngle: 180, + ratio: 0.25, + }) + ); + mapper.setInputConnection(ellipseArcSource.getOutputPort()); + + // now create something to view it, in this case webgl + const glwindow = gc.registerResource(renderWindow.newAPISpecificView()); + glwindow.setContainer(renderWindowContainer); + renderWindow.addView(glwindow); + glwindow.setSize(400, 400); + + const promise = glwindow + .captureNextImage() + .then((image) => + testUtils.compareImages( + image, + [baseline], + 'Filters/Sources/EllipseArcSource/testEllipseArc', + t, + 2.5 + ) + ) + .finally(gc.releaseResources); + renderWindow.render(); + return promise; +}); diff --git a/Sources/Filters/Sources/EllipseArcSource/test/testEllipseArc.png b/Sources/Filters/Sources/EllipseArcSource/test/testEllipseArc.png new file mode 100644 index 00000000000..a9f4d03b9cb Binary files /dev/null and b/Sources/Filters/Sources/EllipseArcSource/test/testEllipseArc.png differ diff --git a/Sources/Filters/Sources/index.js b/Sources/Filters/Sources/index.js index 51624152d23..814d894851d 100644 --- a/Sources/Filters/Sources/index.js +++ b/Sources/Filters/Sources/index.js @@ -6,6 +6,7 @@ import vtkCubeSource from './CubeSource'; import vtkCursor3D from './Cursor3D'; import vtkCylinderSource from './CylinderSource'; import vtkDiskSource from './DiskSource'; +import vtkEllipseArcSource from './EllipseArcSource'; import vtkImageGridSource from './ImageGridSource'; import vtkLineSource from './LineSource'; import vtkPlaneSource from './PlaneSource'; @@ -23,6 +24,7 @@ export default { vtkCursor3D, vtkCylinderSource, vtkDiskSource, + vtkEllipseArcSource, vtkImageGridSource, vtkLineSource, vtkPlaneSource,