├── .browserslistrc ├── .eslintrc.js ├── .github └── workflows │ └── cid.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── babel.config.js ├── cypress.json ├── nyc.config.js ├── package-lock.json ├── package.json ├── src ├── assets │ ├── gcode-parser.js │ ├── scene.js │ └── utils.js ├── demo │ ├── App.vue │ ├── Benchy.gcode │ ├── color-picker.vue │ ├── main.js │ └── slider.vue ├── gcode-viewer.vue └── main.js ├── tests ├── e2e │ ├── .eslintrc.js │ ├── plugins │ │ └── index.js │ ├── specs │ │ └── demo.js │ └── support │ │ ├── commands.js │ │ └── index.js └── unit │ ├── gcode-parser.spec.js │ └── gcode-viewer.spec.js └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview ESlint config 3 | */ 4 | 5 | module.exports = { 6 | root: true, 7 | env: { 8 | browser: true, 9 | node: true 10 | }, 11 | extends: [ 12 | 'plugin:vue/essential', 13 | 'eslint:recommended' 14 | ], 15 | parserOptions: { 16 | parser: 'babel-eslint' 17 | }, 18 | rules: { 19 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 21 | 'arrow-parens': [ 22 | 'warn', 23 | 'as-needed' 24 | ], 25 | 'brace-style': [ 26 | 'warn', 27 | 'allman' 28 | ], 29 | camelcase: [ 30 | 'warn', 31 | { 32 | properties: 'always' 33 | } 34 | ], 35 | curly: 'warn', 36 | 'no-var': 'error', 37 | 'no-extra-semi': 'error', 38 | 'object-curly-spacing': 'warn', 39 | semi: 'error', 40 | quotes: [ 41 | 'warn', 42 | 'single' 43 | ], 44 | 'quote-props': [ 45 | 'warn', 46 | 'as-needed' 47 | ], 48 | 'space-in-parens': 'warn', 49 | 'prefer-const': 'warn' 50 | }, 51 | overrides: [ 52 | { 53 | files: [ 54 | '**/__tests__/*.{j,t}s?(x)', 55 | '**/tests/unit/**/*.spec.{j,t}s?(x)', 56 | '**/tests/e2e/**/*.{j,t}s?(x)', 57 | ], 58 | env: { 59 | mocha: true 60 | } 61 | } 62 | ] 63 | }; 64 | -------------------------------------------------------------------------------- /.github/workflows/cid.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | on: push 3 | jobs: 4 | test: 5 | name: Test with Node ${{ matrix.node }} 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | node: [13, 14] 10 | steps: 11 | - uses: actions/checkout@v2.1.0 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: ${{ matrix.node }} 15 | - uses: stefanoeb/eslint-action@1.0.2 16 | name: ESLint 17 | - name: Install Modules 18 | run: sudo npm ci --unsafe-perm=true --allow-root 19 | - uses: paambaati/codeclimate-action@v2.6.0 20 | name: Run Tests 21 | env: 22 | CC_TEST_REPORTER_ID: ${{ secrets.CODE_CLIMATE_REPORTER_ID }} 23 | with: 24 | coverageCommand: sudo npm run coverage 25 | publish: 26 | name: Publish 27 | runs-on: ubuntu-latest 28 | needs: test 29 | steps: 30 | - uses: actions/checkout@v2 31 | - uses: actions/setup-node@v1 32 | with: 33 | node-version: 14 34 | - name: Install Modules 35 | run: sudo npm ci --unsafe-perm=true --allow-root 36 | - name: Build 37 | run: npm run build 38 | - name: Publish to NPM 39 | uses: pascalgn/npm-publish-action@1.2.0 40 | with: 41 | #See https://regexr.com/589ii for more information 42 | commit_pattern: "^v?(\\d+\\.\\d+\\.\\d+(?:-(?:alpha|beta)\\.\\d+)?)$" 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # Cypress 6 | tests/e2e/screenshots/* 7 | tests/e2e/videos/* 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | # Istanbul 28 | .nyc_output 29 | coverage -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | //C Spell 3 | "cSpell.allowCompoundWords": true, 4 | "cSpell.language": "en-US,en", 5 | "cSpell.words": [ 6 | "Vuelidate", 7 | "vuetify" 8 | ], 9 | //Editor 10 | "editor.tabSize": 2, 11 | "editor.quickSuggestions": { 12 | "strings": false 13 | }, 14 | //Vetur 15 | "vetur.format.defaultFormatter.js": "vscode-typescript", 16 | "vetur.format.options.tabSize": 2, 17 | "vetur.format.defaultFormatter.ts": "vscode-typescript", 18 | //VS Code Counter 19 | "VSCodeCounter.exclude": [ 20 | "**/.cache/**", 21 | "**/.github/**", 22 | "**/.vscode/**", 23 | "**/crypto/**", 24 | "**/files/**", 25 | "**/dist/**", 26 | "**/node_modules/**", 27 | "package-lock.json", 28 | "package.json" 29 | ], 30 | //Formatter settings 31 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, 32 | "javascript.preferences.quoteStyle": "single", 33 | "javascript.format.placeOpenBraceOnNewLineForControlBlocks": true, 34 | "javascript.format.placeOpenBraceOnNewLineForFunctions": true, 35 | "html.format.indentInnerHtml": true, 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Cloud CNC 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue GCODE Viewer 2 | ![status](https://img.shields.io/badge/status-discontinued-red) 3 | 4 | A Vue component for displaying GCODE via Three.JS (100% client-side). 5 | 6 | # THIS PACKAGE IS DISCONTINUED AND NO LONGER RECEIVES UPDATES! 7 | Please switch to Vue 3D Viewer ([vue-3d-viewer](https://github.com/cloud-cnc/vue-3d-viewer)) which adds support for many more file formats, multithreading, and more. 8 | 9 | ## Documentation 10 | ### Props 11 | Name | Type | Description | Example 12 | --- | --- | --- | --- 13 | `bed` | Object | Bed size | `{X: 22.3, Y: 22.3}` 14 | `gcode` | String | Raw GCODE | `G0 X0 Y0 Z0` 15 | `position` | Object | GCODE Position | `{X: 11.15, Y: 0, Z: 11.15}` 16 | `rotation` | Object | GCODE Rotation | `{X: -90, Y: 0, Z: 180}` 17 | `scale` | Object | GCODE Scale | `{X: 0.1, Y: 0.1, Z: 0.1}` 18 | `theme` | Object | Theme colors | `{extrusionColor: "#4287f5", pathColor: "#0a2f6b",bedColor: "#586375", backgroundColor: "#dfe4ed"}` 19 | 20 | ### Examples 21 | * [Demo](./src/demo) 22 | * [Cloud CNC](https://github.com/Cloud-CNC/frontend/blob/development/src/views/file.vue#L43) 23 | 24 | *Note: These examples might use an outdated version of `vue-gcode-viewer`.* 25 | 26 | ## Reuse 27 | If you're interested in reusing primarily the non-Vue code from this module, you may be interested in [gcode-parser.js](./src/assets/gcode-parser.js), [scene.js](./src/assets/scene.js), and [utils.js](./src/assets/utils.js). 28 | 29 | ## License 30 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FCloud-CNC%2Fvue-gcode-viewer.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FCloud-CNC%2Fvue-gcode-viewer?ref=badge_large) 31 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Babel config 3 | */ 4 | 5 | module.exports = { 6 | plugins: ['istanbul'], 7 | presets: [ 8 | '@vue/cli-plugin-babel/preset' 9 | ] 10 | }; -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /nyc.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview NYC config 3 | */ 4 | 5 | module.exports = { 6 | exclude: [ 7 | '.eslintrc.js', 8 | '*.config.js', 9 | 'dist/*', 10 | 'tests/*' 11 | ] 12 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-gcode-viewer", 3 | "version": "1.2.3", 4 | "author": "Cloud CNC", 5 | "scripts": { 6 | "build": "vue-cli-service build --target lib --name vue-gcode-viewer src/main.js", 7 | "coverage": "npm run test:unit && npm run test:e2e:cli && nyc report --reporter=lcov", 8 | "lint": "vue-cli-service lint", 9 | "serve": "vue-cli-service serve src/demo/main.js", 10 | "test:e2e": "vue-cli-service test:e2e --url http://localhost:8080", 11 | "test:e2e:cli": "concurrently --kill-others --success first \"npm:serve\" \"wait-on http-get://localhost:8080 && npm run test:e2e -- --headless\"", 12 | "test:e2e:gui": "concurrently --kill-others --success first \"npm:serve\" \"wait-on http-get://localhost:8080 && npm run test:e2e\"", 13 | "test:unit": "nyc --no-clean --silent vue-cli-service test:unit" 14 | }, 15 | "main": "dist/vue-gcode-viewer.common.js", 16 | "files": [ 17 | "dist/*", 18 | "src/*" 19 | ], 20 | "dependencies": { 21 | "core-js": "^3.6.4", 22 | "three": "^0.116.1", 23 | "vue": "^2.6.12" 24 | }, 25 | "devDependencies": { 26 | "@cypress/code-coverage": "^3.8.1", 27 | "@mdi/font": "^5.6.55", 28 | "@vue/cli-plugin-babel": "~4.3.0", 29 | "@vue/cli-plugin-e2e-cypress": "^4.5.7", 30 | "@vue/cli-plugin-eslint": "~4.3.0", 31 | "@vue/cli-plugin-unit-mocha": "~4.3.0", 32 | "@vue/cli-service": "~4.3.0", 33 | "@vue/test-utils": "1.0.0-beta.31", 34 | "babel-eslint": "^10.1.0", 35 | "babel-plugin-istanbul": "^6.0.0", 36 | "chai": "^4.1.2", 37 | "concurrently": "^5.3.0", 38 | "eslint": "^6.7.2", 39 | "eslint-plugin-vue": "^6.2.2", 40 | "geckodriver": "^1.20.0", 41 | "nyc": "^15.1.0", 42 | "raw-loader": "^4.0.2", 43 | "vue-cli-plugin-vuetify": "^2.0.7", 44 | "vue-template-compiler": "^2.6.12", 45 | "vuetify": "^2.3.13", 46 | "wait-on": "^5.2.0" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/Cloud-CNC/vue-gcode-viewer/issues" 50 | }, 51 | "homepage": "https://github.com/Cloud-CNC/vue-gcode-viewer#readme", 52 | "keywords": [ 53 | "vue", 54 | "gcode", 55 | "gcode-viewer", 56 | "vue-gcode-viewer", 57 | "gcode-visualizer", 58 | "gcode-parser", 59 | "cloud-cnc" 60 | ], 61 | "license": "MIT", 62 | "repository": { 63 | "type": "git", 64 | "url": "git+https://github.com/Cloud-CNC/vue-gcode-viewer.git" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/assets/gcode-parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview GCODE parser 3 | * Based on https://github.com/mrdoob/three.js/blob/dev/examples/js/loaders/GCodeLoader.js 4 | * @author tentone 5 | * @author joewalnes 6 | * @author wakeful-cloud 7 | */ 8 | 9 | import 10 | { 11 | LineBasicMaterial, 12 | BufferGeometry, 13 | Float32BufferAttribute, 14 | Line 15 | } from 'three'; 16 | 17 | /** 18 | * @class GCodeParser GCODE parser 19 | */ 20 | export default class GCodeParser 21 | { 22 | /** 23 | * @param {Color} extrusionColor 24 | * @param {Color} pathColor 25 | */ 26 | constructor(extrusionColor, pathColor) 27 | { 28 | this.extrusionMaterial = new LineBasicMaterial({color: extrusionColor}); 29 | this.pathMaterial = new LineBasicMaterial({color: pathColor}); 30 | } 31 | 32 | /** 33 | * Parse GCODE 34 | * @param {String} gcode The raw GCODE 35 | * @returns {Promise} 36 | */ 37 | parse(gcode) 38 | { 39 | //Parse asynchronously and without blocking Three.JS 40 | return new Promise(resolve => 41 | { 42 | //Variables 43 | const extrusionVertices = []; 44 | const pathVertices = []; 45 | let state = {x: 0, y: 0, z: 0, e: 0, f: 0}; 46 | let relative = false; 47 | 48 | //Remove comments 49 | gcode = gcode.replace(/;.+/g, '').split('\n'); 50 | 51 | //Parse commands 52 | for (let i = 0; i < gcode.length; i++) 53 | { 54 | //Parse tokens 55 | let tokens = gcode[i].split(' '); 56 | const command = tokens[0].toUpperCase(); 57 | const args = {}; 58 | tokens = tokens.splice(1); 59 | 60 | //Parse arguments 61 | for (let i = 0; i < tokens.length; i++) 62 | { 63 | //If not null, store argument 64 | if (tokens[i][0] != null) 65 | { 66 | const key = tokens[i][0].toLowerCase(); 67 | const value = parseFloat(tokens[i].substring(1)); 68 | args[key] = value; 69 | } 70 | } 71 | 72 | //Convert GCODE to Three.JS land 73 | 74 | //Linear move 75 | if (command == 'G0' || command == 'G1') 76 | { 77 | const line = { 78 | x: args.x != null ? absolute(relative, state.x, args.x) : state.x, 79 | y: args.y != null ? absolute(relative, state.y, args.y) : state.y, 80 | z: args.z != null ? absolute(relative, state.z, args.z) : state.z, 81 | e: args.e != null ? absolute(relative, state.e, args.e) : state.e, 82 | f: args.f != null ? absolute(relative, state.f, args.f) : state.f 83 | }; 84 | 85 | //Only push valid coordinates/states 86 | if (!isNaN(line.x) && 87 | !isNaN(line.y) && 88 | !isNaN(line.z) && 89 | !isNaN(line.e) && 90 | !isNaN(line.f) 91 | ) 92 | { 93 | //Extruding 94 | if (delta(relative, state.e, line.e) > 0) 95 | { 96 | extrusionVertices.push(line.x, line.y, line.z); 97 | } 98 | //Path 99 | else 100 | { 101 | pathVertices.push(line.x, line.y, line.z); 102 | } 103 | } 104 | 105 | //Update position 106 | state = line; 107 | } 108 | //Absolute positioning 109 | else if (command == 'G90') 110 | { 111 | relative = false; 112 | } 113 | //Relative positioning 114 | else if (command == 'G91') 115 | { 116 | relative = true; 117 | } 118 | //Set position 119 | else if (command == 'G92') 120 | { 121 | state = args; 122 | } 123 | } 124 | 125 | //Object generation 126 | const extrusionBuffer = new BufferGeometry(); 127 | const pathBuffer = new BufferGeometry(); 128 | 129 | extrusionBuffer.setAttribute( 130 | 'position', 131 | new Float32BufferAttribute(extrusionVertices, 3) 132 | ); 133 | pathBuffer.setAttribute( 134 | 'position', 135 | new Float32BufferAttribute(pathVertices, 3) 136 | ); 137 | 138 | const extrusionObject = new Line(extrusionBuffer, this.extrusionMaterial); 139 | const pathObject = new Line(pathBuffer, this.pathMaterial); 140 | 141 | //Cleanup 142 | this.extrusionMaterial.dispose(); 143 | this.pathMaterial.dispose(); 144 | extrusionBuffer.dispose(); 145 | pathBuffer.dispose(); 146 | 147 | return resolve({extrusion: extrusionObject, path: pathObject}); 148 | }); 149 | } 150 | } 151 | 152 | //Calculate the delta between 2 vertices 153 | function delta(relative, vertex1, vertex2) 154 | { 155 | return relative ? vertex2 : vertex2 - vertex1; 156 | } 157 | 158 | //Calculate the absolute value between 2 vertices 159 | function absolute(relative, vertex1, vertex2) 160 | { 161 | return relative ? vertex1 + vertex2 : vertex2; 162 | } -------------------------------------------------------------------------------- /src/assets/scene.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview ThreeJS scene management 3 | */ 4 | 5 | //Imports 6 | import 7 | { 8 | Color, 9 | GammaEncoding, 10 | MOUSE, 11 | Mesh, 12 | MeshBasicMaterial, 13 | PerspectiveCamera, 14 | PlaneBufferGeometry, 15 | Scene, 16 | WebGLRenderer 17 | } from 'three'; 18 | import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'; 19 | import utils from './utils'; 20 | 21 | //State 22 | const state = { 23 | camera: null, 24 | canvas: null, 25 | controls: null, 26 | destroyed: false, 27 | object: null, 28 | plane: null, 29 | renderer: null, 30 | scene: null 31 | }; 32 | 33 | //Window resize event handler 34 | const resize = () => 35 | { 36 | //Update renderer size 37 | state.renderer.setSize( 38 | state.canvas.clientWidth, 39 | state.canvas.clientHeight 40 | ); 41 | 42 | //Update camera aspect ratio 43 | state.camera.aspect = state.canvas.clientWidth / state.canvas.clientHeight; 44 | state.camera.updateProjectionMatrix(); 45 | }; 46 | 47 | /** 48 | * @function Setup scene 49 | * @param {Element} canvas HTML canvas element 50 | * @param {Object} bed Bed dimensions 51 | * @param {Object} theme Theme 52 | * @param {String} gcode Raw GCODE 53 | * @param {Object} position Position 54 | * @param {Object} rotation Rotation 55 | * @param {Object} scale Scale 56 | */ 57 | const setup = async (canvas, bed, theme, gcode, position, rotation, scale) => 58 | { 59 | //Canvas 60 | state.canvas = canvas; 61 | 62 | //Renderer 63 | state.renderer = new WebGLRenderer({antialias: true}); 64 | state.renderer.setPixelRatio(window.devicePixelRatio); 65 | state.renderer.setSize( 66 | state.canvas.clientWidth, 67 | state.canvas.clientHeight 68 | ); 69 | state.renderer.encoding = GammaEncoding; 70 | state.renderer.outputEncoding = GammaEncoding; 71 | state.renderer.shadowMap.enabled = true; 72 | state.canvas.appendChild(state.renderer.domElement); 73 | 74 | //Camera 75 | state.camera = new PerspectiveCamera( 76 | 50, 77 | state.canvas.clientWidth / state.canvas.clientHeight, 78 | 0.1, 79 | 200 80 | ); 81 | state.camera.position.set(0, bed.X / 2, -bed.Y); 82 | 83 | //Orbit controls 84 | state.controls = new OrbitControls(state.camera, state.canvas); 85 | state.controls.rotateSpeed = 0.7; 86 | state.controls.minDistance = 1; 87 | state.controls.maxDistance = 100; 88 | state.controls.minPolarAngle = 0; 89 | state.controls.maxPolarAngle = Math.PI; 90 | state.controls.mouseButtons = { 91 | LEFT: MOUSE.PAN, 92 | MIDDLE: MOUSE.DOLLY, 93 | RIGHT: MOUSE.ROTATE 94 | }; 95 | 96 | //Scene 97 | state.scene = new Scene(); 98 | state.scene.background = new Color(theme.backgroundColor); 99 | 100 | //Plane 101 | state.plane = new Mesh( 102 | new PlaneBufferGeometry(), 103 | new MeshBasicMaterial({color: new Color(theme.bedColor)}) 104 | ); 105 | state.plane.rotation.x = -Math.PI / 2; 106 | state.plane.scale.set(bed.X, bed.Y, 1); 107 | state.scene.add(state.plane); 108 | 109 | //GCODE 110 | if (gcode != null) 111 | { 112 | await utils.update.gcode(gcode, theme, state.scene); 113 | utils.update.position(position); 114 | utils.update.rotation(rotation); 115 | utils.update.scale(scale); 116 | } 117 | 118 | //Subscribe to resize event 119 | window.addEventListener('resize', resize); 120 | 121 | //Start 122 | const animate = () => 123 | { 124 | if (!state.destroyed) 125 | { 126 | state.controls.update(); 127 | requestAnimationFrame(animate); 128 | state.renderer.render(state.scene, state.camera); 129 | } 130 | }; 131 | animate(); 132 | 133 | //Environment 134 | if (process.env.NODE_ENV == 'development' || process.env.NODE_ENV == 'testing') 135 | { 136 | window.getVueGcodeViewerState = () => 137 | { 138 | return state; 139 | }; 140 | } 141 | }; 142 | 143 | /** 144 | * @function Tear down scene 145 | */ 146 | const teardown = () => 147 | { 148 | //Unsubscribe to resize event 149 | window.removeEventListener('resize', resize); 150 | 151 | //Clean everything in the scene 152 | state.scene.children.forEach(object => 153 | { 154 | //Geometry 155 | if ( 156 | object.geometry != null && 157 | typeof object.geometry.dispose == 'function' 158 | ) 159 | { 160 | object.geometry.dispose(); 161 | } 162 | 163 | //Material 164 | if ( 165 | object.material != null && 166 | typeof object.material.dispose == 'function' 167 | ) 168 | { 169 | object.material.dispose(); 170 | } 171 | }); 172 | 173 | //Disable animation loop 174 | state.destroyed = true; 175 | 176 | //Delete scene and camera 177 | delete state.scene; 178 | delete state.camera; 179 | 180 | //Dispose of controls and renderer 181 | state.controls.dispose(); 182 | state.renderer.dispose(); 183 | }; 184 | 185 | //Export 186 | export {setup, teardown, state}; -------------------------------------------------------------------------------- /src/assets/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview ThreeJS scene utilities 3 | */ 4 | 5 | //Imports 6 | import {state} from '@/assets/scene'; 7 | import GCodeParser from '@/assets/gcode-parser'; 8 | 9 | /** 10 | * Update various scene properties 11 | */ 12 | const update = { 13 | /** 14 | * @function Update bed 15 | */ 16 | bed: bed => 17 | { 18 | const {X, Y} = bed; 19 | state.plane.scale.set(X, Y, 1); 20 | }, 21 | 22 | /** 23 | * @function Update GCODE 24 | * @param {String} raw Raw GCODE 25 | * @param {Object} theme Theme 26 | * @returns {Promise} 27 | */ 28 | gcode: async (raw, theme) => 29 | { 30 | const parser = new GCodeParser( 31 | theme.extrusionColor, 32 | theme.pathColor 33 | ); 34 | 35 | const object = await parser.parse(raw); 36 | 37 | //Add to scene 38 | state.scene.add(object.extrusion); 39 | state.scene.add(object.path); 40 | 41 | //Save for later manipulation 42 | state.object = object; 43 | }, 44 | 45 | /** 46 | * @function Update object position 47 | * @param {Object} position Position 48 | */ 49 | position: position => 50 | { 51 | const {X, Y, Z} = position; 52 | state.object.extrusion.position.set(X, Y, Z); 53 | state.object.path.position.set(X, Y, Z); 54 | }, 55 | 56 | /** 57 | * @function Update object rotation 58 | * @param {Object} rotation Rotation 59 | */ 60 | rotation: rotation => 61 | { 62 | let {X, Y, Z} = rotation; 63 | X *= Math.PI / 180; 64 | Y *= Math.PI / 180; 65 | Z *= Math.PI / 180; 66 | state.object.extrusion.rotation.set(X, Y, Z); 67 | state.object.path.rotation.set(X, Y, Z); 68 | }, 69 | 70 | /** 71 | * @function Update object scale 72 | * @param {Object} scale Scale 73 | */ 74 | scale: scale => 75 | { 76 | const {X, Y, Z} = scale; 77 | state.object.extrusion.scale.set(Z, X, Y); 78 | state.object.path.scale.set(Z, X, Y); 79 | }, 80 | 81 | /** 82 | * @function Update scene theme 83 | * @param {Object} theme Theme 84 | */ 85 | theme: theme => 86 | { 87 | state.object.extrusion.material.color.set(theme.extrusionColor); 88 | state.object.path.material.color.set(theme.pathColor); 89 | state.plane.material.color.set(theme.bedColor); 90 | state.scene.background.set(theme.backgroundColor); 91 | } 92 | }; 93 | 94 | //Export 95 | export default {update}; -------------------------------------------------------------------------------- /src/demo/App.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 129 | 130 | -------------------------------------------------------------------------------- /src/demo/color-picker.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 27 | 28 | -------------------------------------------------------------------------------- /src/demo/main.js: -------------------------------------------------------------------------------- 1 | //Imports 2 | import App from './App.vue'; 3 | import Viewer from '../main.js'; 4 | import Vue from 'vue'; 5 | import Vuetify from 'vuetify'; 6 | import 'vuetify/dist/vuetify.css'; 7 | import '@mdi/font/css/materialdesignicons.min.css'; 8 | 9 | //Vue plugins 10 | Vue.use(Viewer); 11 | Vue.use(Vuetify); 12 | 13 | //Vue instance 14 | new Vue({ 15 | el: '#app', 16 | render: h => h(App), 17 | vuetify: new Vuetify() 18 | }); 19 | -------------------------------------------------------------------------------- /src/demo/slider.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 59 | 60 | -------------------------------------------------------------------------------- /src/gcode-viewer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 118 | 119 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Vue GCODE Viewer 3 | */ 4 | 5 | //Components 6 | import gcodeViewer from './gcode-viewer.vue'; 7 | 8 | //Export 9 | export default { 10 | /** 11 | * @function Install Vue GCODE Viewer 12 | * @param {Vue} Vue 13 | */ 14 | install: Vue => 15 | { 16 | //Register component 17 | Vue.mixin({ 18 | components: { 19 | 'gcode-viewer': gcodeViewer 20 | } 21 | }); 22 | } 23 | }; -------------------------------------------------------------------------------- /tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'cypress' 4 | ], 5 | env: { 6 | mocha: true, 7 | 'cypress/globals': true 8 | }, 9 | rules: { 10 | strict: 'off' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | // https://docs.cypress.io/guides/guides/plugins-guide.html 3 | 4 | // if you need a custom webpack configuration you can uncomment the following import 5 | // and then use the `file:preprocessor` event 6 | // as explained in the cypress docs 7 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples 8 | 9 | // /* eslint-disable import/no-extraneous-dependencies, global-require */ 10 | // const webpack = require('@cypress/webpack-preprocessor') 11 | 12 | module.exports = (on, config) => 13 | { 14 | // on('file:preprocessor', webpack({ 15 | // webpackOptions: require('@vue/cli-service/webpack.config'), 16 | // watchOptions: {} 17 | // })) 18 | 19 | require('@cypress/code-coverage/task')(on, config); 20 | on('file:preprocessor', require('@cypress/code-coverage/use-babelrc')); 21 | 22 | return Object.assign({}, config, { 23 | fixturesFolder: 'tests/e2e/fixtures', 24 | integrationFolder: 'tests/e2e/specs', 25 | screenshotsFolder: 'tests/e2e/screenshots', 26 | videosFolder: 'tests/e2e/videos', 27 | supportFile: 'tests/e2e/support/index.js' 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /tests/e2e/specs/demo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Demo E2E tests 3 | */ 4 | 5 | describe('demo', () => 6 | { 7 | it('renders everything', () => 8 | { 9 | cy.visit('/'); 10 | 11 | cy.wait(5000); 12 | 13 | cy.get('[data-e2e=gcode-viewer] > canvas').should('be.visible'); 14 | cy.get('[data-e2e=toggle-menu]').should('be.visible'); 15 | 16 | cy.get('[data-e2e=toggle-menu]').click(); 17 | 18 | cy.get('[data-e2e=menu]').should('be.visible'); 19 | cy.get('[data-e2e=bed-x]').should('be.visible'); 20 | cy.get('[data-e2e=bed-y]').should('be.visible'); 21 | cy.get('[data-e2e=position-x]').should('be.visible'); 22 | cy.get('[data-e2e=position-y]').should('be.visible'); 23 | cy.get('[data-e2e=position-z]').should('be.visible'); 24 | cy.get('[data-e2e=rotation-x]').should('be.visible'); 25 | cy.get('[data-e2e=rotation-y]').should('be.visible'); 26 | cy.get('[data-e2e=rotation-z]').should('be.visible'); 27 | cy.get('[data-e2e=scale-x]').scrollIntoView().should('be.visible'); 28 | cy.get('[data-e2e=scale-y]').scrollIntoView().should('be.visible'); 29 | cy.get('[data-e2e=scale-z]').scrollIntoView().should('be.visible'); 30 | cy.get('[data-e2e=extrusion-color]').scrollIntoView().should('be.visible'); 31 | cy.get('[data-e2e=path-color]').scrollIntoView().should('be.visible'); 32 | cy.get('[data-e2e=bed-color]').scrollIntoView().should('be.visible'); 33 | cy.get('[data-e2e=background-color]').scrollIntoView().should('be.visible'); 34 | }); 35 | 36 | it('updates the bed', () => 37 | { 38 | cy.visit('/'); 39 | 40 | cy.get('[data-e2e=toggle-menu]').click(); 41 | 42 | cy.get('[data-e2e=bed-x]').clear().type('15{enter}'); 43 | cy.get('[data-e2e=bed-y]').clear().type('16.5{enter}'); 44 | 45 | cy.window().then(window => 46 | { 47 | const state = window.getVueGcodeViewerState(); 48 | 49 | expect(state.plane.scale.x).to.equal(15); 50 | expect(state.plane.scale.y).to.equal(16.5); 51 | }); 52 | }); 53 | 54 | it('updates the position', () => 55 | { 56 | const x = 10; 57 | const y = -5.5; 58 | const z = 4.3; 59 | 60 | cy.visit('/'); 61 | 62 | cy.get('[data-e2e=toggle-menu]').click(); 63 | 64 | cy.get('[data-e2e=position-x]').clear().type(`${x}{enter}`); 65 | cy.get('[data-e2e=position-y]').clear().type(`${y}{enter}`); 66 | cy.get('[data-e2e=position-z]').clear().type(`${z}{enter}`); 67 | 68 | cy.window().then(window => 69 | { 70 | const state = window.getVueGcodeViewerState(); 71 | 72 | expect(state.object.extrusion.position.x).to.equal(x); 73 | expect(state.object.extrusion.position.y).to.equal(y); 74 | expect(state.object.extrusion.position.z).to.equal(z); 75 | expect(state.object.path.position.x).to.equal(x); 76 | expect(state.object.path.position.y).to.equal(y); 77 | expect(state.object.path.position.z).to.equal(z); 78 | }); 79 | }); 80 | 81 | it('updates the rotation', () => 82 | { 83 | const x = 45; 84 | const y = 135; 85 | const z = -100; 86 | 87 | cy.visit('/'); 88 | 89 | cy.get('[data-e2e=toggle-menu]').click(); 90 | 91 | cy.get('[data-e2e=rotation-x]').clear().type(`${x}{enter}`); 92 | cy.get('[data-e2e=rotation-y]').clear().type(`${y}{enter}`); 93 | cy.get('[data-e2e=rotation-z]').clear().type(`${z}{enter}`); 94 | 95 | cy.window().then(window => 96 | { 97 | const state = window.getVueGcodeViewerState(); 98 | 99 | expect(state.object.extrusion.rotation.x).to.equal(x * (Math.PI / 180)); 100 | expect(state.object.extrusion.rotation.y).to.equal(y * (Math.PI / 180)); 101 | expect(state.object.extrusion.rotation.z).to.equal(z * (Math.PI / 180)); 102 | expect(state.object.path.rotation.x).to.equal(x * (Math.PI / 180)); 103 | expect(state.object.path.rotation.y).to.equal(y * (Math.PI / 180)); 104 | expect(state.object.path.rotation.z).to.equal(z * (Math.PI / 180)); 105 | }); 106 | }); 107 | 108 | it('updates the scale', () => 109 | { 110 | const x = 0.2; 111 | const y = 1; 112 | const z = 4.3; 113 | 114 | cy.visit('/'); 115 | 116 | cy.get('[data-e2e=toggle-menu]').click(); 117 | 118 | cy.get('[data-e2e=scale-x]').clear().type(`${x}{enter}`); 119 | cy.get('[data-e2e=scale-y]').clear().type(`${y}{enter}`); 120 | cy.get('[data-e2e=scale-z]').clear().type(`${z}{enter}`); 121 | 122 | cy.window().then(window => 123 | { 124 | const state = window.getVueGcodeViewerState(); 125 | 126 | expect(state.object.extrusion.scale.x).to.equal(z); 127 | expect(state.object.extrusion.scale.y).to.equal(x); 128 | expect(state.object.extrusion.scale.z).to.equal(y); 129 | expect(state.object.path.scale.x).to.equal(z); 130 | expect(state.object.path.scale.y).to.equal(x); 131 | expect(state.object.path.scale.z).to.equal(y); 132 | }); 133 | }); 134 | 135 | it('updates the theme', () => 136 | { 137 | const extrusionColor = '#00000f'; 138 | const pathColor = '#0000f0'; 139 | const bedColor = '#0000ff'; 140 | const backgroundColor = '#000f00'; 141 | 142 | cy.visit('/'); 143 | 144 | cy.get('[data-e2e=toggle-menu]').click(); 145 | 146 | //Set color pickers to hex code 147 | cy.get('[data-e2e=extrusion-color]').children().eq(1).children().eq(1).children().eq(3).click().click(); 148 | cy.get('[data-e2e=path-color]').children().eq(1).children().eq(1).children().eq(3).click().click(); 149 | cy.get('[data-e2e=bed-color]').children().eq(1).children().eq(1).children().eq(3).click().click(); 150 | cy.get('[data-e2e=background-color]').children().eq(1).children().eq(1).children().eq(3).click().click(); 151 | 152 | cy.get('[data-e2e=extrusion-color]').children().eq(1).children().eq(1).children().eq(0).children().eq(0).clear().type(`${extrusionColor}{enter}`); 153 | cy.get('[data-e2e=path-color]').children().eq(1).children().eq(1).children().eq(0).children().eq(0).clear().type(`${pathColor}{enter}`); 154 | cy.get('[data-e2e=bed-color]').children().eq(1).children().eq(1).children().eq(0).children().eq(0).clear().type(`${bedColor}{enter}`); 155 | cy.get('[data-e2e=background-color]').children().eq(1).children().eq(1).children().eq(0).children().eq(0).clear().type(`${backgroundColor}{enter}`); 156 | 157 | cy.window().then(window => 158 | { 159 | const state = window.getVueGcodeViewerState(); 160 | 161 | expect(`#${state.object.extrusion.material.color.getHexString()}`).to.equal(extrusionColor); 162 | expect(`#${state.object.path.material.color.getHexString()}`).to.equal(pathColor); 163 | expect(`#${state.plane.material.color.getHexString()}`).to.equal(bedColor); 164 | expect(`#${state.scene.background.getHexString()}`).to.equal(backgroundColor); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | //Commands 17 | import './commands'; 18 | 19 | //Code coverage 20 | import '@cypress/code-coverage/support'; -------------------------------------------------------------------------------- /tests/unit/gcode-parser.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview GCODE parser unit tests 3 | */ 4 | 5 | //Imports 6 | import {expect} from 'chai'; 7 | import GCodeParser from '@/assets/gcode-parser.js'; 8 | import {Color} from 'three'; 9 | 10 | //Mock GCODE 11 | const gcode = 'G0 X0 Y0 Z0\nG0 X5 Y0 Z0\nG0 X5 Y5 Z0\nG0 X5 Y5 Z5\nG0 E1 X5 Y5 Z0\nG0 E2 X5 Y0 Z0\nG0 E3 X0 Y0 Z0'; 12 | 13 | //Unit tests 14 | describe('GCODE parser', () => 15 | { 16 | it('generates correct vertices', async () => 17 | { 18 | const parser = new GCodeParser('#ffffff', '#000000'); 19 | const lines = await parser.parse(gcode); 20 | 21 | expect(Array.from(lines.path.geometry.attributes.position.array)).to.eql([ 22 | 0, 0, 0, 23 | 5, 0, 0, 24 | 5, 5, 0, 25 | 5, 5, 5 26 | ]); 27 | 28 | expect(Array.from(lines.extrusion.geometry.attributes.position.array)).to.eql([ 29 | 5, 5, 0, 30 | 5, 0, 0, 31 | 0, 0, 0 32 | ]); 33 | }); 34 | 35 | it('uses correct colors', async () => 36 | { 37 | const extrusionColor = '#ffffff'; 38 | const pathColor = '#000000'; 39 | 40 | const parser = new GCodeParser(extrusionColor, pathColor); 41 | const lines = await parser.parse(gcode); 42 | 43 | expect(lines.extrusion.material.color).to.eql(new Color(extrusionColor)); 44 | expect(lines.path.material.color).to.eql(new Color(pathColor)); 45 | }); 46 | 47 | it('ignores invalid gcode commands and comments', async () => 48 | { 49 | const parser = new GCodeParser('#ffffff', '#000000'); 50 | const lines = await parser.parse(gcode + '\nG2 X10 Y10 R7\n;COMMENT'); 51 | 52 | expect(Array.from(lines.path.geometry.attributes.position.array)).to.eql([ 53 | 0, 0, 0, 54 | 5, 0, 0, 55 | 5, 5, 0, 56 | 5, 5, 5 57 | ]); 58 | 59 | expect(Array.from(lines.extrusion.geometry.attributes.position.array)).to.eql([ 60 | 5, 5, 0, 61 | 5, 0, 0, 62 | 0, 0, 0 63 | ]); 64 | }); 65 | }); -------------------------------------------------------------------------------- /tests/unit/gcode-viewer.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview GCODE viewer unit tests 3 | */ 4 | 5 | //Imports 6 | import {expect} from 'chai'; 7 | import gcodeViewer from '@/gcode-viewer.vue'; 8 | 9 | //Unit tests 10 | describe('GCODE viewer', () => 11 | { 12 | it('excepts props', () => 13 | { 14 | expect(gcodeViewer).to.haveOwnProperty('props'); 15 | expect(gcodeViewer.props).to.haveOwnProperty('bed'); 16 | expect(gcodeViewer.props).to.haveOwnProperty('gcode'); 17 | expect(gcodeViewer.props).to.haveOwnProperty('position'); 18 | expect(gcodeViewer.props).to.haveOwnProperty('rotation'); 19 | expect(gcodeViewer.props).to.haveOwnProperty('scale'); 20 | expect(gcodeViewer.props).to.haveOwnProperty('theme'); 21 | 22 | expect(gcodeViewer.props.bed).to.haveOwnProperty('default'); 23 | expect(gcodeViewer.props.bed.default()).to.eql({ 24 | X: 10, 25 | Y: 10 26 | }); 27 | expect(gcodeViewer.props.bed).to.haveOwnProperty('type', Object); 28 | 29 | expect(gcodeViewer.props.gcode).to.haveOwnProperty('required', true); 30 | expect(gcodeViewer.props.gcode).to.haveOwnProperty('type', String); 31 | 32 | expect(gcodeViewer.props.position).to.haveOwnProperty('default'); 33 | expect(gcodeViewer.props.position.default()).to.eql({ 34 | X: 5, 35 | Y: 0, 36 | Z: -5 37 | }); 38 | expect(gcodeViewer.props.position).to.haveOwnProperty('type', Object); 39 | 40 | expect(gcodeViewer.props.rotation).to.haveOwnProperty('default'); 41 | expect(gcodeViewer.props.rotation.default()).to.eql({ 42 | X: -90, 43 | Y: 0, 44 | Z: 180 45 | }); 46 | expect(gcodeViewer.props.rotation).to.haveOwnProperty('type', Object); 47 | 48 | expect(gcodeViewer.props.scale).to.haveOwnProperty('default'); 49 | expect(gcodeViewer.props.scale.default()).to.eql({ 50 | X: 0.1, 51 | Y: 0.1, 52 | Z: 0.1 53 | }); 54 | expect(gcodeViewer.props.scale).to.haveOwnProperty('type', Object); 55 | 56 | expect(gcodeViewer.props.theme).to.haveOwnProperty('default'); 57 | expect(gcodeViewer.props.theme.default()).to.eql({ 58 | extrusionColor: '#4287f5', 59 | pathColor: '#0a2f6b', 60 | bedColor: '#586375', 61 | backgroundColor: '#dfe4ed' 62 | }); 63 | expect(gcodeViewer.props.theme).to.haveOwnProperty('type', Object); 64 | }); 65 | 66 | it('watches scene properties', () => 67 | { 68 | expect(gcodeViewer).to.haveOwnProperty('watch'); 69 | expect(gcodeViewer.watch).to.haveOwnProperty('bed'); 70 | expect(gcodeViewer.watch).to.haveOwnProperty('gcode'); 71 | expect(gcodeViewer.watch).to.haveOwnProperty('position'); 72 | expect(gcodeViewer.watch).to.haveOwnProperty('rotation'); 73 | expect(gcodeViewer.watch).to.haveOwnProperty('scale'); 74 | expect(gcodeViewer.watch).to.haveOwnProperty('theme'); 75 | 76 | expect(gcodeViewer.watch.bed).to.haveOwnProperty('deep', true); 77 | expect(gcodeViewer.watch.bed).to.haveOwnProperty('handler'); 78 | expect(gcodeViewer.watch.bed.handler).to.be.a('function'); 79 | 80 | expect(gcodeViewer.watch.gcode).to.not.be.null; 81 | 82 | expect(gcodeViewer.watch.position).to.haveOwnProperty('deep', true); 83 | expect(gcodeViewer.watch.position).to.haveOwnProperty('handler'); 84 | expect(gcodeViewer.watch.position.handler).to.be.a('function'); 85 | 86 | expect(gcodeViewer.watch.rotation).to.haveOwnProperty('deep', true); 87 | expect(gcodeViewer.watch.rotation).to.haveOwnProperty('handler'); 88 | expect(gcodeViewer.watch.rotation.handler).to.be.a('function'); 89 | 90 | expect(gcodeViewer.watch.scale).to.haveOwnProperty('deep', true); 91 | expect(gcodeViewer.watch.scale).to.haveOwnProperty('handler'); 92 | expect(gcodeViewer.watch.scale.handler).to.be.a('function'); 93 | 94 | expect(gcodeViewer.watch.theme).to.haveOwnProperty('deep', true); 95 | expect(gcodeViewer.watch.theme).to.haveOwnProperty('handler'); 96 | expect(gcodeViewer.watch.theme.handler).to.be.a('function'); 97 | }); 98 | 99 | it('has lifecycle hooks', () => 100 | { 101 | expect(gcodeViewer).to.haveOwnProperty('mounted'); 102 | expect(gcodeViewer.mounted).to.be.a('function'); 103 | 104 | expect(gcodeViewer).to.haveOwnProperty('destroyed'); 105 | expect(gcodeViewer.destroyed).to.be.a('function'); 106 | }); 107 | }); -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Vue CLI config 3 | */ 4 | 5 | module.exports = { 6 | configureWebpack: { 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.gcode$/i, 11 | use: 'raw-loader', 12 | } 13 | ] 14 | } 15 | }, 16 | css: { 17 | extract: false 18 | } 19 | }; --------------------------------------------------------------------------------