diff --git a/Documentation/content/docs/gallery/GCodeReaderWithIcon.jpg b/Documentation/content/docs/gallery/GCodeReaderWithIcon.jpg
new file mode 100644
index 00000000000..70c5b3aa9d0
Binary files /dev/null and b/Documentation/content/docs/gallery/GCodeReaderWithIcon.jpg differ
diff --git a/Documentation/content/examples/index.md b/Documentation/content/examples/index.md
index 1207c49b9fa..f06c181c010 100644
--- a/Documentation/content/examples/index.md
+++ b/Documentation/content/examples/index.md
@@ -184,6 +184,7 @@ This will allow you to see the some live code running in your browser. Just pick
[![HttpDataSetSeriesReader Example][HttpDataSetSeriesReaderWithIcon]](./HttpDataSetSeriesReader.html "Import a VTK dataset with time support.")
[![HttpSceneLoader Example][HttpSceneLoaderWithIcon]](./HttpSceneLoader.html "Import a VTK scene (data + representation)")
[![OfflineLocalView Example][OfflineLocalViewWithIcon]](./OfflineLocalView.html "Load a serialized scene (VTKSZ)")
+[![G-Code Example][GCodeReaderWithIcon]](./GCodeReader.html "G-Code reader(gcode)")
@@ -203,6 +204,7 @@ This will allow you to see the some live code running in your browser. Just pick
[HttpDataSetSeriesReaderWithIcon]: ../docs/gallery/HttpDataSetSeriesReaderWithIcon.gif
[HttpSceneLoaderWithIcon]: ../docs/gallery/HttpSceneLoaderWithIcon.jpg
[OfflineLocalViewWithIcon]: ../docs/gallery/OfflineLocalViewWithIcon.jpg
+[GCodeReaderWithIcon]: ../docs/gallery/GCodeReaderWithIcon.jpg
# Actors
diff --git a/Sources/IO/Misc/GCodeReader/example/controller.html b/Sources/IO/Misc/GCodeReader/example/controller.html
new file mode 100644
index 00000000000..42f9ea66af5
--- /dev/null
+++ b/Sources/IO/Misc/GCodeReader/example/controller.html
@@ -0,0 +1,13 @@
+
diff --git a/Sources/IO/Misc/GCodeReader/example/index.js b/Sources/IO/Misc/GCodeReader/example/index.js
new file mode 100644
index 00000000000..86fc4674205
--- /dev/null
+++ b/Sources/IO/Misc/GCodeReader/example/index.js
@@ -0,0 +1,114 @@
+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 vtkMath from '@kitware/vtk.js/Common/Core/Math';
+
+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 vtkGCodeReader from '@kitware/vtk.js/IO/Misc/GCodeReader';
+
+import controlPanel from './controller.html';
+
+// ----------------------------------------------------------------------------
+// Example code
+// ----------------------------------------------------------------------------
+
+let renderer = null;
+let renderWindow = null;
+let layersSelector = null;
+let layersLabel = null;
+
+const reader = vtkGCodeReader.newInstance();
+
+// ----------------------------------------------------------------------------
+
+function refresh() {
+ if (renderer && renderWindow) {
+ renderer.resetCamera();
+ renderWindow.render();
+ }
+}
+
+function update() {
+ const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance();
+ fullScreenRenderer.addController(controlPanel);
+ layersSelector = document.querySelector('.layers');
+ layersLabel = document.querySelector('.label');
+ renderer = fullScreenRenderer.getRenderer();
+ renderWindow = fullScreenRenderer.getRenderWindow();
+ const numLayers = reader.getNumberOfOutputPorts();
+
+ layersLabel.innerHTML = `Layers(${numLayers}):`;
+ layersSelector.max = numLayers.toString();
+ layersSelector.value = numLayers.toString();
+
+ const actors = [];
+
+ for (let idx = 1; idx < numLayers; idx++) {
+ const polyData = reader.getOutputData(idx);
+ if (polyData) {
+ const mapper = vtkMapper.newInstance();
+ mapper.setInputData(polyData);
+
+ const actor = vtkActor.newInstance();
+ actor.setMapper(mapper);
+
+ const hue = idx / numLayers;
+ const saturation = 0.8;
+ const value = 1;
+ const hsv = [hue, saturation, value];
+ const rgb = [];
+ vtkMath.hsv2rgb(hsv, rgb);
+ actor.getProperty().setColor(rgb);
+ actor.rotateX(-90);
+
+ actors.push(actor);
+ }
+ }
+
+ layersSelector.addEventListener('input', (event) => {
+ const visibleLayers = parseInt(event.target.value, 10);
+ actors.forEach((actor, index) => {
+ actor.setVisibility(index < visibleLayers);
+ });
+ renderWindow.render();
+ });
+
+ actors.forEach((actor) => renderer.addActor(actor));
+ refresh();
+}
+
+// ----------------------------------------------------------------------------
+// Use a file reader to load a local file
+// ----------------------------------------------------------------------------
+
+const myContainer = document.querySelector('body');
+const fileContainer = document.createElement('div');
+fileContainer.innerHTML =
+ 'Select a gcode file.
';
+myContainer.appendChild(fileContainer);
+
+const fileInput = fileContainer.querySelector('input');
+
+function handleFile(event) {
+ event.preventDefault();
+ const dataTransfer = event.dataTransfer;
+ const files = event.target.files || dataTransfer.files;
+ myContainer.removeChild(fileContainer);
+ const fileReader = new FileReader();
+ fileReader.onload = function onLoad(e) {
+ reader.parse(fileReader.result);
+ update();
+ };
+ fileReader.readAsArrayBuffer(files[0]);
+}
+
+fileInput.addEventListener('change', handleFile);
+
+// ----------------------------------------------------------------------------
+// Use the reader to download a file
+// ----------------------------------------------------------------------------
+// reader.setUrl(`${__BASE_PATH__}/data/gcode/cube.gcode`, { binary: true }).then(update);
diff --git a/Sources/IO/Misc/GCodeReader/index.d.ts b/Sources/IO/Misc/GCodeReader/index.d.ts
new file mode 100644
index 00000000000..3008ae4702f
--- /dev/null
+++ b/Sources/IO/Misc/GCodeReader/index.d.ts
@@ -0,0 +1,128 @@
+import { vtkAlgorithm, vtkObject } from '../../../interfaces';
+import HtmlDataAccessHelper from '../../Core/DataAccessHelper/HtmlDataAccessHelper';
+import HttpDataAccessHelper from '../../Core/DataAccessHelper/HttpDataAccessHelper';
+import JSZipDataAccessHelper from '../../Core/DataAccessHelper/JSZipDataAccessHelper';
+import LiteHttpDataAccessHelper from '../../Core/DataAccessHelper/LiteHttpDataAccessHelper';
+
+interface IGCodeReaderOptions {
+ binary?: boolean;
+ compression?: string;
+ progressCallback?: any;
+}
+
+/**
+ *
+ */
+export interface IGCodeReaderInitialValues {}
+
+type vtkGCodeReaderBase = vtkObject &
+ Omit<
+ vtkAlgorithm,
+ | 'getInputData'
+ | 'setInputData'
+ | 'setInputConnection'
+ | 'getInputConnection'
+ | 'addInputConnection'
+ | 'addInputData'
+ >;
+
+export interface vtkGCodeReader extends vtkGCodeReaderBase {
+ /**
+ *
+ */
+ getBaseURL(): string;
+
+ /**
+ *
+ */
+ getDataAccessHelper():
+ | HtmlDataAccessHelper
+ | HttpDataAccessHelper
+ | JSZipDataAccessHelper
+ | LiteHttpDataAccessHelper;
+
+ /**
+ * Get the url of the object to load.
+ */
+ getUrl(): string;
+
+ /**
+ * Load the object data.
+ * @param {IGCodeReaderOptions} [options]
+ */
+ loadData(options?: IGCodeReaderOptions): Promise;
+
+ /**
+ * Parse data.
+ * @param {String | ArrayBuffer} content The content to parse.
+ */
+ parse(content: string | ArrayBuffer): void;
+
+ /**
+ * Parse data as ArrayBuffer.
+ * @param {ArrayBuffer} content The content to parse.
+ */
+ parseAsArrayBuffer(content: ArrayBuffer): void;
+
+ /**
+ * Parse data as text.
+ * @param {String} content The content to parse.
+ */
+ parseAsText(content: string): void;
+
+ /**
+ *
+ * @param inData
+ * @param outData
+ */
+ requestData(inData: any, outData: any): void;
+
+ /**
+ *
+ * @param dataAccessHelper
+ */
+ setDataAccessHelper(
+ dataAccessHelper:
+ | HtmlDataAccessHelper
+ | HttpDataAccessHelper
+ | JSZipDataAccessHelper
+ | LiteHttpDataAccessHelper
+ ): boolean;
+
+ /**
+ * Set the url of the object to load.
+ * @param {String} url the url of the object to load.
+ * @param {IGCodeReaderOptions} [option] The PLY reader options.
+ */
+ setUrl(url: string, option?: IGCodeReaderOptions): Promise;
+}
+
+/**
+ * Method used to decorate a given object (publicAPI+model) with vtkGCodeReader characteristics.
+ *
+ * @param publicAPI object on which methods will be bounds (public)
+ * @param model object on which data structure will be bounds (protected)
+ * @param {IGCodeReaderInitialValues} [initialValues] (default: {})
+ */
+export function extend(
+ publicAPI: object,
+ model: object,
+ initialValues?: IGCodeReaderInitialValues
+): void;
+
+/**
+ * Method used to create a new instance of vtkGCodeReader
+ * @param {IGCodeReaderInitialValues} [initialValues] for pre-setting some of its content
+ */
+export function newInstance(
+ initialValues?: IGCodeReaderInitialValues
+): vtkGCodeReader;
+
+/**
+ * vtkGCodeReader is a source object that reads a GCODE file.
+ */
+export declare const vtkGCodeReader: {
+ newInstance: typeof newInstance;
+ extend: typeof extend;
+};
+export default vtkGCodeReader;
diff --git a/Sources/IO/Misc/GCodeReader/index.js b/Sources/IO/Misc/GCodeReader/index.js
new file mode 100644
index 00000000000..4993a33b85f
--- /dev/null
+++ b/Sources/IO/Misc/GCodeReader/index.js
@@ -0,0 +1,257 @@
+import macro from 'vtk.js/Sources/macros';
+import BinaryHelper from 'vtk.js/Sources/IO/Core/BinaryHelper';
+import DataAccessHelper from 'vtk.js/Sources/IO/Core/DataAccessHelper';
+import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData';
+import vtkPoints from 'vtk.js/Sources/Common/Core/Points';
+import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray';
+
+// Enable several sources for DataAccessHelper
+import 'vtk.js/Sources/IO/Core/DataAccessHelper/LiteHttpDataAccessHelper'; // Just need HTTP
+// import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; // HTTP + gz
+// import 'vtk.js/Sources/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; // html + base64 + zip
+// import 'vtk.js/Sources/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; // zip
+
+const state = {};
+
+// ----------------------------------------------------------------------------
+// vtkGCodeReader methods
+// ----------------------------------------------------------------------------
+function vtkGCodeReader(publicAPI, model) {
+ state.currentPosition = { x: 0, y: 0, z: 0 };
+ state.offset = { x: 0, y: 0, z: 0 };
+ state.currentLayer = 0;
+ state.layers = new Map(); // Map to store layer data
+ state.isAbsolute = true; // G90 is default
+ state.isMetric = true; // G21 is default
+ state.lastZ = 0; // Track Z changes for layer detection
+
+ model.classHierarchy.push('vtkGCodeReader');
+
+ if (!model.dataAccessHelper) {
+ model.dataAccessHelper = DataAccessHelper.get('http');
+ }
+
+ function fetchData(url, option = {}) {
+ const { compression, progressCallback } = model;
+ if (option.binary) {
+ return model.dataAccessHelper.fetchBinary(url, {
+ compression,
+ progressCallback,
+ });
+ }
+ return model.dataAccessHelper.fetchText(publicAPI, url, {
+ compression,
+ progressCallback,
+ });
+ }
+
+ function detectLayerChange(newZ) {
+ if (Math.abs(newZ - state.lastZ) > 0.001) {
+ state.currentLayer++;
+ state.lastZ = newZ;
+ return true;
+ }
+ return false;
+ }
+
+ function initializeLayer() {
+ if (!state.layers.has(state.currentLayer)) {
+ const points = vtkPoints.newInstance();
+ const lines = vtkCellArray.newInstance();
+ const polyData = vtkPolyData.newInstance();
+
+ polyData.setPoints(points);
+ polyData.setLines(lines);
+
+ state.layers.set(state.currentLayer, {
+ polyData,
+ points,
+ lines,
+ zHeight: state.lastZ,
+ });
+ }
+ }
+
+ function addLineToLayer(startPoint, endPoint) {
+ initializeLayer();
+ const layer = state.layers.get(state.currentLayer);
+
+ // Add points and get their indices
+ const startIndex = layer.points.insertNextPoint(
+ startPoint[0],
+ startPoint[1],
+ startPoint[2]
+ );
+ const endIndex = layer.points.insertNextPoint(
+ endPoint[0],
+ endPoint[1],
+ endPoint[2]
+ );
+
+ // Add line cell
+ layer.lines.insertNextCell([startIndex, endIndex]);
+ }
+
+ function processMove(params) {
+ const newPosition = { ...state.currentPosition };
+ let positionChanged = false;
+
+ ['X', 'Y', 'Z'].forEach((axis) => {
+ if (axis in params) {
+ const value = state.isMetric ? params[axis] : params[axis] * 25.4;
+ newPosition[axis.toLowerCase()] = state.isAbsolute
+ ? value + state.offset[axis.toLowerCase()]
+ : state.currentPosition[axis.toLowerCase()] + value;
+ positionChanged = true;
+ }
+ });
+
+ if (positionChanged) {
+ if ('Z' in params) {
+ detectLayerChange(newPosition.z);
+ }
+
+ const startPoint = [
+ state.currentPosition.x,
+ state.currentPosition.y,
+ state.currentPosition.z,
+ ];
+ const endPoint = [newPosition.x, newPosition.y, newPosition.z];
+
+ addLineToLayer(startPoint, endPoint);
+ state.currentPosition = newPosition;
+ }
+ }
+
+ function processG92(params) {
+ ['X', 'Y', 'Z'].forEach((axis) => {
+ if (axis in params) {
+ state.offset[axis.toLowerCase()] =
+ state.currentPosition[axis.toLowerCase()] -
+ (state.isMetric ? params[axis] : params[axis] * 25.4);
+ }
+ });
+ }
+
+ function processCommand(command, params) {
+ switch (command) {
+ case 'G0': // Rapid move
+ case 'G1': // Linear move
+ processMove(params);
+ break;
+ case 'G20': // Imperial
+ state.isMetric = false;
+ break;
+ case 'G21': // Metric
+ state.isMetric = true;
+ break;
+ case 'G90': // Absolute positioning
+ state.isAbsolute = true;
+ break;
+ case 'G91': // Relative positioning
+ state.isAbsolute = false;
+ break;
+ case 'G92': // Set position
+ processG92(params);
+ break;
+ default:
+ break;
+ }
+ }
+
+ function parseGCode(gcodeText) {
+ const lines = gcodeText.split('\n');
+
+ lines.forEach((line) => {
+ const sline = line.split(';')[0].trim();
+ if (!sline) return;
+
+ const tokens = sline.split(' ');
+ const command = tokens[0];
+
+ const params = {};
+ tokens.slice(1).forEach((token) => {
+ const param = token[0];
+ const value = parseFloat(token.slice(1));
+ if (!Number.isNaN(value)) {
+ params[param] = value;
+ }
+ });
+
+ processCommand(command, params);
+ });
+ }
+
+ // Public methods
+ publicAPI.setUrl = (url, option = { binary: true }) => {
+ model.url = url;
+ const path = url.split('/');
+ path.pop();
+ model.baseURL = path.join('/');
+ model.compression = option.compression;
+
+ return publicAPI.loadData({
+ progressCallback: option.progressCallback,
+ binary: !!option.binary,
+ });
+ };
+
+ publicAPI.loadData = (option = {}) => {
+ const promise = fetchData(model.url, option);
+ promise.then(publicAPI.parse);
+ return promise;
+ };
+
+ publicAPI.parseAsText = (content) => {
+ parseGCode(content);
+ };
+
+ publicAPI.parseAsArrayBuffer = (content) => {
+ const data = BinaryHelper.arrayBufferToString(content);
+ parseGCode(data);
+ };
+
+ publicAPI.parse = (content) => {
+ if (typeof content === 'string') {
+ publicAPI.parseAsText(content);
+ } else {
+ publicAPI.parseAsArrayBuffer(content);
+ }
+
+ state.layers.forEach((layer, i) => {
+ model.output[i] = layer.polyData;
+ });
+ };
+
+ publicAPI.requestData = (inData, outData) => {
+ publicAPI.parse(model.parseData);
+ };
+
+ publicAPI.getNumberOfOutputPorts = () => state.layers.size;
+}
+
+const DEFAULT_VALUES = {
+ // baseURL: null,
+ // dataAccessHelper: null,
+ // url: null,
+};
+
+export function extend(publicAPI, model, initialValues = {}) {
+ Object.assign(model, DEFAULT_VALUES, initialValues);
+
+ // Build VTK API
+ macro.obj(publicAPI, model);
+ macro.get(publicAPI, model, ['url', 'baseURL']);
+ macro.setGet(publicAPI, model, ['dataAccessHelper']);
+ macro.algo(publicAPI, model, 0, 1);
+ macro.event(publicAPI, model, 'ready');
+
+ vtkGCodeReader(publicAPI, model);
+}
+
+export const newInstance = macro.newInstance(extend, 'vtkGCodeReader');
+
+export default {
+ extend,
+ newInstance,
+};
diff --git a/Sources/IO/Misc/index.js b/Sources/IO/Misc/index.js
index 3563a47257c..57704ad77c3 100644
--- a/Sources/IO/Misc/index.js
+++ b/Sources/IO/Misc/index.js
@@ -7,6 +7,7 @@ import vtkMTLReader from './MTLReader';
import vtkOBJReader from './OBJReader';
import vtkPDBReader from './PDBReader';
import vtkSkyboxReader from './SkyboxReader';
+import vtkGCodeReader from './GCodeReader';
export default {
vtkElevationReader,
@@ -18,4 +19,5 @@ export default {
vtkOBJReader,
vtkPDBReader,
vtkSkyboxReader,
+ vtkGCodeReader,
};