├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── babel.config.js ├── package.json ├── rollup.config.mjs ├── src ├── Toolpath.js ├── __tests__ │ ├── fixtures │ │ ├── arc-no-plane.nc │ │ ├── arc-r.nc │ │ ├── arc-xy-plane.nc │ │ ├── arc-yz-plane.nc │ │ ├── arc-zx-plane.nc │ │ ├── circle.nc │ │ ├── coordinate.nc │ │ ├── dwell.nc │ │ ├── feedrate.nc │ │ ├── g92offset.nc │ │ ├── helical-thread-milling.nc │ │ ├── linear.nc │ │ ├── motion.nc │ │ ├── one-inch-circle.nc │ │ ├── t2laser.nc │ │ └── units.nc │ └── index.test.js └── index.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'eslint:recommended', 3 | parser: '@babel/eslint-parser', 4 | env: { 5 | browser: true, 6 | node: true, 7 | jest: true, 8 | }, 9 | plugins: [ 10 | '@babel', 11 | ], 12 | rules: { 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: [20.x] 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: yarn 26 | - run: | 27 | yarn run eslint 28 | yarn run build 29 | yarn run test 30 | - uses: codecov/codecov-action@v4 31 | env: 32 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /coverage 3 | /dist 4 | node_modules 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Cheton Wu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gcode-toolpath [![codecov](https://codecov.io/gh/cncjs/gcode-toolpath/graph/badge.svg?token=JHJU80PHU8)](https://codecov.io/gh/cncjs/gcode-toolpath) 2 | 3 | [![NPM](https://nodei.co/npm/gcode-toolpath.png?downloads=true&stars=true)](https://www.npmjs.com/package/gcode-toolpath) 4 | 5 | ## Install 6 | 7 | `npm install --save gcode-toolpath` 8 | 9 | ## Usage 10 | 11 | ```js 12 | const Toolpath = require('gcode-toolpath'); 13 | 14 | const toolpaths = []; 15 | const toolpath = new Toolpath({ 16 | // Initial position (optional) 17 | position: { x: 0, y: 0, z: 0 }, 18 | 19 | // Initial modal state (optional) 20 | modal: { 21 | motion: 'G0', // G0, G1, G2, G3, G38.2, G38.3, G38.4, G38.5, G80 22 | wcs: 'G54', // G54, G55, G56, G57, G58, G59 23 | plane: 'G17', // G17: xy-plane, G18: xz-plane, G19: yz-plane 24 | units: 'G21', // G20: Inches, G21: Millimeters 25 | distance: 'G90', // G90: Absolute, G91: Relative 26 | feedrate: 'G94', // G93: Inverse time mode, G94: Units per minute, G95: Units per rev 27 | program: 'M0', // M0, M1, M2, M30 28 | spindle: 'M5', // M3, M4, M5 29 | coolant: 'M9', // M7, M8, M9 30 | tool: 0 31 | }, 32 | 33 | // @param {object} modal The modal object. 34 | // @param {object} v1 A 3D vector of the start point. 35 | // @param {object} v2 A 3D vector of the end point. 36 | addLine: (modal, v1, v2) => { 37 | const motion = modal.motion; 38 | const tool = modal.tool; 39 | toolpaths.push({ motion: motion, tool: tool, v1: v1, v2: v2 }); 40 | }, 41 | 42 | // @param {object} modal The modal object. 43 | // @param {object} v1 A 3D vector of the start point. 44 | // @param {object} v2 A 3D vector of the end point. 45 | // @param {object} v0 A 3D vector of the fixed point. 46 | addArcCurve: (modal, v1, v2, v0) => { 47 | const motion = modal.motion; 48 | const tool = modal.tool; 49 | toolpaths.push({ motion: motion, tool: tool, v1: v1, v2: v2, v0: v0 }); 50 | } 51 | }); 52 | 53 | // Position 54 | toolpath.setPosition({ x: 100, y: 10 }); // x=100, y=10, z=0 55 | toolpath.setPosition(10, 20, 30); // x=10, y=20, z=30 56 | 57 | // Modal 58 | toolpath.setModal({ tool: 1 }); 59 | 60 | // Load G-code from file 61 | const file = 'example.nc'; 62 | toolpath.loadFromFile(file, function(err, data) { 63 | }); 64 | 65 | // Load G-code from stream 66 | const stream = fs.createReadStream(file, { encoding: 'utf8' }); 67 | toolpath.loadFromStream(stream, function(err, data) { 68 | }); 69 | 70 | // Load G-code from string 71 | const str = fs.readFileSync(file, 'utf8'); 72 | toolpath.loadFromString(str, function(err, data) { 73 | }); 74 | ``` 75 | 76 | ## Examples 77 | 78 | Run this example with babel-node: 79 | ```js 80 | import Toolpath from 'gcode-toolpath'; 81 | 82 | const GCODE = [ 83 | 'N1 T2 G17 G20 G90 G94 G54', 84 | 'N2 G0 Z0.25', 85 | 'N3 X-0.5 Y0.', 86 | 'N4 Z0.1', 87 | 'N5 G01 Z0. F5.', 88 | 'N6 G02 X0. Y0.5 I0.5 J0. F2.5', 89 | 'N7 X0.5 Y0. I0. J-0.5', 90 | 'N8 X0. Y-0.5 I-0.5 J0.', 91 | 'N9 X-0.5 Y0. I0. J0.5', 92 | 'N10 G01 Z0.1 F5.', 93 | 'N11 G00 X0. Y0. Z0.25' 94 | ].join('\n'); 95 | 96 | const toolpaths = []; 97 | const toolpath = new Toolpath({ 98 | // @param {object} modal The modal object. 99 | // @param {object} v1 A 3D vector of the start point. 100 | // @param {object} v2 A 3D vector of the end point. 101 | addLine: (modal, v1, v2) => { 102 | const motion = modal.motion; 103 | const tool = modal.tool; 104 | toolpaths.push({ motion: motion, tool: tool, v1: v1, v2: v2 }); 105 | }, 106 | // @param {object} modal The modal object. 107 | // @param {object} v1 A 3D vector of the start point. 108 | // @param {object} v2 A 3D vector of the end point. 109 | // @param {object} v0 A 3D vector of the fixed point. 110 | addArcCurve: (modal, v1, v2, v0) => { 111 | const motion = modal.motion; 112 | const tool = modal.tool; 113 | toolpaths.push({ motion: motion, tool: tool, v1: v1, v2: v2, v0: v0 }); 114 | } 115 | }); 116 | 117 | toolpath 118 | .loadFromString(GCODE, (err, results) => { 119 | console.log(toolpaths); 120 | }) 121 | .on('data', (data) => { 122 | // 'data' event listener 123 | }) 124 | .on('end', (results) => { 125 | // 'end' event listener 126 | }); 127 | ``` 128 | 129 | and you will see the output as below: 130 | ```js 131 | [ { motion: 'G0', 132 | tool: 2, 133 | v1: { x: 0, y: 0, z: 0 }, 134 | v2: { x: 0, y: 0, z: 6.35 } }, 135 | { motion: 'G0', 136 | tool: 2, 137 | v1: { x: 0, y: 0, z: 6.35 }, 138 | v2: { x: -12.7, y: 0, z: 6.35 } }, 139 | { motion: 'G0', 140 | tool: 2, 141 | v1: { x: -12.7, y: 0, z: 6.35 }, 142 | v2: { x: -12.7, y: 0, z: 2.54 } }, 143 | { motion: 'G1', 144 | tool: 2, 145 | v1: { x: -12.7, y: 0, z: 2.54 }, 146 | v2: { x: -12.7, y: 0, z: 0 } }, 147 | { motion: 'G2', 148 | tool: 2, 149 | v1: { x: -12.7, y: 0, z: 0 }, 150 | v2: { x: 0, y: 12.7, z: 0 }, 151 | v0: { x: 0, y: 0, z: 0 } }, 152 | { motion: 'G2', 153 | tool: 2, 154 | v1: { x: 0, y: 12.7, z: 0 }, 155 | v2: { x: 12.7, y: 0, z: 0 }, 156 | v0: { x: 0, y: 0, z: 0 } }, 157 | { motion: 'G2', 158 | tool: 2, 159 | v1: { x: 12.7, y: 0, z: 0 }, 160 | v2: { x: 0, y: -12.7, z: 0 }, 161 | v0: { x: 0, y: 0, z: 0 } }, 162 | { motion: 'G2', 163 | tool: 2, 164 | v1: { x: 0, y: -12.7, z: 0 }, 165 | v2: { x: -12.7, y: 0, z: 0 }, 166 | v0: { x: 0, y: 0, z: 0 } }, 167 | { motion: 'G1', 168 | tool: 2, 169 | v1: { x: -12.7, y: 0, z: 0 }, 170 | v2: { x: -12.7, y: 0, z: 2.54 } }, 171 | { motion: 'G0', 172 | tool: 2, 173 | v1: { x: -12.7, y: 0, z: 2.54 }, 174 | v2: { x: 0, y: 0, z: 6.35 } } ] 175 | ``` 176 | 177 | ## G-code Toolpath Visualizer 178 | Check out the source code at https://github.com/cncjs/cnc/blob/master/src/web/widgets/Visualizer/GCodeVisualizer.js 179 | 180 | ## License 181 | 182 | MIT 183 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@trendmicro/babel-config', 3 | presets: [ 4 | '@babel/preset-env', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gcode-toolpath", 3 | "version": "3.0.0", 4 | "description": "G-code Toolpath Generator", 5 | "author": "Cheton Wu", 6 | "homepage": "https://github.com/cncjs/gcode-toolpath", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:cncjs/gcode-toolpath.git" 11 | }, 12 | "engines": { 13 | "node": ">=4" 14 | }, 15 | "keywords": [ 16 | "cnc", 17 | "gcode" 18 | ], 19 | "scripts": { 20 | "build": "cross-env rollup --config rollup.config.mjs", 21 | "clean": "del build coverage dist", 22 | "eslint": "eslint --ext .js,.jsx,.mjs .", 23 | "pre-push": "bash -c 'echo -e \"=> \\e[1;33m$npm_package_name\\e[0m\"' && yarn run build && yarn run eslint && yarn run test", 24 | "prepublish": "yarn run build", 25 | "test": "jest --maxWorkers=2" 26 | }, 27 | "sideEffects": false, 28 | "main": "dist/cjs/index.js", 29 | "module": "dist/esm/index.js", 30 | "files": [ 31 | "dist" 32 | ], 33 | "dependencies": { 34 | "gcode-interpreter": "^3.0.0" 35 | }, 36 | "devDependencies": { 37 | "@babel/cli": "^7.0.0", 38 | "@babel/core": "^7.0.0", 39 | "@babel/eslint-parser": "^7.0.0", 40 | "@babel/eslint-plugin": "^7.0.0", 41 | "@babel/plugin-transform-runtime": "^7.0.0", 42 | "@babel/preset-env": "^7.0.0", 43 | "@rollup/plugin-babel": "^6.0.0", 44 | "@rollup/plugin-node-resolve": "^15.0.0", 45 | "@trendmicro/babel-config": "^1.0.2", 46 | "cross-env": "^7.0.3", 47 | "del-cli": "^5.0.0", 48 | "eslint": "^8.25.0", 49 | "jest": "^29.0.0", 50 | "rollup": "^3" 51 | }, 52 | "jest": { 53 | "collectCoverage": true, 54 | "collectCoverageFrom": [ 55 | "/src/**/*.js" 56 | ], 57 | "coverageReporters": [ 58 | "lcov", 59 | "text", 60 | "html" 61 | ], 62 | "modulePathIgnorePatterns": [], 63 | "setupFiles": [], 64 | "setupFilesAfterEnv": [], 65 | "testEnvironment": "node", 66 | "testMatch": [ 67 | "/**/__tests__/**/*.test.js" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { babel } from '@rollup/plugin-babel'; 4 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | const input = path.resolve(__dirname, 'src', 'index.js'); 10 | const cjsOutputDirectory = path.resolve(__dirname, 'dist', 'cjs'); 11 | const esmOutputDirectory = path.resolve(__dirname, 'dist', 'esm'); 12 | const isExternal = id => !id.startsWith('.') && !id.startsWith('/'); 13 | 14 | export default [ 15 | { 16 | input, 17 | output: { 18 | dir: cjsOutputDirectory, 19 | format: 'cjs', 20 | 21 | // https://rollupjs.org/guide/en/#changed-defaults 22 | // https://rollupjs.org/guide/en/#outputinterop 23 | interop: 'auto', 24 | preserveModules: true, 25 | }, 26 | external: isExternal, 27 | plugins: [ 28 | nodeResolve(), 29 | babel({ babelHelpers: 'bundled' }), 30 | ], 31 | }, 32 | { 33 | input, 34 | output: { 35 | dir: esmOutputDirectory, 36 | format: 'esm', 37 | preserveModules: true, 38 | }, 39 | external: isExternal, 40 | plugins: [ 41 | nodeResolve(), 42 | babel({ babelHelpers: 'bundled' }), 43 | ], 44 | } 45 | ]; 46 | -------------------------------------------------------------------------------- /src/Toolpath.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-self-assign */ 2 | /* eslint-disable no-unused-vars */ 3 | import Interpreter from 'gcode-interpreter'; 4 | 5 | // from in to mm 6 | const in2mm = (val = 0) => val * 25.4; 7 | 8 | // noop 9 | const noop = () => {}; 10 | 11 | const translatePosition = (position, newPosition, relative) => { 12 | relative = !!relative; 13 | newPosition = Number(newPosition); 14 | if (Number.isNaN(newPosition)) { 15 | return position; 16 | } 17 | return relative ? (position + newPosition) : newPosition; 18 | }; 19 | 20 | class Toolpath { 21 | g92offset = { 22 | x: 0, 23 | y: 0, 24 | z: 0 25 | }; 26 | 27 | offsetG92 = (pos) => { 28 | return { 29 | x: pos.x + this.g92offset.x, 30 | y: pos.y + this.g92offset.y, 31 | z: pos.z + this.g92offset.z 32 | }; 33 | } 34 | offsetAddLine = (start, end) => { 35 | this.fn.addLine(this.modal, this.offsetG92(start), this.offsetG92(end)); 36 | } 37 | offsetAddArcCurve = (start, end, center) => { 38 | this.fn.addArcCurve(this.modal, this.offsetG92(start), this.offsetG92(end), this.offsetG92(center)); 39 | } 40 | 41 | position = { 42 | x: 0, 43 | y: 0, 44 | z: 0 45 | }; 46 | 47 | modal = { 48 | // Moton Mode 49 | // G0, G1, G2, G3, G38.2, G38.3, G38.4, G38.5, G80 50 | motion: 'G0', 51 | 52 | // Coordinate System Select 53 | // G54, G55, G56, G57, G58, G59 54 | wcs: 'G54', 55 | 56 | // Plane Select 57 | // G17: XY-plane, G18: ZX-plane, G19: YZ-plane 58 | plane: 'G17', 59 | 60 | // Units Mode 61 | // G20: Inches, G21: Millimeters 62 | units: 'G21', 63 | 64 | // Distance Mode 65 | // G90: Absolute, G91: Relative 66 | distance: 'G90', 67 | 68 | // Arc IJK distance mode 69 | arc: 'G91.1', 70 | 71 | // Feed Rate Mode 72 | // G93: Inverse time mode, G94: Units per minute mode, G95: Units per rev mode 73 | feedrate: 'G94', 74 | 75 | // Cutter Radius Compensation 76 | cutter: 'G40', 77 | 78 | // Tool Length Offset 79 | // G43.1, G49 80 | tlo: 'G49', 81 | 82 | // Program Mode 83 | // M0, M1, M2, M30 84 | program: 'M0', 85 | 86 | // Spingle State 87 | // M3, M4, M5 88 | spindle: 'M5', 89 | 90 | // Coolant State 91 | // M7, M8, M9 92 | coolant: 'M9', // 'M7', 'M8', 'M7,M8', or 'M9' 93 | 94 | // Tool Select 95 | tool: 0 96 | }; 97 | 98 | handlers = { 99 | // G0: Rapid Linear Move 100 | 'G0': (params) => { 101 | if (this.modal.motion !== 'G0') { 102 | this.setModal({ motion: 'G0' }); 103 | } 104 | 105 | const v1 = { 106 | x: this.position.x, 107 | y: this.position.y, 108 | z: this.position.z 109 | }; 110 | const v2 = { 111 | x: this.translateX(params.X), 112 | y: this.translateY(params.Y), 113 | z: this.translateZ(params.Z) 114 | }; 115 | const targetPosition = { x: v2.x, y: v2.y, z: v2.z }; 116 | 117 | this.offsetAddLine(v1, v2); 118 | 119 | // Update position 120 | this.setPosition(targetPosition.x, targetPosition.y, targetPosition.z); 121 | }, 122 | // G1: Linear Move 123 | // Usage 124 | // G1 Xnnn Ynnn Znnn Ennn Fnnn Snnn 125 | // Parameters 126 | // Xnnn The position to move to on the X axis 127 | // Ynnn The position to move to on the Y axis 128 | // Znnn The position to move to on the Z axis 129 | // Fnnn The feedrate per minute of the move between the starting point and ending point (if supplied) 130 | // Snnn Flag to check if an endstop was hit (S1 to check, S0 to ignore, S2 see note, default is S0) 131 | // Examples 132 | // G1 X12 (move to 12mm on the X axis) 133 | // G1 F1500 (Set the feedrate to 1500mm/minute) 134 | // G1 X90.6 Y13.8 E22.4 (Move to 90.6mm on the X axis and 13.8mm on the Y axis while extruding 22.4mm of material) 135 | // 136 | 'G1': (params) => { 137 | if (this.modal.motion !== 'G1') { 138 | this.setModal({ motion: 'G1' }); 139 | } 140 | 141 | const v1 = { 142 | x: this.position.x, 143 | y: this.position.y, 144 | z: this.position.z 145 | }; 146 | const v2 = { 147 | x: this.translateX(params.X), 148 | y: this.translateY(params.Y), 149 | z: this.translateZ(params.Z) 150 | }; 151 | const targetPosition = { x: v2.x, y: v2.y, z: v2.z }; 152 | 153 | this.offsetAddLine(v1, v2); 154 | 155 | // Update position 156 | this.setPosition(targetPosition.x, targetPosition.y, targetPosition.z); 157 | }, 158 | // G2 & G3: Controlled Arc Move 159 | // Usage 160 | // G2 Xnnn Ynnn Innn Jnnn Ennn Fnnn (Clockwise Arc) 161 | // G3 Xnnn Ynnn Innn Jnnn Ennn Fnnn (Counter-Clockwise Arc) 162 | // Parameters 163 | // Xnnn The position to move to on the X axis 164 | // Ynnn The position to move to on the Y axis 165 | // Innn The point in X space from the current X position to maintain a constant distance from 166 | // Jnnn The point in Y space from the current Y position to maintain a constant distance from 167 | // Fnnn The feedrate per minute of the move between the starting point and ending point (if supplied) 168 | // Examples 169 | // G2 X90.6 Y13.8 I5 J10 E22.4 (Move in a Clockwise arc from the current point to point (X=90.6,Y=13.8), 170 | // with a center point at (X=current_X+5, Y=current_Y+10), extruding 22.4mm of material between starting and stopping) 171 | // G3 X90.6 Y13.8 I5 J10 E22.4 (Move in a Counter-Clockwise arc from the current point to point (X=90.6,Y=13.8), 172 | // with a center point at (X=current_X+5, Y=current_Y+10), extruding 22.4mm of material between starting and stopping) 173 | // Referring 174 | // http://linuxcnc.org/docs/2.5/html/gcode/gcode.html#sec:G2-G3-Arc 175 | // https://github.com/grbl/grbl/issues/236 176 | 'G2': (params) => { 177 | if (this.modal.motion !== 'G2') { 178 | this.setModal({ motion: 'G2' }); 179 | } 180 | 181 | const v1 = { 182 | x: this.position.x, 183 | y: this.position.y, 184 | z: this.position.z 185 | }; 186 | const v2 = { 187 | x: this.translateX(params.X), 188 | y: this.translateY(params.Y), 189 | z: this.translateZ(params.Z) 190 | }; 191 | const v0 = { // fixed point 192 | x: this.translateI(params.I), 193 | y: this.translateJ(params.J), 194 | z: this.translateK(params.K) 195 | }; 196 | const isClockwise = true; 197 | const targetPosition = { x: v2.x, y: v2.y, z: v2.z }; 198 | 199 | if (this.isXYPlane()) { // XY-plane 200 | [v1.x, v1.y, v1.z] = [v1.x, v1.y, v1.z]; 201 | [v2.x, v2.y, v2.z] = [v2.x, v2.y, v2.z]; 202 | [v0.x, v0.y, v0.z] = [v0.x, v0.y, v0.z]; 203 | } else if (this.isZXPlane()) { // ZX-plane 204 | [v1.x, v1.y, v1.z] = [v1.z, v1.x, v1.y]; 205 | [v2.x, v2.y, v2.z] = [v2.z, v2.x, v2.y]; 206 | [v0.x, v0.y, v0.z] = [v0.z, v0.x, v0.y]; 207 | } else if (this.isYZPlane()) { // YZ-plane 208 | [v1.x, v1.y, v1.z] = [v1.y, v1.z, v1.x]; 209 | [v2.x, v2.y, v2.z] = [v2.y, v2.z, v2.x]; 210 | [v0.x, v0.y, v0.z] = [v0.y, v0.z, v0.x]; 211 | } else { 212 | console.error('The plane mode is invalid', this.modal.plane); 213 | return; 214 | } 215 | 216 | if (params.R) { 217 | const radius = this.translateR(Number(params.R) || 0); 218 | const x = v2.x - v1.x; 219 | const y = v2.y - v1.y; 220 | const distance = Math.sqrt(x * x + y * y); 221 | let height = Math.sqrt(4 * radius * radius - x * x - y * y) / 2; 222 | 223 | if (isClockwise) { 224 | height = -height; 225 | } 226 | if (radius < 0) { 227 | height = -height; 228 | } 229 | 230 | const offsetX = x / 2 - y / distance * height; 231 | const offsetY = y / 2 + x / distance * height; 232 | 233 | v0.x = v1.x + offsetX; 234 | v0.y = v1.y + offsetY; 235 | } 236 | 237 | this.offsetAddArcCurve(v1, v2, v0); 238 | 239 | // Update position 240 | this.setPosition(targetPosition.x, targetPosition.y, targetPosition.z); 241 | }, 242 | 'G3': (params) => { 243 | if (this.modal.motion !== 'G3') { 244 | this.setModal({ motion: 'G3' }); 245 | } 246 | 247 | const v1 = { 248 | x: this.position.x, 249 | y: this.position.y, 250 | z: this.position.z 251 | }; 252 | const v2 = { 253 | x: this.translateX(params.X), 254 | y: this.translateY(params.Y), 255 | z: this.translateZ(params.Z) 256 | }; 257 | const v0 = { // fixed point 258 | x: this.translateI(params.I), 259 | y: this.translateJ(params.J), 260 | z: this.translateK(params.K) 261 | }; 262 | const isClockwise = false; 263 | const targetPosition = { x: v2.x, y: v2.y, z: v2.z }; 264 | 265 | if (this.isXYPlane()) { // XY-plane 266 | [v1.x, v1.y, v1.z] = [v1.x, v1.y, v1.z]; 267 | [v2.x, v2.y, v2.z] = [v2.x, v2.y, v2.z]; 268 | [v0.x, v0.y, v0.z] = [v0.x, v0.y, v0.z]; 269 | } else if (this.isZXPlane()) { // ZX-plane 270 | [v1.x, v1.y, v1.z] = [v1.z, v1.x, v1.y]; 271 | [v2.x, v2.y, v2.z] = [v2.z, v2.x, v2.y]; 272 | [v0.x, v0.y, v0.z] = [v0.z, v0.x, v0.y]; 273 | } else if (this.isYZPlane()) { // YZ-plane 274 | [v1.x, v1.y, v1.z] = [v1.y, v1.z, v1.x]; 275 | [v2.x, v2.y, v2.z] = [v2.y, v2.z, v2.x]; 276 | [v0.x, v0.y, v0.z] = [v0.y, v0.z, v0.x]; 277 | } else { 278 | console.error('The plane mode is invalid', this.modal.plane); 279 | return; 280 | } 281 | 282 | if (params.R) { 283 | const radius = this.translateR(Number(params.R) || 0); 284 | const x = v2.x - v1.x; 285 | const y = v2.y - v1.y; 286 | const distance = Math.sqrt(x * x + y * y); 287 | let height = Math.sqrt(4 * radius * radius - x * x - y * y) / 2; 288 | 289 | if (isClockwise) { 290 | height = -height; 291 | } 292 | if (radius < 0) { 293 | height = -height; 294 | } 295 | 296 | const offsetX = x / 2 - y / distance * height; 297 | const offsetY = y / 2 + x / distance * height; 298 | 299 | v0.x = v1.x + offsetX; 300 | v0.y = v1.y + offsetY; 301 | } 302 | 303 | this.offsetAddArcCurve(v1, v2, v0); 304 | 305 | // Update position 306 | this.setPosition(targetPosition.x, targetPosition.y, targetPosition.z); 307 | }, 308 | // G4: Dwell 309 | // Parameters 310 | // Pnnn Time to wait, in milliseconds 311 | // Snnn Time to wait, in seconds (Only on Marlin and Smoothie) 312 | // Example 313 | // G4 P200 314 | 'G4': (params) => { 315 | }, 316 | // G10: Coordinate System Data Tool and Work Offset Tables 317 | 'G10': (params) => { 318 | }, 319 | // G17..19: Plane Selection 320 | // G17: XY (default) 321 | 'G17': (params) => { 322 | if (this.modal.plane !== 'G17') { 323 | this.setModal({ plane: 'G17' }); 324 | } 325 | }, 326 | // G18: XZ 327 | 'G18': (params) => { 328 | if (this.modal.plane !== 'G18') { 329 | this.setModal({ plane: 'G18' }); 330 | } 331 | }, 332 | // G19: YZ 333 | 'G19': (params) => { 334 | if (this.modal.plane !== 'G19') { 335 | this.setModal({ plane: 'G19' }); 336 | } 337 | }, 338 | // G20: Use inches for length units 339 | 'G20': (params) => { 340 | if (this.modal.units !== 'G20') { 341 | this.setModal({ units: 'G20' }); 342 | } 343 | }, 344 | // G21: Use millimeters for length units 345 | 'G21': (params) => { 346 | if (this.modal.units !== 'G21') { 347 | this.setModal({ units: 'G21' }); 348 | } 349 | }, 350 | // G38.x: Straight Probe 351 | // G38.2: Probe toward workpiece, stop on contact, signal error if failure 352 | 'G38.2': (params) => { 353 | if (this.modal.motion !== 'G38.2') { 354 | this.setModal({ motion: 'G38.2' }); 355 | } 356 | }, 357 | // G38.3: Probe toward workpiece, stop on contact 358 | 'G38.3': (params) => { 359 | if (this.modal.motion !== 'G38.3') { 360 | this.setModal({ motion: 'G38.3' }); 361 | } 362 | }, 363 | // G38.4: Probe away from workpiece, stop on loss of contact, signal error if failure 364 | 'G38.4': (params) => { 365 | if (this.modal.motion !== 'G38.4') { 366 | this.setModal({ motion: 'G38.4' }); 367 | } 368 | }, 369 | // G38.5: Probe away from workpiece, stop on loss of contact 370 | 'G38.5': (params) => { 371 | if (this.modal.motion !== 'G38.5') { 372 | this.setModal({ motion: 'G38.5' }); 373 | } 374 | }, 375 | // G43.1: Tool Length Offset 376 | 'G43.1': (params) => { 377 | if (this.modal.tlo !== 'G43.1') { 378 | this.setModal({ tlo: 'G43.1' }); 379 | } 380 | }, 381 | // G49: No Tool Length Offset 382 | 'G49': () => { 383 | if (this.modal.tlo !== 'G49') { 384 | this.setModal({ tlo: 'G49' }); 385 | } 386 | }, 387 | // G54..59: Coordinate System Select 388 | 'G54': () => { 389 | if (this.modal.wcs !== 'G54') { 390 | this.setModal({ wcs: 'G54' }); 391 | } 392 | }, 393 | 'G55': () => { 394 | if (this.modal.wcs !== 'G55') { 395 | this.setModal({ wcs: 'G55' }); 396 | } 397 | }, 398 | 'G56': () => { 399 | if (this.modal.wcs !== 'G56') { 400 | this.setModal({ wcs: 'G56' }); 401 | } 402 | }, 403 | 'G57': () => { 404 | if (this.modal.wcs !== 'G57') { 405 | this.setModal({ wcs: 'G57' }); 406 | } 407 | }, 408 | 'G58': () => { 409 | if (this.modal.wcs !== 'G58') { 410 | this.setModal({ wcs: 'G58' }); 411 | } 412 | }, 413 | 'G59': () => { 414 | if (this.modal.wcs !== 'G59') { 415 | this.setModal({ wcs: 'G59' }); 416 | } 417 | }, 418 | // G80: Cancel Canned Cycle 419 | 'G80': () => { 420 | if (this.modal.motion !== 'G80') { 421 | this.setModal({ motion: 'G80' }); 422 | } 423 | }, 424 | // G90: Set to Absolute Positioning 425 | // Example 426 | // G90 427 | // All coordinates from now on are absolute relative to the origin of the machine. 428 | 'G90': () => { 429 | if (this.modal.distance !== 'G90') { 430 | this.setModal({ distance: 'G90' }); 431 | } 432 | }, 433 | // G91: Set to Relative Positioning 434 | // Example 435 | // G91 436 | // All coordinates from now on are relative to the last position. 437 | 'G91': () => { 438 | if (this.modal.distance !== 'G91') { 439 | this.setModal({ distance: 'G91' }); 440 | } 441 | }, 442 | // G92: Set Position 443 | // Parameters 444 | // This command can be used without any additional parameters. 445 | // Xnnn new X axis position 446 | // Ynnn new Y axis position 447 | // Znnn new Z axis position 448 | // Example 449 | // G92 X10 450 | // Allows programming of absolute zero point, by reseting the current position to the params specified. 451 | // This would set the machine's X coordinate to 10. No physical motion will occur. 452 | // A G92 without coordinates will reset all axes to zero. 453 | 'G92': (params) => { 454 | // A G92 without coordinates will reset all axes to zero. 455 | if ((params.X === undefined) && (params.Y === undefined) && (params.Z === undefined)) { 456 | this.position.x += this.g92offset.x; 457 | this.g92offset.x = 0; 458 | this.position.y += this.g92offset.y; 459 | this.g92offset.y = 0; 460 | this.position.z += this.g92offset.z; 461 | this.g92offset.z = 0; 462 | } else { 463 | // The calls to translateX/Y/Z() below are necessary for inch/mm conversion 464 | // params.X/Y/Z must be interpreted as absolute positions, hence the "false" 465 | if (params.X !== undefined) { 466 | const xmm = this.translateX(params.X, false); 467 | this.g92offset.x += this.position.x - xmm; 468 | this.position.x = xmm; 469 | } 470 | if (params.Y !== undefined) { 471 | const ymm = this.translateY(params.Y, false); 472 | this.g92offset.y += this.position.y - ymm; 473 | this.position.y = ymm; 474 | } 475 | if (params.Z !== undefined) { 476 | const zmm = this.translateX(params.Z, false); 477 | this.g92offset.z += this.position.z - zmm; 478 | this.position.z = zmm; 479 | } 480 | } 481 | }, 482 | // G92.1: Cancel G92 offsets 483 | // Parameters 484 | // none 485 | 'G92.1': (params) => { 486 | this.position.x += this.g92offset.x; 487 | this.g92offset.x = 0; 488 | this.position.y += this.g92offset.y; 489 | this.g92offset.y = 0; 490 | this.position.z += this.g92offset.z; 491 | this.g92offset.z = 0; 492 | }, 493 | // G93: Inverse Time Mode 494 | // In inverse time feed rate mode, an F word means the move should be completed in 495 | // [one divided by the F number] minutes. 496 | // For example, if the F number is 2.0, the move should be completed in half a minute. 497 | 'G93': () => { 498 | if (this.modal.feedmode !== 'G93') { 499 | this.setModal({ feedmode: 'G93' }); 500 | } 501 | }, 502 | // G94: Units per Minute Mode 503 | // In units per minute feed rate mode, an F word on the line is interpreted to mean the 504 | // controlled point should move at a certain number of inches per minute, 505 | // millimeters per minute or degrees per minute, depending upon what length units 506 | // are being used and which axis or axes are moving. 507 | 'G94': () => { 508 | if (this.modal.feedmode !== 'G94') { 509 | this.setModal({ feedmode: 'G94' }); 510 | } 511 | }, 512 | // G94: Units per Revolution Mode 513 | // In units per rev feed rate mode, an F word on the line is interpreted to mean the 514 | // controlled point should move at a certain number of inches per spindle revolution, 515 | // millimeters per spindle revolution or degrees per spindle revolution, depending upon 516 | // what length units are being used and which axis or axes are moving. 517 | 'G95': () => { 518 | if (this.modal.feedmode !== 'G95') { 519 | this.setModal({ feedmode: 'G95' }); 520 | } 521 | }, 522 | // M0: Program Pause 523 | 'M0': () => { 524 | if (this.modal.program !== 'M0') { 525 | this.setModal({ program: 'M0' }); 526 | } 527 | }, 528 | // M1: Program Pause 529 | 'M1': () => { 530 | if (this.modal.program !== 'M1') { 531 | this.setModal({ program: 'M1' }); 532 | } 533 | }, 534 | // M2: Program End 535 | 'M2': () => { 536 | if (this.modal.program !== 'M2') { 537 | this.setModal({ program: 'M2' }); 538 | } 539 | }, 540 | // M30: Program End 541 | 'M30': () => { 542 | if (this.modal.program !== 'M30') { 543 | this.setModal({ program: 'M30' }); 544 | } 545 | }, 546 | // Spindle Control 547 | // M3: Start the spindle turning clockwise at the currently programmed speed 548 | 'M3': (params) => { 549 | if (this.modal.spindle !== 'M3') { 550 | this.setModal({ spindle: 'M3' }); 551 | } 552 | }, 553 | // M4: Start the spindle turning counterclockwise at the currently programmed speed 554 | 'M4': (params) => { 555 | if (this.modal.spindle !== 'M4') { 556 | this.setModal({ spindle: 'M4' }); 557 | } 558 | }, 559 | // M5: Stop the spindle from turning 560 | 'M5': () => { 561 | if (this.modal.spindle !== 'M5') { 562 | this.setModal({ spindle: 'M5' }); 563 | } 564 | }, 565 | // M6: Tool Change 566 | 'M6': (params) => { 567 | if (params && params.T !== undefined) { 568 | this.setModal({ tool: params.T }); 569 | } 570 | }, 571 | // Coolant Control 572 | // M7: Turn mist coolant on 573 | 'M7': () => { 574 | const coolants = this.modal.coolant.split(','); 575 | if (coolants.indexOf('M7') >= 0) { 576 | return; 577 | } 578 | 579 | this.setModal({ 580 | coolant: coolants.indexOf('M8') >= 0 ? 'M7,M8' : 'M7' 581 | }); 582 | }, 583 | // M8: Turn flood coolant on 584 | 'M8': () => { 585 | const coolants = this.modal.coolant.split(','); 586 | if (coolants.indexOf('M8') >= 0) { 587 | return; 588 | } 589 | 590 | this.setModal({ 591 | coolant: coolants.indexOf('M7') >= 0 ? 'M7,M8' : 'M8' 592 | }); 593 | }, 594 | // M9: Turn all coolant off 595 | 'M9': () => { 596 | if (this.modal.coolant !== 'M9') { 597 | this.setModal({ coolant: 'M9' }); 598 | } 599 | }, 600 | 'T': (tool) => { 601 | if (tool !== undefined) { 602 | this.setModal({ tool: tool }); 603 | } 604 | } 605 | }; 606 | 607 | // @param {object} [options] 608 | // @param {object} [options.position] 609 | // @param {object} [options.modal] 610 | // @param {function} [options.addLine] 611 | // @param {function} [options.addArcCurve] 612 | constructor(options) { 613 | const { 614 | position, 615 | modal, 616 | addLine = noop, 617 | addArcCurve = noop 618 | } = { ...options }; 619 | this.g92offset.x = 0; 620 | this.g92offset.y = 0; 621 | this.g92offset.z = 0; 622 | 623 | // Position 624 | if (position) { 625 | const { x, y, z } = { ...position }; 626 | this.setPosition(x, y, z); 627 | } 628 | 629 | // Modal 630 | const nextModal = {}; 631 | Object.keys({ ...modal }).forEach(key => { 632 | if (!Object.prototype.hasOwnProperty.call(this.modal, key)) { 633 | return; 634 | } 635 | nextModal[key] = modal[key]; 636 | }); 637 | this.setModal(nextModal); 638 | 639 | this.fn = { addLine, addArcCurve }; 640 | 641 | const toolpath = new Interpreter({ handlers: this.handlers }); 642 | toolpath.getPosition = () => ({ ...this.position }); 643 | toolpath.getModal = () => ({ ...this.modal }); 644 | toolpath.setPosition = (...pos) => { 645 | return this.setPosition(...pos); 646 | }; 647 | toolpath.setModal = (modal) => { 648 | return this.setModal(modal); 649 | }; 650 | 651 | return toolpath; 652 | } 653 | setModal(modal) { 654 | this.modal = { 655 | ...this.modal, 656 | ...modal 657 | }; 658 | return this.modal; 659 | } 660 | isMetricUnits() { // mm 661 | return this.modal.units === 'G21'; 662 | } 663 | isImperialUnits() { // inches 664 | return this.modal.units === 'G20'; 665 | } 666 | isAbsoluteDistance() { 667 | return this.modal.distance === 'G90'; 668 | } 669 | isRelativeDistance() { 670 | return this.modal.distance === 'G91'; 671 | } 672 | isXYPlane() { 673 | return this.modal.plane === 'G17'; 674 | } 675 | isZXPlane() { 676 | return this.modal.plane === 'G18'; 677 | } 678 | isYZPlane() { 679 | return this.modal.plane === 'G19'; 680 | } 681 | setPosition(...pos) { 682 | if (typeof pos[0] === 'object') { 683 | const { x, y, z } = { ...pos[0] }; 684 | this.position.x = (typeof x === 'number') ? x : this.position.x; 685 | this.position.y = (typeof y === 'number') ? y : this.position.y; 686 | this.position.z = (typeof z === 'number') ? z : this.position.z; 687 | } else { 688 | const [x, y, z] = pos; 689 | this.position.x = (typeof x === 'number') ? x : this.position.x; 690 | this.position.y = (typeof y === 'number') ? y : this.position.y; 691 | this.position.z = (typeof z === 'number') ? z : this.position.z; 692 | } 693 | } 694 | translateX(x, relative) { 695 | if (x !== undefined) { 696 | x = this.isImperialUnits() ? in2mm(x) : x; 697 | } 698 | if (relative === undefined) { 699 | relative = this.isRelativeDistance(); 700 | } 701 | return translatePosition(this.position.x, x, !!relative); 702 | } 703 | translateY(y, relative) { 704 | if (y !== undefined) { 705 | y = this.isImperialUnits() ? in2mm(y) : y; 706 | } 707 | if (relative === undefined) { 708 | relative = this.isRelativeDistance(); 709 | } 710 | return translatePosition(this.position.y, y, !!relative); 711 | } 712 | translateZ(z, relative) { 713 | if (z !== undefined) { 714 | z = this.isImperialUnits() ? in2mm(z) : z; 715 | } 716 | if (relative === undefined) { 717 | relative = this.isRelativeDistance(); 718 | } 719 | return translatePosition(this.position.z, z, !!relative); 720 | } 721 | translateI(i) { 722 | return this.translateX(i, true); 723 | } 724 | translateJ(j) { 725 | return this.translateY(j, true); 726 | } 727 | translateK(k) { 728 | return this.translateZ(k, true); 729 | } 730 | translateR(r) { 731 | r = Number(r); 732 | if (Number.isNaN(r)) { 733 | return 0; 734 | } 735 | return this.isImperialUnits() ? in2mm(r) : r; 736 | } 737 | } 738 | 739 | export default Toolpath; 740 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/arc-no-plane.nc: -------------------------------------------------------------------------------- 1 | G1 X0Y20 F200 2 | G2 X20Y0 I0 J-20.0 3 | G1 X0Y20 F200 4 | G3 X20Y0 I0 J-20.0 5 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/arc-r.nc: -------------------------------------------------------------------------------- 1 | G0 X0Y0 2 | G02 3 | X0Y20 4 | X10Y0 R20 5 | G0 X0Y0 6 | G03 7 | X0Y20 8 | X10Y0 R20 9 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/arc-xy-plane.nc: -------------------------------------------------------------------------------- 1 | G17 2 | G1 X0Y20 F200 3 | G2 X20Y0 I0 J-20.0 4 | G1 X0Y20 F200 5 | G3 X20Y0 I0 J-20.0 6 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/arc-yz-plane.nc: -------------------------------------------------------------------------------- 1 | G19 2 | G1 Y0Z20 F200 3 | G2 Y20Z0 J0 K-20.0 4 | G1 Y0Z20 F200 5 | G3 Y20Z0 J0 K-20.0 6 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/arc-zx-plane.nc: -------------------------------------------------------------------------------- 1 | G18 2 | G1 X0Z20 F200 3 | G2 X20Z0 I0 K-20.0 4 | G1 X0Z20 F200 5 | G3 X20Z0 I0 K-20.0 6 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/circle.nc: -------------------------------------------------------------------------------- 1 | G0 X-5 Y0 Z0 F200 2 | G2 X0 Y5 I5 J0 F200 3 | G02 X5 Y0 I0 J-5 4 | G02 X0 Y-5 I-5 J0 5 | G02 X-5 Y0 I0 J5 6 | G01 Z1 F500 7 | G00 X0 Y0 Z5 -------------------------------------------------------------------------------- /src/__tests__/fixtures/coordinate.nc: -------------------------------------------------------------------------------- 1 | G54 2 | G55 3 | G56 4 | G57 5 | G58 6 | G59 7 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/dwell.nc: -------------------------------------------------------------------------------- 1 | G4 P2000 2 | G4 S2 3 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/feedrate.nc: -------------------------------------------------------------------------------- 1 | G93 2 | G94 3 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/g92offset.nc: -------------------------------------------------------------------------------- 1 | (Test for G92 temporary offsets) 2 | (The motions should be performed in absolute coordinates) 3 | 4 | (Start with no offsets - rel=abs) 5 | 6 | (Go from 0,0,0 to 1,2,3 with rel=abs) 7 | g0 x1 y2 z3 8 | 9 | (Offset the current point so abs 1,2,3 becomes rel 0,0,0) 10 | g92 x0 y0 z0 11 | 12 | (Go from rel 0,0,0 = abs 1,2,3 to rel 1,2,3 = abs 2,4,6) 13 | g0 x1 y2 z3 14 | 15 | (Cancel the offsets so the current point rel 1,2,3 = abs 2,4,6 becomes rel 2,4,6) 16 | g92.1 17 | 18 | (Go from 2,4,6 to 0,0,0 with rel=abs) 19 | g0 x0 y0 z0 20 | 21 | (Change just one offset so rel=0,0,1) 22 | g92 z1 23 | 24 | (Go from rel 0,0,1 = abs 0,0,0 to rel 0,0,0 = abs 0,0,-1) 25 | g0 z0 26 | 27 | (Cancel the offsets using the alternate syntax - now rel is 0,0,-1) 28 | g92 29 | 30 | (Go from 0,0,-1 to 0,0,0 with rel=abs) 31 | g0 z0 32 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/helical-thread-milling.nc: -------------------------------------------------------------------------------- 1 | G01 G91 Z-0.6533 F100. 2 | G01 G42 D08 X0.0235 Y-0.0939 F10. 3 | G03 X0.0939 Y0.0939 Z0.0179 R0.0939 4 | G03 X-0.1179 Y0.1179 Z0.0179 R0.1179 5 | G03 X-0.1185 Y-0.1185 Z0.0179 R0.1185 6 | G03 X0.1191 Y-0.1191 Z0.0179 R0.1191 F16. 7 | G03 X0.1196 Y0.1196 Z0.0179 R0.1196 8 | G03 X-0.1202 Y0.1202 Z0.0179 R0.1202 F26. 9 | G03 X-0.1207 Y-0.1207 Z0.0179 R0.1207 10 | G03 X0.1213 Y-0.1213 Z0.0179 R0.1213 11 | G03 X0.1218 Y0.1218 Z0.0179 R0.1218 12 | G03 X-0.0975 Y0.0975 Z0.0179 R0.0975 -------------------------------------------------------------------------------- /src/__tests__/fixtures/linear.nc: -------------------------------------------------------------------------------- 1 | G0 G90 G94 G17 2 | G20 3 | G53 Z0. 4 | M5 5 | M9 6 | T4 7 | S16500 M3 8 | G55 9 | M9 10 | G0 X1.1185 Y0.5562 11 | Z0.0394 12 | M6 T2 13 | Z-0.0231 14 | G1 Z-0.0365 F19.7 15 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/motion.nc: -------------------------------------------------------------------------------- 1 | G92 2 | G0 X0Y0Z0 3 | G1 X0Y0Z0 F200 4 | G2 X0Y0 R0 5 | G3 X0Y0 R0 6 | G38.2 7 | G38.3 8 | G38.4 9 | G38.5 10 | G80 11 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/one-inch-circle.nc: -------------------------------------------------------------------------------- 1 | N1 G17 G20 G90 G94 G54 2 | N2 G0 Z0.25 3 | N3 X-0.5 Y0. 4 | N4 Z0.1 5 | N5 G01 Z0. F5. 6 | N6 G02 X0. Y0.5 I0.5 J0. F2.5 7 | N7 X0.5 Y0. I0. J-0.5 8 | N8 X0. Y-0.5 I-0.5 J0. 9 | N9 X-0.5 Y0. I0. J0.5 10 | N10 G01 Z0.1 F5. 11 | N11 G00 X0. Y0. Z0.25 12 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/t2laser.nc: -------------------------------------------------------------------------------- 1 | ( Generated by T2Laser ) 2 | ( Image2Gcode for Grbl ) 3 | ( Start Point: Center ) 4 | ( Frame Mode : Abs. ) 5 | ( X Maximum : 84.4 ) 6 | ( Y Maximum : 99.9 ) 7 | ( Laser Max : 1000 ) 8 | ( Feed Rate : 750 ) 9 | ( Resolution : 0.1 ) 10 | ( Image Name : text4272.png ) 11 | M6 T1 12 | T1 13 | G21 14 | G90 15 | F750 16 | M05 17 | G00X0Y0F750 18 | G92X42.2Y49.95 19 | G90 20 | G00 X0 Y0 21 | T2 22 | G01 23 | M03 S0 24 | X5.2 Y0.2 M03 S0 25 | X5.3 Y0.1 M03 S1000 26 | X5.4 Y0 M03 S0 27 | X5.5 Y0 M03 S0 28 | X5.4 Y0.1 M03 S1000 29 | X5.2 Y0.3 M03 S1000 30 | X5.1 Y0.4 M03 S0 31 | X5.2 Y0.4 M03 S0 32 | X5.3 Y0.3 M03 S1000 33 | X5.5 Y0.1 M03 S1000 34 | X5.6 Y0 M03 S0 35 | X5.7 Y0 M03 S0 36 | X5.6 Y0.1 M03 S1000 37 | X5.2 Y0.5 M03 S1000 38 | X5.1 Y0.6 M03 S0 39 | X5.2 Y0.6 M03 S0 40 | X5.3 Y0.5 M03 S1000 41 | X5.7 Y0.1 M03 S1000 42 | X5.8 Y0 M03 S0 43 | X5.9 Y0 M03 S0 44 | X5.8 Y0.1 M03 S1000 45 | X5.2 Y0.7 M03 S1000 46 | X5.1 Y0.8 M03 S0 47 | X5.2 Y0.8 M03 S0 48 | X5.3 Y0.7 M03 S1000 49 | X5.9 Y0.1 M03 S1000 50 | X6 Y0 M03 S0 51 | X6.1 Y0 M03 S0 52 | X6 Y0.1 M03 S1000 53 | X5.2 Y0.9 M03 S1000 54 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/units.nc: -------------------------------------------------------------------------------- 1 | G20 2 | G21 3 | -------------------------------------------------------------------------------- /src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import Toolpath from '..'; 5 | 6 | describe('Pass a null value as the first argument', () => { 7 | const toolpath = new Toolpath(); 8 | it('should call loadFromString\'s callback.', (done) => { 9 | toolpath.loadFromString(null, (err, results) => { 10 | expect(err).toBeNull(); 11 | expect(results).toHaveLength(0); 12 | done(); 13 | }); 14 | }); 15 | it('should call loadFromFile\'s callback.', (done) => { 16 | toolpath.loadFromFile(null, (err, results) => { 17 | expect(err).not.toBeNull(); 18 | expect(results).toBeUndefined(); 19 | done(); 20 | }); 21 | }); 22 | it('should call loadFromStream\'s callback.', (done) => { 23 | toolpath.loadFromStream(null, (err, results) => { 24 | expect(err).not.toBeNull(); 25 | expect(results).toBeUndefined(); 26 | done(); 27 | }); 28 | }); 29 | }); 30 | 31 | describe('Event listeners', () => { 32 | it('should call event listeners when loading G-code from file.', (done) => { 33 | const filepath = path.resolve(__dirname, 'fixtures/circle.nc'); 34 | 35 | new Toolpath() 36 | .loadFromFile(filepath, (err, results) => { 37 | expect(err).toBeNull(); 38 | expect(results).toHaveLength(7); 39 | done(); 40 | }) 41 | .on('data', (data) => { 42 | expect(typeof data).toBe('object'); 43 | }) 44 | .on('end', (results) => { 45 | expect(results).toHaveLength(7); 46 | }); 47 | }); 48 | 49 | it('should call event listeners when loading G-code from stream.', (done) => { 50 | const filepath = path.resolve(__dirname, 'fixtures/circle.nc'); 51 | const stream = fs.createReadStream(filepath); 52 | 53 | new Toolpath() 54 | .loadFromStream(stream, (err, results) => { 55 | expect(err).toBeNull(); 56 | expect(results).toHaveLength(7); 57 | done(); 58 | }) 59 | .on('data', (data) => { 60 | expect(typeof data).toBe('object'); 61 | }) 62 | .on('end', (results) => { 63 | expect(results).toHaveLength(7); 64 | }); 65 | }); 66 | 67 | it('should call event listeners when loading G-code from string.', (done) => { 68 | const filepath = path.resolve(__dirname, 'fixtures/circle.nc'); 69 | const string = fs.readFileSync(filepath, 'utf8'); 70 | 71 | new Toolpath() 72 | .loadFromString(string, (err, results) => { 73 | expect(err).toBeNull(); 74 | expect(results).toHaveLength(7); 75 | done(); 76 | }) 77 | .on('data', (data) => { 78 | expect(typeof data).toBe('object'); 79 | }) 80 | .on('end', (results) => { 81 | expect(results).toHaveLength(7); 82 | }); 83 | }); 84 | }); 85 | 86 | describe('position', () => { 87 | it('should match the specified position.', (done) => { 88 | const toolpath = new Toolpath({ 89 | position: { x: 200, y: 100 } 90 | }); 91 | expect(toolpath.getPosition()).toEqual({ x: 200, y: 100, z: 0 }); 92 | toolpath.setPosition({ y: 200, z: 10 }); 93 | expect(toolpath.getPosition()).toEqual({ x: 200, y: 200, z: 10 }); 94 | toolpath.setPosition(10, 10); 95 | expect(toolpath.getPosition()).toEqual({ x: 10, y: 10, z: 10 }); 96 | toolpath.setPosition(0, 0, 0); 97 | expect(toolpath.getPosition()).toEqual({ x: 0, y: 0, z: 0 }); 98 | done(); 99 | }); 100 | }); 101 | 102 | describe('modal', () => { 103 | it('should match the specified modal state.', (done) => { 104 | const toolpath = new Toolpath({ 105 | modal: { 106 | tool: 1 107 | } 108 | }); 109 | const expectedModal = { 110 | // Moton Mode 111 | // G0, G1, G2, G3, G38.2, G38.3, G38.4, G38.5, G80 112 | motion: 'G0', 113 | 114 | // Coordinate System Select 115 | // G54, G55, G56, G57, G58, G59 116 | wcs: 'G54', 117 | 118 | // Plane Select 119 | // G17: XY-plane, G18: ZX-plane, G19: YZ-plane 120 | plane: 'G17', 121 | 122 | // Units Mode 123 | // G20: Inches, G21: Millimeters 124 | units: 'G21', 125 | 126 | // Distance Mode 127 | // G90: Absolute, G91: Relative 128 | distance: 'G90', 129 | 130 | // Arc IJK distance mode 131 | arc: 'G91.1', 132 | 133 | // Feed Rate Mode 134 | // G93: Inverse time mode, G94: Units per minute mode, G95: Units per rev mode 135 | feedrate: 'G94', 136 | 137 | // Cutter Radius Compensation 138 | cutter: 'G40', 139 | 140 | // Tool Length Offset 141 | // G43.1, G49 142 | tlo: 'G49', 143 | 144 | // Program Mode 145 | // M0, M1, M2, M30 146 | program: 'M0', 147 | 148 | // Spingle State 149 | // M3, M4, M5 150 | spindle: 'M5', 151 | 152 | // Coolant State 153 | // M7, M8, M9 154 | coolant: 'M9', // 'M7', 'M8', 'M7,M8', or 'M9' 155 | 156 | // Tool Select 157 | tool: 1, 158 | }; 159 | expect(toolpath.getModal()).toEqual(expectedModal); 160 | 161 | toolpath.setModal({ tool: 2 }); 162 | expect(toolpath.getModal().tool).toBe(2); 163 | 164 | done(); 165 | }); 166 | }); 167 | 168 | describe('Linear Move: G0/G1', () => { 169 | it('should generate tool paths for linear movement.', (done) => { 170 | const expectedMotions = [ 171 | { 172 | "motion": "G0", 173 | "v1": { 174 | "x": 0, 175 | "y": 0, 176 | "z": 0 177 | }, 178 | "v2": { 179 | "x": 0, 180 | "y": 0, 181 | "z": 0 182 | } 183 | }, 184 | { 185 | "motion": "G0", 186 | "v1": { 187 | "x": 0, 188 | "y": 0, 189 | "z": 0 190 | }, 191 | "v2": { 192 | "x": 28.4099, 193 | "y": 14.12748, 194 | "z": 0 195 | } 196 | }, 197 | { 198 | "motion": "G0", 199 | "v1": { 200 | "x": 28.4099, 201 | "y": 14.12748, 202 | "z": 0 203 | }, 204 | "v2": { 205 | "x": 28.4099, 206 | "y": 14.12748, 207 | "z": 1.0007599999999999 208 | } 209 | }, 210 | { 211 | "motion": "G0", 212 | "v1": { 213 | "x": 28.4099, 214 | "y": 14.12748, 215 | "z": 1.0007599999999999 216 | }, 217 | "v2": { 218 | "x": 28.4099, 219 | "y": 14.12748, 220 | "z": -0.5867399999999999 221 | } 222 | }, 223 | { 224 | "motion": "G1", 225 | "v1": { 226 | "x": 28.4099, 227 | "y": 14.12748, 228 | "z": -0.5867399999999999 229 | }, 230 | "v2": { 231 | "x": 28.4099, 232 | "y": 14.12748, 233 | "z": -0.9270999999999999 234 | } 235 | } 236 | ]; 237 | const motions = []; 238 | const toolpath = new Toolpath({ 239 | modal: {}, 240 | addLine: (modal, v1, v2) => { 241 | motions.push({ 242 | motion: modal.motion, 243 | v1: v1, 244 | v2: v2 245 | }); 246 | } 247 | }); 248 | 249 | const filepath = path.resolve(__dirname, 'fixtures/linear.nc'); 250 | toolpath.loadFromFileSync(filepath); 251 | expect(motions).toEqual(expectedMotions); 252 | done(); 253 | }); 254 | 255 | }); 256 | 257 | describe('Arc Curve: G2/G3', () => { 258 | it('should generate tool paths for simple radius.', (done) => { 259 | const motions = []; 260 | const toolpath = new Toolpath({ 261 | modal: {}, 262 | addLine: (modal, v1, v2) => { 263 | motions.push({ 264 | motion: modal.motion, 265 | v1: v1, 266 | v2: v2 267 | }); 268 | }, 269 | addArcCurve: (modal, v1, v2, v0) => { 270 | motions.push({ 271 | motion: modal.motion, 272 | v1: v1, 273 | v2: v2, 274 | v0: v0 275 | }); 276 | } 277 | }); 278 | 279 | const filepath = path.resolve(__dirname, 'fixtures/arc-r.nc'); 280 | toolpath.loadFromFileSync(filepath); 281 | done(); 282 | }); 283 | 284 | it('should generate tool paths for helical thread milling.', (done) => { 285 | const motions = []; 286 | const toolpath = new Toolpath({ 287 | modal: {}, 288 | addLine: (modal, v1, v2) => { 289 | motions.push({ 290 | motion: modal.motion, 291 | v1: v1, 292 | v2: v2 293 | }); 294 | }, 295 | addArcCurve: (modal, v1, v2, v0) => { 296 | motions.push({ 297 | motion: modal.motion, 298 | v1: v1, 299 | v2: v2, 300 | v0: v0 301 | }); 302 | } 303 | }); 304 | 305 | const filepath = path.resolve(__dirname, 'fixtures/helical-thread-milling.nc'); 306 | toolpath.loadFromFileSync(filepath); 307 | done(); 308 | }); 309 | 310 | it('should generate for one inch circle.', (done) => { 311 | const motions = []; 312 | const toolpath = new Toolpath({ 313 | modal: {}, 314 | addLine: (modal, v1, v2) => { 315 | motions.push({ 316 | motion: modal.motion, 317 | v1: v1, 318 | v2: v2 319 | }); 320 | }, 321 | addArcCurve: (modal, v1, v2, v0) => { 322 | motions.push({ 323 | motion: modal.motion, 324 | v1: v1, 325 | v2: v2, 326 | v0: v0 327 | }); 328 | } 329 | }); 330 | 331 | const filepath = path.resolve(__dirname, 'fixtures/one-inch-circle.nc'); 332 | toolpath.loadFromFileSync(filepath); 333 | done(); 334 | }); 335 | 336 | }); 337 | 338 | describe('Dwell: G4', () => { 339 | it('should not generate tool paths.', (done) => { 340 | const motions = []; 341 | const toolpath = new Toolpath({ 342 | addLine: (modal, v1, v2) => { 343 | motions.push({ 344 | motion: modal.motion, 345 | v1: v1, 346 | v2: v2 347 | }); 348 | }, 349 | addArcCurve: (modal, v1, v2, v0) => { 350 | motions.push({ 351 | motion: modal.motion, 352 | v1: v1, 353 | v2: v2, 354 | v0: v0 355 | }); 356 | } 357 | }); 358 | const filepath = path.resolve(__dirname, 'fixtures/dwell.nc'); 359 | toolpath.loadFromFileSync(filepath); 360 | expect(motions).toHaveLength(0); 361 | done(); 362 | }); 363 | }); 364 | 365 | describe('Motion: G0/G1/G2/G3/G38.2/G38.3/G38.4/G38.5/G80', () => { 366 | it('should generate tool paths for an empty object.', (done) => { 367 | const expectedmotions = [ 368 | { 369 | motion: 'G0', 370 | v1: { x: 0, y: 0, z: 0 }, 371 | v2: { x: 0, y: 0, z: 0 } 372 | }, 373 | { 374 | motion: 'G1', 375 | v1: { x: 0, y: 0, z: 0 }, 376 | v2: { x: 0, y: 0, z: 0 } 377 | }, 378 | { 379 | motion: 'G2', 380 | v1: { x: 0, y: 0, z: 0 }, 381 | v2: { x: 0, y: 0, z: 0 }, 382 | v0: { x: 0, y: 0, z: 0 } 383 | }, 384 | { 385 | motion: 'G3', 386 | v1: { x: 0, y: 0, z: 0 }, 387 | v2: { x: 0, y: 0, z: 0 }, 388 | v0: { x: 0, y: 0, z: 0 } 389 | } 390 | ]; 391 | const motions = []; 392 | const toolpath = new Toolpath({ 393 | addLine: (modal, v1, v2) => { 394 | motions.push({ 395 | motion: modal.motion, 396 | v1: v1, 397 | v2: v2 398 | }); 399 | }, 400 | addArcCurve: (modal, v1, v2, v0) => { 401 | motions.push({ 402 | motion: modal.motion, 403 | v1: v1, 404 | v2: v2, 405 | v0: v0 406 | }); 407 | } 408 | }); 409 | const filepath = path.resolve(__dirname, 'fixtures/motion.nc'); 410 | toolpath.loadFromFileSync(filepath); 411 | expect(motions).toEqual(expectedmotions); 412 | done(); 413 | }); 414 | }); 415 | 416 | describe('Plane: G17/G18/G19', () => { 417 | it('should not generate tool paths with wrong plane mode.', (done) => { 418 | const motions = []; 419 | const toolpath = new Toolpath({ 420 | modal: { 421 | plane: 'xx' // The plane is invalid 422 | }, 423 | addArcCurve: (modal, v1, v2, v0) => { 424 | motions.push({ 425 | motion: modal.motion, 426 | v1: v1, 427 | v2: v2, 428 | v0: v0 429 | }); 430 | } 431 | }); 432 | const filepath = path.resolve(__dirname, 'fixtures/arc-no-plane.nc'); 433 | toolpath.loadFromFileSync(filepath); 434 | expect(motions).toHaveLength(0); 435 | done(); 436 | }); 437 | 438 | it('should generate correct tool paths in the XY-plane (G17)', (done) => { 439 | const expectedmotions = [ 440 | { 441 | motion: 'G1', 442 | v1: { x: 0, y: 0, z: 0 }, 443 | v2: { x: 0, y: 20, z: 0 } 444 | }, 445 | { 446 | motion: 'G2', 447 | v1: { x: 0, y: 20, z: 0 }, 448 | v2: { x: 20, y: 0, z: 0 }, 449 | v0: { x: 0, y: 0, z: 0 } 450 | }, 451 | { 452 | motion: 'G1', 453 | v1: { x: 20, y: 0, z: 0 }, 454 | v2: { x: 0, y: 20, z: 0 } 455 | }, 456 | { 457 | motion: 'G3', 458 | v1: { x: 0, y: 20, z: 0 }, 459 | v2: { x: 20, y: 0, z: 0 }, 460 | v0: { x: 0, y: 0, z: 0 } 461 | } 462 | ]; 463 | const motions = []; 464 | const toolpath = new Toolpath({ 465 | modal: {}, 466 | addLine: (modal, v1, v2) => { 467 | motions.push({ 468 | motion: modal.motion, 469 | v1: v1, 470 | v2: v2 471 | }); 472 | }, 473 | addArcCurve: (modal, v1, v2, v0) => { 474 | motions.push({ 475 | motion: modal.motion, 476 | v1: v1, 477 | v2: v2, 478 | v0: v0 479 | }); 480 | } 481 | }); 482 | const filepath = path.resolve(__dirname, 'fixtures/arc-xy-plane.nc'); 483 | toolpath.loadFromFileSync(filepath); 484 | expect(motions).toEqual(expectedmotions); 485 | done(); 486 | }); 487 | 488 | it('should generate correct tool paths in the ZX-plane (G18)', (done) => { 489 | const expectedmotions = [ 490 | { 491 | motion: 'G1', 492 | v1: { x: 0, y: 0, z: 0 }, 493 | v2: { x: 0, y: 0, z: 20 } 494 | }, 495 | { 496 | motion: 'G2', 497 | v1: { x: 20, y: 0, z: 0 }, 498 | v2: { x: 0, y: 20, z: 0 }, 499 | v0: { x: 0, y: 0, z: 0 } 500 | }, 501 | { 502 | motion: 'G1', 503 | v1: { x: 20, y: 0, z: 0 }, 504 | v2: { x: 0, y: 0, z: 20 } 505 | }, 506 | { 507 | motion: 'G3', 508 | v1: { x: 20, y: 0, z: 0 }, 509 | v2: { x: 0, y: 20, z: 0 }, 510 | v0: { x: 0, y: 0, z: 0 } 511 | } 512 | ]; 513 | const motions = []; 514 | const toolpath = new Toolpath({ 515 | modal: {}, 516 | addLine: (modal, v1, v2) => { 517 | motions.push({ 518 | motion: modal.motion, 519 | v1: v1, 520 | v2: v2 521 | }); 522 | }, 523 | addArcCurve: (modal, v1, v2, v0) => { 524 | motions.push({ 525 | motion: modal.motion, 526 | v1: v1, 527 | v2: v2, 528 | v0: v0 529 | }); 530 | } 531 | }); 532 | const filepath = path.resolve(__dirname, 'fixtures/arc-zx-plane.nc'); 533 | toolpath.loadFromFileSync(filepath); 534 | expect(motions).toEqual(expectedmotions); 535 | done(); 536 | }); 537 | 538 | it('should generate correct tool paths in the YZ-plane (G19)', (done) => { 539 | const expectedmotions = [ 540 | { 541 | motion: 'G1', 542 | v1: { x: 0, y: 0, z: 0 }, 543 | v2: { x: 0, y: 0, z: 20 } 544 | }, 545 | { 546 | motion: 'G2', 547 | v1: { x: 0, y: 20, z: 0 }, 548 | v2: { x: 20, y: 0, z: 0 }, 549 | v0: { x: 0, y: 0, z: 0 } 550 | }, 551 | { 552 | motion: 'G1', 553 | v1: { x: 0, y: 20, z: 0 }, 554 | v2: { x: 0, y: 0, z: 20 } 555 | }, 556 | { 557 | motion: 'G3', 558 | v1: { x: 0, y: 20, z: 0 }, 559 | v2: { x: 20, y: 0, z: 0 }, 560 | v0: { x: 0, y: 0, z: 0 } 561 | } 562 | ]; 563 | const motions = []; 564 | const toolpath = new Toolpath({ 565 | modal: {}, 566 | addLine: (modal, v1, v2) => { 567 | motions.push({ 568 | motion: modal.motion, 569 | v1: v1, 570 | v2: v2 571 | }); 572 | }, 573 | addArcCurve: (modal, v1, v2, v0) => { 574 | motions.push({ 575 | motion: modal.motion, 576 | v1: v1, 577 | v2: v2, 578 | v0: v0 579 | }); 580 | } 581 | }); 582 | const filepath = path.resolve(__dirname, 'fixtures/arc-yz-plane.nc'); 583 | toolpath.loadFromFileSync(filepath); 584 | expect(motions).toEqual(expectedmotions); 585 | done(); 586 | }); 587 | 588 | }); 589 | 590 | describe('Units: G20/G21', () => { 591 | it('should not generate tool paths.', (done) => { 592 | const motions = []; 593 | const toolpath = new Toolpath({ 594 | addLine: (modal, v1, v2) => { 595 | motions.push({ 596 | motion: modal.motion, 597 | v1: v1, 598 | v2: v2 599 | }); 600 | }, 601 | addArcCurve: (modal, v1, v2, v0) => { 602 | motions.push({ 603 | motion: modal.motion, 604 | v1: v1, 605 | v2: v2, 606 | v0: v0 607 | }); 608 | } 609 | }); 610 | const filepath = path.resolve(__dirname, 'fixtures/units.nc'); 611 | toolpath.loadFromFileSync(filepath); 612 | expect(motions).toHaveLength(0); 613 | done(); 614 | }); 615 | }); 616 | 617 | describe('Coordinate: G54/G55/G56/G57/G58/G59', () => { 618 | it('should not generate tool paths.', (done) => { 619 | const motions = []; 620 | const toolpath = new Toolpath({ 621 | addLine: (modal, v1, v2) => { 622 | motions.push({ 623 | motion: modal.motion, 624 | v1: v1, 625 | v2: v2 626 | }); 627 | }, 628 | addArcCurve: (modal, v1, v2, v0) => { 629 | motions.push({ 630 | motion: modal.motion, 631 | v1: v1, 632 | v2: v2, 633 | v0: v0 634 | }); 635 | } 636 | }); 637 | const filepath = path.resolve(__dirname, 'fixtures/coordinate.nc'); 638 | toolpath.loadFromFileSync(filepath); 639 | expect(motions).toHaveLength(0); 640 | done(); 641 | }); 642 | }); 643 | 644 | describe('Feed Rate: G93/G94', () => { 645 | it('should not generate tool paths.', (done) => { 646 | const motions = []; 647 | const toolpath = new Toolpath({ 648 | addLine: (modal, v1, v2) => { 649 | motions.push({ 650 | motion: modal.motion, 651 | v1: v1, 652 | v2: v2 653 | }); 654 | }, 655 | addArcCurve: (modal, v1, v2, v0) => { 656 | motions.push({ 657 | motion: modal.motion, 658 | v1: v1, 659 | v2: v2, 660 | v0: v0 661 | }); 662 | } 663 | }); 664 | const filepath = path.resolve(__dirname, 'fixtures/feedrate.nc'); 665 | toolpath.loadFromFileSync(filepath); 666 | expect(motions).toHaveLength(0); 667 | done(); 668 | }); 669 | }); 670 | 671 | describe('Tool Change & Tool Select: M6/T', () => { 672 | it('should change the modal state: t2laser.nc', (done) => { 673 | const motions = []; 674 | const toolpath = new Toolpath({ 675 | addLine: (modal, v1, v2) => { 676 | motions.push({ 677 | motion: modal.motion, 678 | tool: modal.tool 679 | }); 680 | }, 681 | addArcCurve: (modal, v1, v2, v0) => { 682 | motions.push({ 683 | motion: modal.motion, 684 | tool: modal.tool 685 | }); 686 | } 687 | }); 688 | const expectedMotions = [ 689 | { "motion": "G0", tool: 1 }, 690 | { "motion": "G0", tool: 1 }, 691 | { "motion": "G1", tool: 2 }, 692 | { "motion": "G1", tool: 2 }, 693 | { "motion": "G1", tool: 2 }, 694 | { "motion": "G1", tool: 2 }, 695 | { "motion": "G1", tool: 2 }, 696 | { "motion": "G1", tool: 2 }, 697 | { "motion": "G1", tool: 2 }, 698 | { "motion": "G1", tool: 2 }, 699 | { "motion": "G1", tool: 2 }, 700 | { "motion": "G1", tool: 2 }, 701 | { "motion": "G1", tool: 2 }, 702 | { "motion": "G1", tool: 2 }, 703 | { "motion": "G1", tool: 2 }, 704 | { "motion": "G1", tool: 2 }, 705 | { "motion": "G1", tool: 2 }, 706 | { "motion": "G1", tool: 2 }, 707 | { "motion": "G1", tool: 2 }, 708 | { "motion": "G1", tool: 2 }, 709 | { "motion": "G1", tool: 2 }, 710 | { "motion": "G1", tool: 2 }, 711 | { "motion": "G1", tool: 2 }, 712 | { "motion": "G1", tool: 2 }, 713 | { "motion": "G1", tool: 2 }, 714 | { "motion": "G1", tool: 2 }, 715 | { "motion": "G1", tool: 2 }, 716 | { "motion": "G1", tool: 2 }, 717 | { "motion": "G1", tool: 2 }, 718 | { "motion": "G1", tool: 2 }, 719 | { "motion": "G1", tool: 2 }, 720 | { "motion": "G1", tool: 2 }, 721 | { "motion": "G1", tool: 2 } 722 | ]; 723 | 724 | const filepath = path.resolve(__dirname, 'fixtures/t2laser.nc'); 725 | toolpath.loadFromFileSync(filepath); 726 | expect(motions).toEqual(expectedMotions); 727 | done(); 728 | }); 729 | 730 | it('should change the modal state: linear.nc', (done) => { 731 | const expectedMotions = [ 732 | { 733 | "motion": "G0", 734 | "tool": 0 735 | }, 736 | { 737 | "motion": "G0", 738 | "tool": 4 739 | }, 740 | { 741 | "motion": "G0", 742 | "tool": 4 743 | }, 744 | { 745 | "motion": "G0", 746 | "tool": 2 747 | }, 748 | { 749 | "motion": "G1", 750 | "tool": 2 751 | } 752 | ]; 753 | const motions = []; 754 | const toolpath = new Toolpath({ 755 | addLine: (modal, v1, v2) => { 756 | motions.push({ 757 | motion: modal.motion, 758 | tool: modal.tool 759 | }); 760 | }, 761 | addArcCurve: (modal, v1, v2, v0) => { 762 | motions.push({ 763 | motion: modal.motion, 764 | tool: modal.tool 765 | }); 766 | } 767 | }); 768 | 769 | const filepath = path.resolve(__dirname, 'fixtures/linear.nc'); 770 | toolpath.loadFromFileSync(filepath); 771 | expect(motions).toEqual(expectedMotions); 772 | done(); 773 | }); 774 | }); 775 | 776 | describe('Temporary Offset: G92', () => { 777 | it('should generate tool paths with correct endpoints.', (done) => { 778 | const expectedMotions = [ 779 | { 780 | "motion": "G0", 781 | "v1": { 782 | "x": 0, 783 | "y": 0, 784 | "z": 0 785 | }, 786 | "v2": { 787 | "x": 1, 788 | "y": 2, 789 | "z": 3 790 | } 791 | }, 792 | { 793 | "motion": "G0", 794 | "v1": { 795 | "x": 1, 796 | "y": 2, 797 | "z": 3 798 | }, 799 | "v2": { 800 | "x": 2, 801 | "y": 4, 802 | "z": 6 803 | } 804 | }, 805 | { 806 | "motion": "G0", 807 | "v1": { 808 | "x": 2, 809 | "y": 4, 810 | "z": 6 811 | }, 812 | "v2": { 813 | "x": 0, 814 | "y": 0, 815 | "z": 0 816 | } 817 | }, 818 | { 819 | "motion": "G0", 820 | "v1": { 821 | "x": 0, 822 | "y": 0, 823 | "z": 0 824 | }, 825 | "v2": { 826 | "x": 0, 827 | "y": 0, 828 | "z": -1 829 | } 830 | }, 831 | { 832 | "motion": "G0", 833 | "v1": { 834 | "x": 0, 835 | "y": 0, 836 | "z": -1 837 | }, 838 | "v2": { 839 | "x": 0, 840 | "y": 0, 841 | "z": 0 842 | } 843 | }, 844 | ]; 845 | const motions = []; 846 | const toolpath = new Toolpath({ 847 | modal: {}, 848 | addLine: (modal, v1, v2) => { 849 | motions.push({ 850 | motion: modal.motion, 851 | v1: v1, 852 | v2: v2 853 | }); 854 | } 855 | }); 856 | 857 | const filepath = path.resolve(__dirname, 'fixtures/g92offset.nc'); 858 | toolpath.loadFromFileSync(filepath); 859 | expect(motions).toEqual(expectedMotions); 860 | done(); 861 | }); 862 | 863 | }); 864 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Toolpath from './Toolpath'; 2 | 3 | export default Toolpath; 4 | --------------------------------------------------------------------------------