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 @@
+
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,