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

Options

+
Layers: + +
diff --git a/Sources/IO/Misc/GCodeReader/example/index.js b/Sources/IO/Misc/GCodeReader/example/index.js new file mode 100644 index 00000000000..eb921a9cb5f --- /dev/null +++ b/Sources/IO/Misc/GCodeReader/example/index.js @@ -0,0 +1,137 @@ +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 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 hslToRgb(h, s, l) { + let r; + let g; + let b; + + if (s === 0) { + r = l; + g = l; + b = l; + } else { + const hue2rgb = (p, q, t) => { + let tMod = t; + if (tMod < 0) tMod += 1; + if (tMod > 1) tMod -= 1; + if (tMod < 1 / 6) return p + (q - p) * 6 * tMod; + if (tMod < 1 / 2) return q; + if (tMod < 2 / 3) return p + (q - p) * (2 / 3 - tMod) * 6; + return p; + }; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [r, g, b]; +} + +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) * 0.8; // Use 0.8 instead of 1.0 to avoid red wrapping + actor.getProperty().setColor(...hslToRgb(hue, 1, 0.5)); + 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, };