diff --git a/Documentation/content/docs/gallery/FrustumSource.jpg b/Documentation/content/docs/gallery/FrustumSource.jpg new file mode 100644 index 00000000000..4f161a482ca Binary files /dev/null and b/Documentation/content/docs/gallery/FrustumSource.jpg differ diff --git a/Documentation/content/examples/index.md b/Documentation/content/examples/index.md index 8b96dbf7291..8c422db4dad 100644 --- a/Documentation/content/examples/index.md +++ b/Documentation/content/examples/index.md @@ -144,6 +144,7 @@ This will allow you to see the some live code running in your browser. Just pick [![Cursor3D Example][Cursor3D]](./Cursor3D.html "Cursor3D") [![CylinderSource Example][CylinderSource]](./CylinderSource.html "CylinderSource") [![EllipseArcSource Example][EllipseArcSource]](./EllipseArcSource.html "EllipseArcSource") +[![FrustumSource Example][FrustumSource]](./FrustumSource.html "FrustumSource") [![LineSource Example][LineSource]](./LineSource.html "LineSource") [![PlaneSource Example][PlaneSource]](./PlaneSource.html "PlaneSource") [![PointSource Example][PointSource]](./PointSource.html "PointSource") @@ -164,6 +165,7 @@ This will allow you to see the some live code running in your browser. Just pick [Cursor3D]: ../docs/gallery/Cursor3D.gif [CylinderSource]: ../docs/gallery/CylinderSource.jpg [EllipseArcSource]: ../docs/gallery/EllipseArcSource.jpg +[FrustumSource]: ../docs/gallery/FrustumSource.jpg [LineSource]: ../docs/gallery/LineSource.jpg [PlaneSource]: ../docs/gallery/PlaneSource.jpg [PointSource]: ../docs/gallery/PointSource.jpg diff --git a/Sources/Filters/Sources/FrustumSource/example/controlPanel.html b/Sources/Filters/Sources/FrustumSource/example/controlPanel.html new file mode 100644 index 00000000000..3c9f6eaf825 --- /dev/null +++ b/Sources/Filters/Sources/FrustumSource/example/controlPanel.html @@ -0,0 +1,15 @@ + + + + + + + + + +
Shrink Factor + +
Show Lines + +
+ diff --git a/Sources/Filters/Sources/FrustumSource/example/index.js b/Sources/Filters/Sources/FrustumSource/example/index.js new file mode 100644 index 00000000000..f1822155ac4 --- /dev/null +++ b/Sources/Filters/Sources/FrustumSource/example/index.js @@ -0,0 +1,85 @@ +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 vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; +import vtkProperty from '@kitware/vtk.js/Rendering/Core/Property'; +import vtkCamera from '@kitware/vtk.js/Rendering/Core/Camera'; +import vtkPlanes from '@kitware/vtk.js/Common/DataModel/Planes'; +import vtkFrustumSource from '@kitware/vtk.js/Filters/Sources/FrustumSource'; +import vtkShrinkPolyData from '@kitware/vtk.js/Filters/General/ShrinkPolyData'; +import controlPanel from './controlPanel.html'; + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- + +const frustum = vtkFrustumSource.newInstance(); +const actor = vtkActor.newInstance(); +const mapper = vtkMapper.newInstance(); + +const camera = vtkCamera.newInstance(); +camera.setClippingRange(0.1, 0.4); + +const planesArray = camera.getFrustumPlanes(1.0); +const planes = vtkPlanes.newInstance(); +planes.setFrustumPlanes(planesArray); + +frustum.setPlanes(planes); +frustum.setShowLines(false); + +const backProperty = vtkProperty.newInstance(); +backProperty.setColor(1, 1, 50 / 255); + +const shrink = vtkShrinkPolyData.newInstance(); +shrink.setShrinkFactor(0.9); +shrink.setInputConnection(frustum.getOutputPort()); +mapper.setInputConnection(shrink.getOutputPort()); +mapper.setScalarVisibility(false); +actor.setMapper(mapper); +actor.getProperty().setColor(255 / 255, 99 / 255, 71 / 255); +actor.getProperty().setEdgeVisibility(true); +actor.setBackfaceProperty(backProperty); +renderer.addActor(actor); + +renderer.getActiveCamera().setPosition(1, 0, 0); +renderer.getActiveCamera().setFocalPoint(0, 0, 0); +renderer.getActiveCamera().setViewUp(0, 1, 0); +renderer.getActiveCamera().azimuth(30); +renderer.getActiveCamera().elevation(30); + +renderer.resetCamera(); +renderWindow.render(); + +// ----------------------------------------------------------- +// UI control handling +// ----------------------------------------------------------- + +fullScreenRenderer.addController(controlPanel); + +['showLines'].forEach((propertyName) => { + document.querySelector(`.${propertyName}`).addEventListener('input', (e) => { + const value = e.target.checked; + frustum.set({ [propertyName]: value }); + renderWindow.render(); + }); +}); + +['shrinkFactor'].forEach((propertyName) => { + document.querySelector(`.${propertyName}`).addEventListener('input', (e) => { + const value = Number(e.target.value); + shrink.set({ [propertyName]: value }); + renderWindow.render(); + }); +}); diff --git a/Sources/Filters/Sources/FrustumSource/index.d.ts b/Sources/Filters/Sources/FrustumSource/index.d.ts new file mode 100644 index 00000000000..06e1e3066f6 --- /dev/null +++ b/Sources/Filters/Sources/FrustumSource/index.d.ts @@ -0,0 +1,111 @@ +import { DesiredOutputPrecision } from '../../../Common/DataModel/DataSetAttributes'; +import { vtkAlgorithm, vtkObject } from '../../../interfaces'; +import vtkPlanes from '../../../Common/DataModel/Planes'; + +/** + * + */ +export interface IFrustumSourceInitialValues { + planes?: vtkPlanes; + showLines?: boolean; + outputPointsPrecision?: DesiredOutputPrecision; +} + +type vtkFrustumSourceBase = vtkObject & + Omit< + vtkAlgorithm, + | 'getInputData' + | 'setInputData' + | 'setInputConnection' + | 'getInputConnection' + | 'addInputConnection' + | 'addInputData' + >; + +export interface vtkFrustumSource extends vtkFrustumSourceBase { + /** + * Get the output points precision. + */ + getOutputPointsPrecision(): DesiredOutputPrecision; + + /** + * Get the planes defining the frustum. + */ + getPlanes(): vtkPlanes; + + /** + * Get whether to show lines. + */ + getShowLines(): boolean; + + /** + * + * @param inData + * @param outData + */ + requestData(inData: any, outData: any): void; + + /** + * Set the output points precision. + * @param {DesiredOutputPrecision} precision + */ + setOutputPointsPrecision(precision: DesiredOutputPrecision): boolean; + + /** + * Set the planes defining the frustum. + * @param {vtkPlanes} planes + */ + setPlanes(planes: vtkPlanes): boolean; + + /** + * Set whether to show lines. + * @param {Boolean} showLines + */ + setShowLines(showLines: boolean): boolean; +} + +/** + * Method used to decorate a given object (publicAPI+model) with vtkFrustumSource characteristics. + * + * @param publicAPI object on which methods will be bounds (public) + * @param model object on which data structure will be bounds (protected) + * @param {IFrustumSourceInitialValues} [initialValues] (default: {}) + */ +export function extend( + publicAPI: object, + model: object, + initialValues?: IFrustumSourceInitialValues +): void; + +/** + * Method used to create a new instance of vtkFrustumSource. + * @param {IFrustumSourceInitialValues} [initialValues] for pre-setting some of its content + */ +export function newInstance( + initialValues?: IFrustumSourceInitialValues +): vtkFrustumSource; + +/** + * vtkFrustumSource creates a frustum defines by a set of planes. The frustum is + * represented with four-sided polygons. It is possible to specify extra lines + * to better visualize the field of view. + * + * @example + * ```js + * import vtkFrustumSource from '@kitware/vtk.js/Filters/Sources/FrustumSource'; + * + * const frustum = vtkFrustumSource.newInstance(); + * const camera = vtkCamera.newInstance(); + * camera.setClippingRange(0.1, 0.4); + * const planesArray = camera.getFrustumPlanes(1.0); + * const planes = vtkPlanes.newInstance(); + * planes.setFrustumPlanes(planesArray); + * frustum.setPlanes(planes); + * frustum.setShowLines(false); + * ``` + */ +export declare const vtkFrustumSource: { + newInstance: typeof newInstance; + extend: typeof extend; +}; +export default vtkFrustumSource; diff --git a/Sources/Filters/Sources/FrustumSource/index.js b/Sources/Filters/Sources/FrustumSource/index.js new file mode 100644 index 00000000000..51d894a78b8 --- /dev/null +++ b/Sources/Filters/Sources/FrustumSource/index.js @@ -0,0 +1,277 @@ +import macro from 'vtk.js/Sources/macros'; +import vtkMath from 'vtk.js/Sources/Common/Core/Math'; +import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray'; +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'; + +const { vtkErrorMacro } = macro; + +// ---------------------------------------------------------------------------- +// vtkFrustumSource methods +// ---------------------------------------------------------------------------- + +function vtkFrustumSource(publicAPI, model) { + // Set our className + model.classHierarchy.push('vtkFrustumSource'); + + function computePoint(planeIndices, pt) { + // Get planes and their normals/origins + const plane0 = model.planes.getPlane(planeIndices[0]); + const n0 = plane0.getNormal(); + const p0 = plane0.getOrigin(); + + const plane1 = model.planes.getPlane(planeIndices[1]); + const n1 = plane1.getNormal(); + const p1 = plane1.getOrigin(); + + const plane2 = model.planes.getPlane(planeIndices[2]); + const n2 = plane2.getNormal(); + const p2 = plane2.getOrigin(); + + // Dot products + const d0 = vtkMath.dot(p0, n0); + const d1 = vtkMath.dot(p1, n1); + const d2 = vtkMath.dot(p2, n2); + + // Cross products + const c12 = [0, 0, 0]; + vtkMath.cross(n1, n2, c12); + const c20 = [0, 0, 0]; + vtkMath.cross(n2, n0, c20); + const c01 = [0, 0, 0]; + vtkMath.cross(n0, n1, c01); + + // Determinant + const d = vtkMath.determinant3x3([...n0, ...n1, ...n2]); + + // Intersection point + for (let i = 0; i < 3; ++i) { + pt[i] = (d0 * c12[i] + d1 * c20[i] + d2 * c01[i]) / d; + } + } + + publicAPI.requestData = (inData, outData) => { + const output = outData[0] || vtkPolyData.newInstance(); + + if (!model.planes || model.planes.getNumberOfPlanes() !== 6) { + vtkErrorMacro('vtkFrustum requires 6 planes.'); + return; + } + + let nbPts = 8; + + let leftRightNull = false; + let bottomTopNull = false; + let parallelFrustum = false; + + // angle between left and right planes + const n0 = model.planes.getPlane(0).getNormal().slice(); + const n1 = model.planes.getPlane(1).getNormal().slice(); + const c = [0, 0, 0]; + + vtkMath.normalize(n0); + vtkMath.normalize(n1); + vtkMath.dot(n0, n1); + + vtkMath.cross(n0, n1, c); + vtkMath.norm(c); + + // angle between bottom and top planes + n0.splice(0, 3, ...model.planes.getPlane(2).getNormal()); + n1.splice(0, 3, ...model.planes.getPlane(3).getNormal()); + + vtkMath.normalize(n0); + vtkMath.normalize(n1); + vtkMath.dot(n0, n1); + + vtkMath.cross(n0, n1, c); + vtkMath.norm(c); + + if (model.showLines) { + const left = model.planes.getPlane(0).getNormal(); + const right = model.planes.getPlane(1).getNormal(); + const bottom = model.planes.getPlane(2).getNormal(); + const top = model.planes.getPlane(3).getNormal(); + + const leftRight = [0, 0, 0]; + vtkMath.cross(left, right, leftRight); + + leftRightNull = + leftRight[0] === 0.0 && leftRight[1] === 0.0 && leftRight[2] === 0.0; + + const bottomTop = [0, 0, 0]; + vtkMath.cross(bottom, top, bottomTop); + bottomTopNull = + bottomTop[0] === 0.0 && bottomTop[1] === 0.0 && bottomTop[2] === 0.0; + parallelFrustum = leftRightNull && bottomTopNull; + + if (parallelFrustum) { + // start at near points, just add the 4 extra far points. + nbPts += 4; + } else if (leftRightNull || bottomTopNull) { + // two extra starting points, and 4 extra far points. + nbPts += 6; + } else { + // there is an apex, and 4 extra far points + nbPts += 5; + } + } + + let pointType = 0; + if (model.outputPointsPrecision === DesiredOutputPrecision.SINGLE) { + pointType = VtkDataTypes.FLOAT; + } else if (model.outputPointsPrecision === DesiredOutputPrecision.DOUBLE) { + pointType = VtkDataTypes.DOUBLE; + } + const newPoints = vtkPoints.newInstance({ dataType: pointType }); + + newPoints.setNumberOfPoints(nbPts); + + const pt = [0.0, 0.0, 0.0]; + const planes = [0, 0, 0]; + + planes[0] = 0; // left + planes[1] = 2; // bottom + planes[2] = 5; // near + computePoint(planes, pt); + newPoints.setPoint(0, ...pt); + + planes[0] = 1; + computePoint(planes, pt); + newPoints.setPoint(1, ...pt); + + planes[1] = 3; + computePoint(planes, pt); + newPoints.setPoint(2, ...pt); + + planes[0] = 0; + computePoint(planes, pt); + newPoints.setPoint(3, ...pt); + + planes[1] = 2; + planes[2] = 4; // far + computePoint(planes, pt); + newPoints.setPoint(4, ...pt); + + planes[0] = 1; + computePoint(planes, pt); + newPoints.setPoint(5, ...pt); + + planes[1] = 3; + computePoint(planes, pt); + newPoints.setPoint(6, ...pt); + + planes[0] = 0; + computePoint(planes, pt); + newPoints.setPoint(7, ...pt); + + const numPolys = 6; + const newPolys = vtkCellArray.newInstance(); + newPolys.allocate(numPolys * 5); + + // left + newPolys.insertNextCell([4, 0, 3, 7]); + + // right + newPolys.insertNextCell([1, 5, 6, 2]); + + // bottom + newPolys.insertNextCell([0, 4, 5, 1]); + + // top + newPolys.insertNextCell([3, 2, 6, 7]); + + // near + newPolys.insertNextCell([0, 1, 2, 3]); + + // far + newPolys.insertNextCell([4, 7, 6, 5]); + + let newLines = null; + if (model.showLines) { + const numLines = 4; + newLines = vtkCellArray.newInstance(); + newLines.allocate(numLines * 3); + + const pts = [12, 8]; // apex, or first of the two extra near points. + + // line from lower-left corner + if (parallelFrustum) { + pts[0] = 0; + } + newLines.insertNextCell(pts); + + // line from lower-right corner + if (parallelFrustum) { + pts[0]++; + } else if (leftRightNull) { + pts[0] = 13; + } + pts[1]++; + newLines.insertNextCell(pts); + + // line from upper-right corner + if (parallelFrustum) { + pts[0]++; + } else if (bottomTopNull) { + pts[0] = 13; + } + pts[1]++; + newLines.insertNextCell(pts); + + // line from upper-left corner + if (parallelFrustum) { + pts[0]++; + } else if (leftRightNull) { + pts[0] = 12; + } + pts[1]++; + newLines.insertNextCell(pts); + output.setLines(newLines); + } + + output.setPoints(newPoints); + output.setPolys(newPolys); + + outData[0] = output; + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + planes: null, + showLines: true, + outputPointsPrecision: DesiredOutputPrecision.DEFAULT, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + // Build VTK API + macro.obj(publicAPI, model); + + // Also make it an algorithm with no input and one output + macro.algo(publicAPI, model, 0, 1); + macro.setGet(publicAPI, model, [ + 'showLines', + 'outputPointsPrecision', + 'planes', + ]); + + vtkFrustumSource(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkFrustumSource'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend };