From e411292a041d9bc6bab3bb18b1457f7592819e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Thu, 15 Aug 2024 19:59:49 -0400 Subject: [PATCH 01/34] Interpreter prototype --- src/gcode-parser.ts | 62 ++++++++++++++++++++++++++-- src/interpreter.ts | 96 ++++++++++++++++++++++++++++++++++++++++++++ src/path.ts | 58 ++++++++++++++++++++++++++ src/webgl-preview.ts | 63 ++++++++++++++++++++++++----- 4 files changed, 264 insertions(+), 15 deletions(-) create mode 100644 src/interpreter.ts create mode 100644 src/path.ts diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index 700deb8e..b0e028f9 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -60,13 +60,66 @@ type MoveCommandParamName = 'x' | 'y' | 'z' | 'r' | 'e' | 'f' | 'i' | 'j'; type MoveCommandParams = { [key in MoveCommandParamName]?: number; }; + +export enum Code { + G0 = 'G0', + G1 = 'G1', + G2 = 'G2', + G3 = 'G3', + T0 = 'T0', + T1 = 'T1', + T2 = 'T2', + T3 = 'T3', + T4 = 'T4', + T5 = 'T5', + T6 = 'T6', + T7 = 'T7' +} export class GCodeCommand { + public code?: Code; constructor( public src: string, public gcode: string, public params: CommandParams, public comment?: string - ) {} + ) { + this.code = this.match(gcode); + } + + match(gcode: string): Code { + switch (gcode) { + case 'g0': + case 'g00': + return Code.G0; + case 'g1': + case 'g01': + return Code.G1; + case 'g2': + case 'g02': + return Code.G2; + case 'g3': + case 'g03': + return Code.G3; + case 't0': + return Code.T0; + case 't1': + return Code.T1; + case 't2': + return Code.T2; + case 't3': + return Code.T3; + case 't4': + return Code.T4; + case 't5': + return Code.T5; + case 't6': + return Code.T6; + case 't7': + return Code.T7; + default: + return undefined; + } + } } export class MoveCommand extends GCodeCommand { @@ -104,6 +157,7 @@ export class Layer { export class Parser { lines: string[] = []; + commands: GCodeCommand[] = []; /** * @experimental GCode commands before extrusion starts. @@ -133,12 +187,12 @@ export class Parser { this.lines = this.lines.concat(lines); - const commands = this.lines2commands(lines); + this.commands = this.lines2commands(lines); - this.groupIntoLayers(commands); + this.groupIntoLayers(this.commands); // merge thumbs - const thumbs = this.parseMetadata(commands.filter((cmd) => cmd.comment)).thumbnails; + const thumbs = this.parseMetadata(this.commands.filter((cmd) => cmd.comment)).thumbnails; for (const [key, value] of Object.entries(thumbs)) { this.metadata.thumbnails[key] = value; } diff --git a/src/interpreter.ts b/src/interpreter.ts new file mode 100644 index 00000000..acd82eed --- /dev/null +++ b/src/interpreter.ts @@ -0,0 +1,96 @@ +import { Path, PathType } from './path'; +import { GCodeCommand, SelectToolCommand } from './gcode-parser'; + +export class State { + x: number; + y: number; + z: number; + r: number; + e: number; + i: number; + j: number; + t: number; + + static get initial(): State { + const state = new State(); + Object.assign(state, { x: 0, y: 0, z: 0, r: 0, e: 0, i: 0, j: 0, t: 0 }); + return state; + } +} + +class Print { + paths: Path[]; + state: State; + + constructor(state?: State) { + this.paths = []; + this.state = state || State.initial; + } +} + +export class Interpreter { + execute(commands: GCodeCommand[], initialState?: State): Print { + const print = new Print(initialState); + + commands.forEach((command) => { + if (command.code !== undefined) { + this[command.code](command, print); + } + }); + + return print; + } + + G0(command: GCodeCommand, print: Print): void { + const { x, y, z, e } = command.params; + const { state } = print; + + let lastPath = print.paths[print.paths.length - 1]; + const pathType = e ? PathType.Extrusion : PathType.Travel; + + if (lastPath === undefined || lastPath.type !== pathType) { + lastPath = new Path(pathType, 0.6, 0.2, state.t); + print.paths.push(lastPath); + lastPath.addPoint(state.x, state.y, state.z); + } + + state.x = x || state.x; + state.y = y || state.y; + state.z = z || state.z; + + lastPath.addPoint(state.x, state.y, state.z); + } + + G1 = this.G0; + G2(command: GCodeCommand, print: Print): void { + console.log(command.src, print); + } + G3(command: GCodeCommand, print: Print): void { + console.log(command.src, print); + } + + T0(command: SelectToolCommand, print: Print): void { + print.state.t = 0; + } + T1(command: SelectToolCommand, print: Print): void { + print.state.t = 1; + } + T2(command: SelectToolCommand, print: Print): void { + print.state.t = 2; + } + T3(command: SelectToolCommand, print: Print): void { + print.state.t = 3; + } + T4(command: SelectToolCommand, print: Print): void { + print.state.t = 4; + } + T5(command: SelectToolCommand, print: Print): void { + print.state.t = 5; + } + T6(command: SelectToolCommand, print: Print): void { + print.state.t = 6; + } + T7(command: SelectToolCommand, print: Print): void { + print.state.t = 7; + } +} diff --git a/src/path.ts b/src/path.ts new file mode 100644 index 00000000..f88116a4 --- /dev/null +++ b/src/path.ts @@ -0,0 +1,58 @@ +/* eslint-disable no-unused-vars */ +import { BufferGeometry, Vector3 } from 'three'; +import { ExtrusionGeometry } from './extrusion-geometry'; + +export enum PathType { + Travel = 'Travel', + Extrusion = 'Extrusion' +} + +export class Path { + type: PathType; + vertices: number[]; + extrusionWidth: number; + lineHeight: number; + geometryCache: BufferGeometry | undefined; + tool: number; + + constructor(type: PathType, extrusionWidth = 0.6, lineHeight = 0.2, tool = 0) { + this.type = type; + this.vertices = []; + this.extrusionWidth = extrusionWidth; + this.lineHeight = lineHeight; + this.tool = tool; + } + + addPoint(x: number, y: number, z: number): void { + this.vertices.push(x, y, z); + } + + checkLineContinuity(x: number, y: number, z: number): boolean { + if (this.vertices.length < 3) { + return false; + } + + const lastX = this.vertices[this.vertices.length - 3]; + const lastY = this.vertices[this.vertices.length - 2]; + const lastZ = this.vertices[this.vertices.length - 1]; + + return x === lastX && y === lastY && z === lastZ; + } + + geometry(): BufferGeometry { + if (!this.geometryCache) { + if (this.vertices.length < 3) { + return new BufferGeometry(); + } + + const extrusionPaths: Vector3[] = []; + + for (let i = 0; i < this.vertices.length; i += 3) { + extrusionPaths.push(new Vector3(this.vertices[i], this.vertices[i + 1], this.vertices[i + 2])); + } + + this.geometryCache = new ExtrusionGeometry(extrusionPaths, this.extrusionWidth, this.lineHeight, 4); + } + return this.geometryCache; + } +} diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 276d53ff..fc379a42 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -9,6 +9,9 @@ import Stats from 'three/examples/jsm/libs/stats.module.js'; import { DevGUI, DevModeOptions } from './dev-gui'; +import { Path, PathType } from './path'; +import { Interpreter } from './interpreter'; + import { AmbientLight, AxesHelper, @@ -31,7 +34,7 @@ import { WebGLRenderer } from 'three'; -import { ExtrusionGeometry } from './extrusion-geometry'; +// import { ExtrusionGeometry } from './extrusion-geometry'; type RenderLayer = { extrusion: number[]; travel: number[]; z: number; height: number }; type GVector3 = { @@ -143,7 +146,9 @@ export class WebGLPreview { private _extrusionColor: Color | Color[] = WebGLPreview.defaultExtrusionColor; private animationFrameId?: number; private renderLayerIndex = 0; - private _geometries: Record = {}; + private _geometries: Record = {}; + paths: Path[]; + interpreter: Interpreter; // colors private _backgroundColor = new Color(0xe0e0e0); @@ -183,6 +188,8 @@ export class WebGLPreview { this.extrusionWidth = opts.extrusionWidth ?? this.extrusionWidth; this.devMode = opts.devMode ?? this.devMode; this.stats = this.devMode ? new Stats() : undefined; + this.paths = []; + this.interpreter = new Interpreter(); if (opts.extrusionColor !== undefined) { this.extrusionColor = opts.extrusionColor; @@ -521,15 +528,15 @@ export class WebGLPreview { const endPoint = layer.extrusion.splice(-3); const preendPoint = layer.extrusion.splice(-3); if (this.renderTubes) { - this.addTubeLine(layer.extrusion, layerColor.getHex(), layer.height); - this.addTubeLine([...preendPoint, ...endPoint], lastSegmentColor.getHex(), layer.height); + // this.addTubeLine(layer.extrusion, layerColor.getHex(), layer.height); + // this.addTubeLine([...preendPoint, ...endPoint], lastSegmentColor.getHex(), layer.height); } else { this.addLine(layer.extrusion, layerColor.getHex()); this.addLine([...preendPoint, ...endPoint], lastSegmentColor.getHex()); } } else { if (this.renderTubes) { - this.addTubeLine(layer.extrusion, extrusionColor.getHex(), layer.height); + // this.addTubeLine(layer.extrusion, extrusionColor.getHex(), layer.height); } else { this.addLine(layer.extrusion, extrusionColor.getHex()); } @@ -577,6 +584,7 @@ export class WebGLPreview { this.state = State.initial; this.devGui?.reset(); this._geometries = {}; + this.paths = []; } resize(): void { @@ -589,6 +597,18 @@ export class WebGLPreview { /** @internal */ addLineSegment(layer: RenderLayer, p1: Point, p2: Point, extrude: boolean): void { + const lastPath = this.paths[this.paths.length - 1]; + if ( + lastPath === undefined || + !lastPath.checkLineContinuity(p1.x, p1.y, p1.z) || + lastPath.type !== (extrude ? PathType.Extrusion : PathType.Travel) + ) { + this.paths.push(new Path(extrude ? PathType.Extrusion : PathType.Travel, this.extrusionWidth, this.lineHeight)); + this.paths[this.paths.length - 1].addPoint(p1.x, p1.y, p1.z); + } + + this.paths[this.paths.length - 1].addPoint(p2.x, p2.y, p2.z); + const line = extrude ? layer.extrusion : layer.travel; line.push(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z); } @@ -712,7 +732,7 @@ export class WebGLPreview { } /** @internal */ - addTubeLine(vertices: number[], color: number, layerHeight = 0.2): void { + addTubeLine(vertices: number[]): void { let curvePoints: Vector3[] = []; const extrusionPaths: Vector3[][] = []; @@ -732,11 +752,11 @@ export class WebGLPreview { } } - extrusionPaths.forEach((extrusionPath) => { - const geometry = new ExtrusionGeometry(extrusionPath, this.extrusionWidth, this.lineHeight || layerHeight, 4); - this._geometries[color] ||= []; - this._geometries[color].push(geometry); - }); + // extrusionPaths.forEach((extrusionPath) => { + // // const geometry = new ExtrusionGeometry(extrusionPath, this.extrusionWidth, this.lineHeight || layerHeight, 4); + // // this._geometries[color] ||= []; + // // this._geometries[color].push(geometry); + // }); } /** @internal */ @@ -803,6 +823,27 @@ export class WebGLPreview { } private batchGeometries() { + if (Object.keys(this._geometries).length === 0 && this.renderTubes) { + const print = this.interpreter.execute(this.parser.commands); + console.log(this._extrusionColor); + + let color: number; + print.paths + .filter(({ type }) => { + return type === PathType.Extrusion; + }) + .forEach((path) => { + if (Array.isArray(this._extrusionColor)) { + color = this._extrusionColor[path.tool].getHex(); + } else { + color = this._extrusionColor.getHex(); + } + + this._geometries[color] ||= []; + this._geometries[color].push(path.geometry()); + }); + } + if (this._geometries) { for (const color in this._geometries) { const batchedMesh = this.createBatchMesh(parseInt(color)); From 2a099fed42d0a203a9b2acc83ea41ab0e205e8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Mon, 19 Aug 2024 15:55:27 -0400 Subject: [PATCH 02/34] arc support --- demo/js/demo.js | 613 +++++++++++++++++++++++++++++++++++++++++++ src/gcode-parser.ts | 23 +- src/interpreter.ts | 159 ++++++++--- src/path.ts | 21 +- src/webgl-preview.ts | 468 ++------------------------------- 5 files changed, 775 insertions(+), 509 deletions(-) create mode 100644 demo/js/demo.js diff --git a/demo/js/demo.js b/demo/js/demo.js new file mode 100644 index 00000000..e5fb9dc9 --- /dev/null +++ b/demo/js/demo.js @@ -0,0 +1,613 @@ +import * as GCodePreview from 'gcode-preview'; +import * as THREE from 'three'; +import * as Canvas2Image from 'canvas2image'; + +let gcodePreview; +let favIcon; +let thumb; +const maxToolCount = 8; +let toolCount = 4; +let gcode; +let renderProgressive = false; + +const canvasElement = document.querySelector('.gcode-previewer'); +const settingsPreset = document.getElementById('settings-presets'); +const startLayer = document.getElementById('start-layer'); +const startLayerValue = document.getElementById('start-layer-value'); +const endLayer = document.getElementById('end-layer'); +const endLayerValue = document.getElementById('end-layer-value'); +const lineWidth = document.getElementById('line-width'); +const lineWidthValue = document.getElementById('line-width-value'); +const extrusionWidth = document.getElementById('extrusion-width'); +const extrusionWidthValue = document.getElementById('extrusion-width-value'); +const toggleSingleLayerMode = document.getElementById('single-layer-mode'); +const toggleExtrusion = document.getElementById('extrusion'); +const toggleRenderTubes = document.getElementById('render-tubes'); +const extrusionColor = {}; +for (let i = 0; i < maxToolCount; i++) { + extrusionColor[i] = document.getElementById(`extrusion-color-t${i}`); +} +const addColorButton = document.getElementById('add-color'); +const removeColorButton = document.getElementById('remove-color'); + +const backgroundColor = document.getElementById('background-color'); +const toggleTravel = document.getElementById('travel'); +const toggleHighlight = document.getElementById('highlight'); +const topLayerColorInput = document.getElementById('top-layer-color'); +const lastSegmentColorInput = document.getElementById('last-segment-color'); +// const layerCount = document.getElementById('layer-count'); +const fileName = document.getElementById('file-name'); +const fileSelector = document.getElementById('file-selector'); +const fileSize = document.getElementById('file-size'); +const snapshot = document.getElementById('snapshot'); +const buildVolumeX = document.getElementById('buildVolumeX'); +const buildVolumeY = document.getElementById('buildVolumeY'); +const buildVolumeZ = document.getElementById('buildVolumeZ'); +const drawBuildVolume = document.getElementById('drawBuildVolume'); +const travelColor = document.getElementById('travel-color'); +const preferDarkMode = window.matchMedia('(prefers-color-scheme: dark)'); + +const defaultPreset = 'multicolor'; + +const settingsPresets = { + multicolor: { + file: 'gcodes/3DBenchy-Multi-part.gcode', + lineWidth: 1, + singleLayerMode: false, + renderExtrusion: true, + renderTubes: true, + extrusionColors: ['#CF439D', 'rgb(84,74,187)', 'white', 'rgb(83,209,104)'], + travel: false, + travelColor: '#00FF00', + highlightTopLayer: false, + topLayerColor: undefined, + lastSegmentColor: undefined, + drawBuildVolume: true, + buildVolume: { + x: 180, + y: 180, + z: 200 + } + }, + mach3: { + file: 'gcodes/mach3.gcode', + lineWidth: 1, + singleLayerMode: false, + renderExtrusion: false, + renderTubes: false, + extrusionColors: [], + travel: true, + travelColor: '#00FF00', + highlightTopLayer: false, + topLayerColor: undefined, + lastSegmentColor: undefined, + drawBuildVolume: true, + buildVolume: { + x: 20, + y: 20, + z: '' + } + }, + arcs: { + file: 'gcodes/screw.gcode', + lineWidth: 2, + singleLayerMode: true, + renderExtrusion: true, + renderTubes: true, + extrusionColors: ['rgb(83,209,104)'], + travel: false, + travelColor: '#00FF00', + highlightTopLayer: false, + topLayerColor: undefined, + lastSegmentColor: undefined, + drawBuildVolume: true, + buildVolume: { + x: 200, + y: 200, + z: 180 + } + }, + 'vase-mode': { + file: 'gcodes/vase.gcode', + lineWidth: 1, + singleLayerMode: true, + renderExtrusion: true, + renderTubes: true, + extrusionColors: ['rgb(84,74,187)'], + travel: false, + travelColor: '#00FF00', + highlightTopLayer: true, + topLayerColor: '#40BFBF', + lastSegmentColor: '#ffffff', + drawBuildVolume: true, + buildVolume: { + x: 200, + y: 200, + z: 220 + } + }, + 'travel-moves': { + file: 'gcodes/plant-sign.gcode', + lineWidth: 2, + singleLayerMode: false, + renderExtrusion: true, + renderTubes: true, + extrusionColors: ['#777777'], + travel: true, + travelColor: '#00FF00', + highlightTopLayer: true, + topLayerColor: '#aaaaaa', + lastSegmentColor: undefined, + drawBuildVolume: true, + buildVolume: { + x: 200, + y: 200, + z: 220 + } + } +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars +export function initDemo() { + // eslint-disable-line no-unused-vars, @typescript-eslint/no-unused-vars + const settings = JSON.parse(localStorage.getItem('settings')); + + const initialBackgroundColor = preferDarkMode.matches ? '#111' : '#eee'; + + const preview = (window.preview = new GCodePreview.init({ + canvas: canvasElement, + buildVolume: settings?.buildVolume || { x: 190, y: 210, z: 0 }, + initialCameraPosition: [180, 150, 300], + backgroundColor: initialBackgroundColor, + lineHeight: 0.3, + devMode: { + camera: true, + renderer: true, + parser: true, + buildVolume: true, + devHelpers: true, + statsContainer: document.querySelector('.sidebar') + } + })); + + backgroundColor.value = initialBackgroundColor; + + loadSettingPreset(defaultPreset); + + settingsPreset.addEventListener('change', function (e) { + loadSettingPreset(e.target.value); + }); + + fileSelector.addEventListener('change', function (e) { + const fileName = e.target.value; + changeFile(fileName); + }); + + startLayer.addEventListener('input', function () { + preview.startLayer = +startLayer.value; + startLayerValue.innerText = startLayer.value; + endLayer.value = preview.endLayer = Math.max(preview.startLayer, preview.endLayer); + endLayerValue.innerText = endLayer.value; + preview.render(); + }); + + endLayer.addEventListener('input', function () { + preview.endLayer = +endLayer.value; + endLayerValue.innerText = endLayer.value; + startLayer.value = preview.startLayer = Math.min(preview.startLayer, preview.endLayer); + startLayerValue.innerText = startLayer.value; + preview.render(); + }); + + lineWidth.addEventListener('input', function () { + changeLineWidth(lineWidth.value); + preview.render(); + }); + + extrusionWidth.addEventListener('input', function () { + preview.extrusionWidth = +extrusionWidth.value; + extrusionWidthValue.innerText = extrusionWidth.value; + preview.render(); + }); + + toggleSingleLayerMode.addEventListener('click', function () { + changeSingleLayerMode(!!toggleSingleLayerMode.checked); + preview.render(); + }); + + toggleExtrusion.addEventListener('click', function () { + changeRenderExtrusion(!!toggleExtrusion.checked); + preview.render(); + }); + + toggleRenderTubes.addEventListener('click', function () { + changeRenderTubes(!!toggleRenderTubes.checked); + startLoadingProgressive(gcode); + }); + + for (let i = 0; i < 8; i++) { + extrusionColor[i].addEventListener('input', () => + debounce(() => { + const colors = preview.extrusionColor; + colors[i] = extrusionColor[i].value; + preview.extrusionColor = colors; + preview.render(); + }) + ); + } + + addColorButton.addEventListener('click', function () { + if (toolCount >= maxToolCount) return; + toolCount++; + showExtrusionColors(); + }); + + removeColorButton.addEventListener('click', function () { + if (toolCount <= 1) return; + toolCount--; + showExtrusionColors(); + }); + + backgroundColor.addEventListener('input', () => + throttle(() => { + changeBackgroundColor(backgroundColor.value); + preview.render(); + }) + ); + + toggleTravel.addEventListener('click', function () { + changeRenderTravel(!!toggleTravel.checked); + preview.render(); + }); + + travelColor.addEventListener('input', () => + throttle(() => { + changeTravelColor(travelColor.value); + preview.render(); + }) + ); + + toggleHighlight.addEventListener('click', function () { + changeHighlightTopLayer(!!toggleHighlight.checked); + preview.render(); + }); + + topLayerColorInput.addEventListener('input', () => + throttle(() => { + changeTopLayerColor(topLayerColorInput.value); + preview.render(); + }) + ); + + lastSegmentColorInput.addEventListener('input', () => + throttle(() => { + changeLastSegmentColor(lastSegmentColorInput.value); + preview.render(); + }) + ); + + canvasElement.addEventListener('dragover', (evt) => { + evt.stopPropagation(); + evt.preventDefault(); + evt.dataTransfer.dropEffect = 'copy'; + canvasElement.classList.add('dragging'); + }); + + canvasElement.addEventListener('dragleave', (evt) => { + evt.stopPropagation(); + evt.preventDefault(); + canvasElement.classList.remove('dragging'); + }); + + canvasElement.addEventListener('drop', async (evt) => { + evt.stopPropagation(); + evt.preventDefault(); + preview.topLayerColor = undefined; + preview.lastSegmentColor = undefined; + canvasElement.classList.remove('dragging'); + const files = evt.dataTransfer.files; + const file = files[0]; + + fileName.innerText = file.name; + fileSize.innerText = humanFileSize(file.size); + + // await preview._readFromStream(file.stream()); + _handleGCode(file.name, await file.text()); + updateUI(); + }); + + function updateBuildVolume() { + const x = parseInt(buildVolumeX.value, 10); + const y = parseInt(buildVolumeY.value, 10); + const z = parseInt(buildVolumeZ.value, 10); + + const draw = !!drawBuildVolume.checked; + + changeDrawBuildVolume(draw); + changeBuildVolume({ x, y, z }); + + preview.render(); + + storeSettings(); + } + + buildVolumeX.addEventListener('input', updateBuildVolume); + buildVolumeY.addEventListener('input', updateBuildVolume); + buildVolumeZ.addEventListener('input', updateBuildVolume); + drawBuildVolume.addEventListener('input', updateBuildVolume); + + // lineWidth.addEventListener('change', function() { + // preview.lineWidth = parseInt(lineWidth.value,10); + // preview.render(); + // }); + + window.addEventListener('resize', function () { + preview.resize(); + }); + + snapshot.addEventListener('click', function (evt) { + evt.stopPropagation(); + evt.preventDefault(); + + Canvas2Image.saveAsJPEG(gcodePreview.canvas, innerWidth, innerHeight, fileName.innerText.replace('.gcode', '.jpg')); + }); + + function changeFile(name) { + fileSelector.value = name; + loadGCodeFromServer(name); + } + + function changeLineWidth(width) { + lineWidthValue.innerText = parseInt(width, 10); + lineWidth.value = parseInt(width, 10); + preview.lineWidth = parseInt(width, 10); + } + + function changeSingleLayerMode(enabled) { + preview.singleLayerMode = enabled; + toggleSingleLayerMode.checked = enabled; + if (preview.singleLayerMode) { + startLayer.setAttribute('disabled', 'disabled'); + } else { + startLayer.removeAttribute('disabled'); + } + } + + function changeRenderExtrusion(enabled) { + preview.renderExtrusion = enabled; + toggleExtrusion.checked = enabled; + if (enabled) { + for (let i = 0; i < 8; i++) { + extrusionColor[i].removeAttribute('disabled'); + } + toggleRenderTubes.removeAttribute('disabled'); + } else { + for (let i = 0; i < 8; i++) { + extrusionColor[i].setAttribute('disabled', 'disabled'); + } + toggleRenderTubes.setAttribute('disabled', 'disabled'); + } + } + + function changeRenderTubes(enabled) { + preview.renderTubes = enabled; + toggleRenderTubes.checked = enabled; + } + + function changeRenderTravel(enabled) { + preview.renderTravel = enabled; + toggleTravel.checked = enabled; + if (enabled) { + travelColor.removeAttribute('disabled'); + } else { + travelColor.setAttribute('disabled', 'disabled'); + } + } + + function changeHighlightTopLayer(enabled) { + toggleHighlight.checked = enabled; + if (enabled) { + changeTopLayerColor(preview.topLayerColor || '#40BFBF'); + changeLastSegmentColor(preview.lastSegmentColor || '#ffffff'); + topLayerColorInput.removeAttribute('disabled'); + lastSegmentColorInput.removeAttribute('disabled'); + } else { + preview.topLayerColor = undefined; + preview.lastSegmentColor = undefined; + topLayerColorInput.setAttribute('disabled', 'disabled'); + lastSegmentColorInput.setAttribute('disabled', 'disabled'); + } + } + + function changeTravelColor(color) { + preview.travelColor = color; + travelColor.value = color; + } + + function changeBackgroundColor(color) { + preview.backgroundColor = color; + backgroundColor.value = color; + } + + function changeTopLayerColor(color) { + topLayerColorInput.value = color; + preview.topLayerColor = color; + } + + function changeLastSegmentColor(color) { + lastSegmentColorInput.value = color; + preview.lastSegmentColor = color; + } + + function changeBuildVolume(volume) { + buildVolumeX.value = volume.x; + buildVolumeY.value = volume.y; + buildVolumeZ.value = volume.z; + preview.buildVolume.x = volume.x; + preview.buildVolume.y = volume.y; + preview.buildVolume.z = volume.z; + } + + function changeDrawBuildVolume(draw) { + drawBuildVolume.checked = draw; + if (draw) { + buildVolumeX.removeAttribute('disabled'); + buildVolumeY.removeAttribute('disabled'); + buildVolumeZ.removeAttribute('disabled'); + } else { + buildVolumeX.setAttribute('disabled', 'disabled'); + buildVolumeY.setAttribute('disabled', 'disabled'); + buildVolumeZ.setAttribute('disabled', 'disabled'); + } + } + + function changeToolColors(colors) { + toolCount = colors.length; + for (let i = 0; i < toolCount; i++) extrusionColor[i].value = '#' + new THREE.Color(colors[i]).getHexString(); + preview.extrusionColor = colors; + showExtrusionColors(); + } + + function loadSettingPreset(name) { + const preset = settingsPresets[name]; + changeLineWidth(preset.lineWidth); + changeSingleLayerMode(preset.singleLayerMode); + changeRenderExtrusion(preset.renderExtrusion); + changeRenderTubes(preset.renderTubes); + changeRenderTravel(preset.travel); + changeHighlightTopLayer(preset.highlightTopLayer); + changeTravelColor(preset.travelColor); + changeTopLayerColor(preset.topLayerColor); + changeLastSegmentColor(preset.lastSegmentColor); + changeDrawBuildVolume(preset.drawBuildVolume); + changeBuildVolume(preset.buildVolume); + changeToolColors(preset.extrusionColors); + changeFile(preset.file); + } + + gcodePreview = preview; + + updateUI(); + + return preview; +} + +function storeSettings() { + localStorage.setItem( + 'settings', + JSON.stringify({ + buildVolume: { + x: gcodePreview.buildVolume.x, + y: gcodePreview.buildVolume.y, + z: gcodePreview.buildVolume.z + } + }) + ); +} + +function updateUI() { + // startLayer.setAttribute('max', gcodePreview.layers.length); + // endLayer.setAttribute('max', gcodePreview.layers.length); + // endLayer.value = gcodePreview.layers.length; + // endLayerValue.innerText = endLayer.value; + + startLayerValue.innerText = startLayer.value; + + // layerCount.innerText = gcodePreview.layers && gcodePreview.layers.length + ' layers'; + + if (favIcon != gcodePreview.parser.metadata.thumbnails['16x16']) { + favIcon = gcodePreview.parser.metadata.thumbnails['16x16']; + setFavicons(favIcon?.src); + } + + if (thumb != gcodePreview.parser.metadata.thumbnails['220x124']) { + thumb = gcodePreview.parser.metadata.thumbnails['220x124']; + document.getElementById('thumb').src = thumb?.src ?? 'https://via.placeholder.com/120x60?text=noThumbnail'; + } + + showExtrusionColors(); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars +async function loadGCodeFromServer(filename) { + const response = await fetch(filename); + if (response.status !== 200) { + console.error('ERROR. Status Code: ' + response.status); + return; + } + + const gcode = await response.text(); + _handleGCode(filename, gcode); + fileName.setAttribute('href', filename); +} + +function _handleGCode(filename, text) { + gcode = text; + fileName.innerText = filename; + fileSize.innerText = humanFileSize(text.length); + + updateUI(); + + startLoadingProgressive(text); +} + +async function startLoadingProgressive(gcode) { + startLayer.setAttribute('disabled', 'disabled'); + endLayer.setAttribute('disabled', 'disabled'); + + gcodePreview.clear(); + if (renderProgressive) { + gcodePreview.parser.parseGCode(gcode); + updateUI(); + // await gcodePreview.renderAnimated(Math.ceil(gcodePreview.layers.length / 60)); + } else { + gcodePreview.processGCode(gcode); + } + updateUI(); + + startLayer.removeAttribute('disabled'); + endLayer.removeAttribute('disabled'); +} + +function humanFileSize(size) { + var i = Math.floor(Math.log(size) / Math.log(1024)); + return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; +} + +function setFavicons(favImg) { + const headTitle = document.querySelector('head'); + const setFavicon = document.createElement('link'); + setFavicon.setAttribute('rel', 'shortcut icon'); + setFavicon.setAttribute('href', favImg); + headTitle.appendChild(setFavicon); +} + +let throttleTimer; +const throttle = (callback, time) => { + if (throttleTimer) return; + throttleTimer = true; + setTimeout(() => { + callback(); + throttleTimer = false; + }, time); +}; + +// debounce function +let debounceTimer; +const debounce = (callback) => { + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(callback, 300); +}; + +function showExtrusionColors() { + // loop through inputs and show/hide them + for (let i = 0; i < 8; i++) { + // find parent element + const parent = extrusionColor[i].parentNode; + if (i < toolCount) { + parent.style.display = 'flex'; + } else { + parent.style.display = 'none'; + } + } +} diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index b0e028f9..f0b8e2fd 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -66,6 +66,7 @@ export enum Code { G1 = 'G1', G2 = 'G2', G3 = 'G3', + G28 = 'G28', T0 = 'T0', T1 = 'T1', T2 = 'T2', @@ -182,6 +183,7 @@ export class Parser { parseGCode(input: string | string[]): { layers: Layer[]; metadata: Metadata; + commands: GCodeCommand[]; } { const lines = Array.isArray(input) ? input : input.split('\n'); @@ -189,7 +191,7 @@ export class Parser { this.commands = this.lines2commands(lines); - this.groupIntoLayers(this.commands); + // this.groupIntoLayers(this.commands); // merge thumbs const thumbs = this.parseMetadata(this.commands.filter((cmd) => cmd.comment)).thumbnails; @@ -197,7 +199,7 @@ export class Parser { this.metadata.thumbnails[key] = value; } - return { layers: this.layers, metadata: this.metadata }; + return { layers: this.layers, metadata: this.metadata, commands: this.commands }; } private lines2commands(lines: string[]) { @@ -248,16 +250,6 @@ export class Parser { } } - // G0 & G1 - private parseMove(params: string[]): MoveCommandParams { - return params.reduce((acc: MoveCommandParams, cur: string) => { - const key = cur.charAt(0).toLowerCase(); - if (key == 'x' || key == 'y' || key == 'z' || key == 'e' || key == 'r' || key == 'f' || key == 'i' || key == 'j') - acc[key] = parseFloat(cur.slice(1)); - return acc; - }, {}); - } - private isAlpha(char: string | singleLetter): char is singleLetter { const code = char.charCodeAt(0); return (code >= 97 && code <= 122) || (code >= 65 && code <= 90); @@ -337,10 +329,3 @@ export class Parser { return { thumbnails }; } } - -// backwards compat; -// eslint-disable-next-line no-redeclare -export interface Parser { - parseGcode: typeof Parser.prototype.parseGCode; -} -Parser.prototype.parseGcode = Parser.prototype.parseGCode; diff --git a/src/interpreter.ts b/src/interpreter.ts index acd82eed..db5081d7 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1,5 +1,5 @@ import { Path, PathType } from './path'; -import { GCodeCommand, SelectToolCommand } from './gcode-parser'; +import { Code, GCodeCommand, SelectToolCommand } from './gcode-parser'; export class State { x: number; @@ -18,7 +18,7 @@ export class State { } } -class Print { +export class Machine { paths: Path[]; state: State; @@ -29,28 +29,26 @@ class Print { } export class Interpreter { - execute(commands: GCodeCommand[], initialState?: State): Print { - const print = new Print(initialState); - + execute(commands: GCodeCommand[], machine = new Machine()): Machine { commands.forEach((command) => { if (command.code !== undefined) { - this[command.code](command, print); + this[command.code](command, machine); } }); - return print; + return machine; } - G0(command: GCodeCommand, print: Print): void { + G0(command: GCodeCommand, machine: Machine): void { const { x, y, z, e } = command.params; - const { state } = print; + const { state } = machine; - let lastPath = print.paths[print.paths.length - 1]; + let lastPath = machine.paths[machine.paths.length - 1]; const pathType = e ? PathType.Extrusion : PathType.Travel; if (lastPath === undefined || lastPath.type !== pathType) { lastPath = new Path(pathType, 0.6, 0.2, state.t); - print.paths.push(lastPath); + machine.paths.push(lastPath); lastPath.addPoint(state.x, state.y, state.z); } @@ -62,35 +60,134 @@ export class Interpreter { } G1 = this.G0; - G2(command: GCodeCommand, print: Print): void { - console.log(command.src, print); + + G2(command: GCodeCommand, machine: Machine): void { + const { x, y, z, e } = command.params; + let { i, j, r } = command.params; + const { state } = machine; + + const cw = command.code === Code.G2; + let lastPath = machine.paths[machine.paths.length - 1]; + const pathType = e ? PathType.Extrusion : PathType.Travel; + + if (lastPath === undefined || lastPath.type !== pathType) { + lastPath = new Path(pathType, 0.6, 0.2, state.t); + machine.paths.push(lastPath); + lastPath.addPoint(state.x, state.y, state.z); + } + + if (r) { + // in r mode a minimum radius will be applied if the distance can otherwise not be bridged + const deltaX = x - state.x; // assume abs mode + const deltaY = y - state.y; + + // apply a minimal radius to bridge the distance + const minR = Math.sqrt(Math.pow(deltaX / 2, 2) + Math.pow(deltaY / 2, 2)); + r = Math.max(r, minR); + + const dSquared = Math.pow(deltaX, 2) + Math.pow(deltaY, 2); + const hSquared = Math.pow(r, 2) - dSquared / 4; + // if (dSquared == 0 || hSquared < 0) { + // return { position: { x: x, y: z, z: y }, points: [] }; //we'll abort the render and move te position to the new position. + // } + let hDivD = Math.sqrt(hSquared / dSquared); + + // Ref RRF DoArcMove for details + if ((cw && r < 0.0) || (!cw && r > 0.0)) { + hDivD = -hDivD; + } + i = deltaX / 2 + deltaY * hDivD; + j = deltaY / 2 - deltaX * hDivD; + // } else { + // //the radial point is an offset from the current position + // ///Need at least on point + // if (i == 0 && j == 0) { + // return { position: { x: x, y: y, z: z }, points: [] }; //we'll abort the render and move te position to the new position. + // } + } + + const wholeCircle = state.x == x && state.y == y; + const centerX = state.x + i; + const centerY = state.y + j; + + const arcRadius = Math.sqrt(i * i + j * j); + const arcCurrentAngle = Math.atan2(-j, -i); + const finalTheta = Math.atan2(y - centerY, x - centerX); + + let totalArc; + if (wholeCircle) { + totalArc = 2 * Math.PI; + } else { + totalArc = cw ? arcCurrentAngle - finalTheta : finalTheta - arcCurrentAngle; + if (totalArc < 0.0) { + totalArc += 2 * Math.PI; + } + } + let totalSegments = (arcRadius * totalArc) / 1.8; + // if (this.inches) { + // totalSegments *= 25; + // } + if (totalSegments < 1) { + totalSegments = 1; + } + let arcAngleIncrement = totalArc / totalSegments; + arcAngleIncrement *= cw ? -1 : 1; + + const zDist = state.z - (z || state.z); + const zStep = zDist / totalSegments; + + // get points for the arc + let px = state.x; + let py = state.y; + let pz = state.z; + // calculate segments + let currentAngle = arcCurrentAngle; + + for (let moveIdx = 0; moveIdx < totalSegments - 1; moveIdx++) { + currentAngle += arcAngleIncrement; + px = centerX + arcRadius * Math.cos(currentAngle); + py = centerY + arcRadius * Math.sin(currentAngle); + pz += zStep; + lastPath.addPoint(px, py, pz); + } + + state.x = x || state.x; + state.y = y || state.y; + state.z = z || state.z; + + lastPath.addPoint(state.x, state.y, state.z); } - G3(command: GCodeCommand, print: Print): void { - console.log(command.src, print); + + G3 = this.G2; + + G28(command: GCodeCommand, machine: Machine): void { + machine.state.x = 0; + machine.state.y = 0; + machine.state.z = 0; } - T0(command: SelectToolCommand, print: Print): void { - print.state.t = 0; + T0(command: SelectToolCommand, machine: Machine): void { + machine.state.t = 0; } - T1(command: SelectToolCommand, print: Print): void { - print.state.t = 1; + T1(command: SelectToolCommand, machine: Machine): void { + machine.state.t = 1; } - T2(command: SelectToolCommand, print: Print): void { - print.state.t = 2; + T2(command: SelectToolCommand, machine: Machine): void { + machine.state.t = 2; } - T3(command: SelectToolCommand, print: Print): void { - print.state.t = 3; + T3(command: SelectToolCommand, machine: Machine): void { + machine.state.t = 3; } - T4(command: SelectToolCommand, print: Print): void { - print.state.t = 4; + T4(command: SelectToolCommand, machine: Machine): void { + machine.state.t = 4; } - T5(command: SelectToolCommand, print: Print): void { - print.state.t = 5; + T5(command: SelectToolCommand, machine: Machine): void { + machine.state.t = 5; } - T6(command: SelectToolCommand, print: Print): void { - print.state.t = 6; + T6(command: SelectToolCommand, machine: Machine): void { + machine.state.t = 6; } - T7(command: SelectToolCommand, print: Print): void { - print.state.t = 7; + T7(command: SelectToolCommand, machine: Machine): void { + machine.state.t = 7; } } diff --git a/src/path.ts b/src/path.ts index f88116a4..69e90657 100644 --- a/src/path.ts +++ b/src/path.ts @@ -39,20 +39,27 @@ export class Path { return x === lastX && y === lastY && z === lastZ; } + path(): Vector3[] { + const path: Vector3[] = []; + + for (let i = 0; i < this.vertices.length; i += 3) { + path.push(new Vector3(this.vertices[i], this.vertices[i + 1], this.vertices[i + 2])); + } + return path; + } + geometry(): BufferGeometry { if (!this.geometryCache) { if (this.vertices.length < 3) { return new BufferGeometry(); } - const extrusionPaths: Vector3[] = []; - - for (let i = 0; i < this.vertices.length; i += 3) { - extrusionPaths.push(new Vector3(this.vertices[i], this.vertices[i + 1], this.vertices[i + 2])); - } - - this.geometryCache = new ExtrusionGeometry(extrusionPaths, this.extrusionWidth, this.lineHeight, 4); + this.geometryCache = new ExtrusionGeometry(this.path(), this.extrusionWidth, this.lineHeight, 4); } return this.geometryCache; } + + line(): BufferGeometry { + return new BufferGeometry().setFromPoints(this.path()); + } } diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index fc379a42..90a1fbea 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -1,8 +1,5 @@ -import { Parser, MoveCommand, Layer, SelectToolCommand } from './gcode-parser'; +import { Parser } from './gcode-parser'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; -import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'; -import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js'; -import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js'; import { GridHelper } from './gridHelper'; import { LineBox } from './lineBox'; import Stats from 'three/examples/jsm/libs/stats.module.js'; @@ -10,7 +7,7 @@ import Stats from 'three/examples/jsm/libs/stats.module.js'; import { DevGUI, DevModeOptions } from './dev-gui'; import { Path, PathType } from './path'; -import { Interpreter } from './interpreter'; +import { Interpreter, Machine } from './interpreter'; import { AmbientLight, @@ -20,48 +17,21 @@ import { Color, ColorRepresentation, Euler, - Float32BufferAttribute, Fog, Group, - LineBasicMaterial, - LineSegments, MeshLambertMaterial, PerspectiveCamera, PointLight, REVISION, Scene, - Vector3, WebGLRenderer } from 'three'; -// import { ExtrusionGeometry } from './extrusion-geometry'; - -type RenderLayer = { extrusion: number[]; travel: number[]; z: number; height: number }; -type GVector3 = { +type BuildVolume = { x: number; y: number; z: number; }; -type Arc = GVector3 & { r: number; i: number; j: number }; - -type Point = GVector3; -type BuildVolume = GVector3; -export class State { - x: number; - y: number; - z: number; - r: number; - e: number; - i: number; - j: number; - t: number; // tool index - // feedrate? - static get initial(): State { - const state = new State(); - Object.assign(state, { x: 0, y: 0, z: 0, r: 0, e: 0, i: 0, j: 0, t: 0 }); - return state; - } -} export type GCodePreviewOptions = { buildVolume?: BuildVolume; @@ -98,12 +68,6 @@ export type GCodePreviewOptions = { devMode?: boolean | DevModeOptions; }; -const target = { - h: 0, - s: 0, - l: 0 -}; - export class WebGLPreview { minLayerThreshold = 0.05; parser: Parser; @@ -135,10 +99,6 @@ export class WebGLPreview { nonTravelmoves: string[] = []; disableGradient = false; - // gcode processing state - private state: State = State.initial; - private beyondFirstMove = false; // TODO: move to state - // rendering private group?: Group; private disposables: { dispose(): void }[] = []; @@ -149,6 +109,7 @@ export class WebGLPreview { private _geometries: Record = {}; paths: Path[]; interpreter: Interpreter; + virtualMachine: Machine = new Machine(); // colors private _backgroundColor = new Color(0xe0e0e0); @@ -251,9 +212,6 @@ export class WebGLPreview { this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.initScene(); this.animate(); - - if (opts.allowDragNDrop) this._enableDropHandler(); - this.initStats(); } @@ -272,18 +230,6 @@ export class WebGLPreview { this._extrusionColor = new Color(value); } - // get tool color based on current state - get currentToolColor(): Color { - if (this._extrusionColor === undefined) { - return WebGLPreview.defaultExtrusionColor; - } - if (this._extrusionColor instanceof Color) { - return this._extrusionColor; - } - - return this._extrusionColor[this.state.t] ?? WebGLPreview.defaultExtrusionColor; - } - get backgroundColor(): Color { return this._backgroundColor; } @@ -314,23 +260,6 @@ export class WebGLPreview { this._lastSegmentColor = value !== undefined ? new Color(value) : undefined; } - /** - * @internal Do not use externally. - */ - get layers(): Layer[] { - return [this.parser.preamble].concat(this.parser.layers.concat()); - } - - // convert from 1-based to 0-based - get maxLayerIndex(): number { - return (this.endLayer ?? this.layers.length) - 1; - } - - // convert from 1-based to 0-based - get minLayerIndex(): number { - return this.singleLayerMode ? this.maxLayerIndex : (this.startLayer ?? 0) - 1; - } - /** @internal */ animate(): void { this.animationFrameId = requestAnimationFrame(() => this.animate()); @@ -340,7 +269,9 @@ export class WebGLPreview { } processGCode(gcode: string | string[]): void { - this.parser.parseGCode(gcode); + console.log('asdf'); + const { commands } = this.parser.parseGCode(gcode); + this.interpreter.execute(commands, this.virtualMachine); this.render(); } @@ -390,170 +321,21 @@ export class WebGLPreview { render(): void { const startRender = performance.now(); this.group = this.createGroup('allLayers'); - this.state = State.initial; this.initScene(); - for (let index = 0; index < this.layers.length; index++) { - this.renderLayer(index); - } - - this.batchGeometries(); + this.renderGeometries(); this.scene.add(this.group); this.renderer.render(this.scene, this.camera); this._lastRenderTime = performance.now() - startRender; } - // create a new render method to use an animation loop to render the layers incrementally - /** @experimental */ - async renderAnimated(layerCount = 1): Promise { - this.initScene(); - - this.renderLayerIndex = 0; - return this.renderFrameLoop(layerCount > 0 ? layerCount : 1); - } - - private renderFrameLoop(layerCount: number): Promise { - return new Promise((resolve) => { - const loop = () => { - if (this.renderLayerIndex > this.layers.length - 1) { - resolve(); - } else { - this.renderFrame(layerCount); - requestAnimationFrame(loop); - } - }; - loop(); - }); - } - - private renderFrame(layerCount: number): void { - this.group = this.createGroup('layer' + this.renderLayerIndex); - - for (let l = 0; l < layerCount && this.renderLayerIndex + l < this.layers.length; l++) { - this.renderLayer(this.renderLayerIndex); - this.renderLayerIndex++; - } - - this.batchGeometries(); - - this.scene.add(this.group); - } - - /** - * @internal - */ - renderLayer(index: number): void { - if (index > this.maxLayerIndex) return; - const l = this.layers[index]; - - const currentLayer: RenderLayer = { - extrusion: [], - travel: [], - z: this.state.z, - height: l.height - }; - - for (const cmd of l.commands) { - if (cmd.gcode == 'g20') { - this.setInches(); - continue; - } - - if (cmd.gcode.startsWith('t')) { - // flush render queue - this.doRenderExtrusion(currentLayer, index); - currentLayer.extrusion = []; - - const tool = cmd as SelectToolCommand; - this.state.t = tool.toolIndex; - continue; - } - - if (['g0', 'g00', 'g1', 'g01', 'g2', 'g02', 'g3', 'g03'].indexOf(cmd.gcode) > -1) { - const g = cmd as MoveCommand; - const next: State = { - x: g.params.x ?? this.state.x, - y: g.params.y ?? this.state.y, - z: g.params.z ?? this.state.z, - r: g.params.r ?? this.state.r, - e: g.params.e ?? this.state.e, - i: g.params.i ?? this.state.i, - j: g.params.j ?? this.state.j, - t: this.state.t - }; - - if (index >= this.minLayerIndex) { - const extrude = (g.params.e ?? 0) > 0 || this.nonTravelmoves.indexOf(cmd.gcode) > -1; - const moving = next.x != this.state.x || next.y != this.state.y || next.z != this.state.z; - if (moving) { - if ((extrude && this.renderExtrusion) || (!extrude && this.renderTravel)) { - if (cmd.gcode == 'g2' || cmd.gcode == 'g3' || cmd.gcode == 'g02' || cmd.gcode == 'g03') { - this.addArcSegment(currentLayer, this.state, next, extrude, cmd.gcode == 'g2' || cmd.gcode == 'g02'); - } else { - this.addLineSegment(currentLayer, this.state, next, extrude); - } - } - } - } - - // update this.state - this.state.x = next.x; - this.state.y = next.y; - this.state.z = next.z; - // if (next.e) state.e = next.e; // where not really tracking e as distance (yet) but we only check if some commands are extruding (positive e) - if (!this.beyondFirstMove) this.beyondFirstMove = true; - } - } - - this.doRenderExtrusion(currentLayer, index); - } - - /** @internal */ - doRenderExtrusion(layer: RenderLayer, index: number): void { - if (this.renderExtrusion) { - let extrusionColor = this.currentToolColor; - - if (!this.singleLayerMode && !this.renderTubes && !this.disableGradient) { - const brightness = 0.1 + (0.7 * index) / this.layers.length; - - extrusionColor.getHSL(target); - extrusionColor = new Color().setHSL(target.h, target.s, brightness); - } - - if (index == this.layers.length - 1) { - const layerColor = this._topLayerColor ?? extrusionColor; - const lastSegmentColor = this._lastSegmentColor ?? layerColor; - - const endPoint = layer.extrusion.splice(-3); - const preendPoint = layer.extrusion.splice(-3); - if (this.renderTubes) { - // this.addTubeLine(layer.extrusion, layerColor.getHex(), layer.height); - // this.addTubeLine([...preendPoint, ...endPoint], lastSegmentColor.getHex(), layer.height); - } else { - this.addLine(layer.extrusion, layerColor.getHex()); - this.addLine([...preendPoint, ...endPoint], lastSegmentColor.getHex()); - } - } else { - if (this.renderTubes) { - // this.addTubeLine(layer.extrusion, extrusionColor.getHex(), layer.height); - } else { - this.addLine(layer.extrusion, extrusionColor.getHex()); - } - } - } - - if (this.renderTravel) { - this.addLine(layer.travel, this._travelColor.getHex()); - } - } - setInches(): void { - if (this.beyondFirstMove) { - console.warn('Switching units after movement is already made is discouraged and is not supported.'); - return; - } - this.inches = true; + // if (this.beyondFirstMove) { + // console.warn('Switching units after movement is already made is discouraged and is not supported.'); + // return; + // } + // this.inches = true; } /** @internal */ @@ -572,6 +354,7 @@ export class WebGLPreview { clear(): void { this.resetState(); this.parser = new Parser(this.minLayerThreshold); + this.virtualMachine = new Machine(); } // reset processing state @@ -579,9 +362,6 @@ export class WebGLPreview { this.startLayer = 1; this.endLayer = Infinity; this.singleLayerMode = false; - - this.beyondFirstMove = false; - this.state = State.initial; this.devGui?.reset(); this._geometries = {}; this.paths = []; @@ -595,189 +375,6 @@ export class WebGLPreview { this.renderer.setSize(w, h, false); } - /** @internal */ - addLineSegment(layer: RenderLayer, p1: Point, p2: Point, extrude: boolean): void { - const lastPath = this.paths[this.paths.length - 1]; - if ( - lastPath === undefined || - !lastPath.checkLineContinuity(p1.x, p1.y, p1.z) || - lastPath.type !== (extrude ? PathType.Extrusion : PathType.Travel) - ) { - this.paths.push(new Path(extrude ? PathType.Extrusion : PathType.Travel, this.extrusionWidth, this.lineHeight)); - this.paths[this.paths.length - 1].addPoint(p1.x, p1.y, p1.z); - } - - this.paths[this.paths.length - 1].addPoint(p2.x, p2.y, p2.z); - - const line = extrude ? layer.extrusion : layer.travel; - line.push(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z); - } - - /** @internal */ - addArcSegment(layer: RenderLayer, p1: Point, p2: Arc, extrude: boolean, cw: boolean): void { - const line = extrude ? layer.extrusion : layer.travel; - - const currX = p1.x, - currY = p1.y, - currZ = p1.z, - x = p2.x, - y = p2.y, - z = p2.z; - let r = p2.r; - - let i = p2.i, - j = p2.j; - - if (r) { - // in r mode a minimum radius will be applied if the distance can otherwise not be bridged - const deltaX = x - currX; // assume abs mode - const deltaY = y - currY; - - // apply a minimal radius to bridge the distance - const minR = Math.sqrt(Math.pow(deltaX / 2, 2) + Math.pow(deltaY / 2, 2)); - r = Math.max(r, minR); - - const dSquared = Math.pow(deltaX, 2) + Math.pow(deltaY, 2); - const hSquared = Math.pow(r, 2) - dSquared / 4; - // if (dSquared == 0 || hSquared < 0) { - // return { position: { x: x, y: z, z: y }, points: [] }; //we'll abort the render and move te position to the new position. - // } - let hDivD = Math.sqrt(hSquared / dSquared); - - // Ref RRF DoArcMove for details - if ((cw && r < 0.0) || (!cw && r > 0.0)) { - hDivD = -hDivD; - } - i = deltaX / 2 + deltaY * hDivD; - j = deltaY / 2 - deltaX * hDivD; - // } else { - // //the radial point is an offset from the current position - // ///Need at least on point - // if (i == 0 && j == 0) { - // return { position: { x: x, y: y, z: z }, points: [] }; //we'll abort the render and move te position to the new position. - // } - } - - const wholeCircle = currX == x && currY == y; - const centerX = currX + i; - const centerY = currY + j; - - const arcRadius = Math.sqrt(i * i + j * j); - const arcCurrentAngle = Math.atan2(-j, -i); - const finalTheta = Math.atan2(y - centerY, x - centerX); - - let totalArc; - if (wholeCircle) { - totalArc = 2 * Math.PI; - } else { - totalArc = cw ? arcCurrentAngle - finalTheta : finalTheta - arcCurrentAngle; - if (totalArc < 0.0) { - totalArc += 2 * Math.PI; - } - } - let totalSegments = (arcRadius * totalArc) / 1.8; - if (this.inches) { - totalSegments *= 25; - } - if (totalSegments < 1) { - totalSegments = 1; - } - let arcAngleIncrement = totalArc / totalSegments; - arcAngleIncrement *= cw ? -1 : 1; - - const points = []; - - points.push({ x: currX, y: currY, z: currZ }); - - const zDist = currZ - z; - const zStep = zDist / totalSegments; - - // get points for the arc - let px = currX; - let py = currY; - let pz = currZ; - // calculate segments - let currentAngle = arcCurrentAngle; - - for (let moveIdx = 0; moveIdx < totalSegments - 1; moveIdx++) { - currentAngle += arcAngleIncrement; - px = centerX + arcRadius * Math.cos(currentAngle); - py = centerY + arcRadius * Math.sin(currentAngle); - pz += zStep; - points.push({ x: px, y: py, z: pz }); - } - - points.push({ x: p2.x, y: p2.y, z: p2.z }); - - for (let idx = 0; idx < points.length - 1; idx++) { - line.push(points[idx].x, points[idx].y, points[idx].z, points[idx + 1].x, points[idx + 1].y, points[idx + 1].z); - } - } - - /** @internal */ - addLine(vertices: number[], color: number): void { - if (typeof this.lineWidth === 'number' && this.lineWidth > 0) { - this.addThickLine(vertices, color); - return; - } - - const geometry = new BufferGeometry(); - geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3)); - this.disposables.push(geometry); - const material = new LineBasicMaterial({ color: color }); - this.disposables.push(material); - const lineSegments = new LineSegments(geometry, material); - - this.group?.add(lineSegments); - } - - /** @internal */ - addTubeLine(vertices: number[]): void { - let curvePoints: Vector3[] = []; - const extrusionPaths: Vector3[][] = []; - - // Merging into one curve for performance - for (let i = 0; i < vertices.length; i += 6) { - const v = vertices.slice(i, i + 9); - const startPoint = new Vector3(v[0], v[1], v[2]); - const endPoint = new Vector3(v[3], v[4], v[5]); - const nextPoint = new Vector3(v[6], v[7], v[8]); - - curvePoints.push(startPoint); - - if (!endPoint.equals(nextPoint)) { - curvePoints.push(endPoint); - extrusionPaths.push(curvePoints); - curvePoints = []; - } - } - - // extrusionPaths.forEach((extrusionPath) => { - // // const geometry = new ExtrusionGeometry(extrusionPath, this.extrusionWidth, this.lineHeight || layerHeight, 4); - // // this._geometries[color] ||= []; - // // this._geometries[color].push(geometry); - // }); - } - - /** @internal */ - addThickLine(vertices: number[], color: number): void { - if (!vertices.length || !this.lineWidth) return; - - const geometry = new LineSegmentsGeometry(); - this.disposables.push(geometry); - - const matLine = new LineMaterial({ - color: color, - linewidth: this.lineWidth / (1000 * window.devicePixelRatio) - }); - this.disposables.push(matLine); - - geometry.setPositions(vertices); - const line = new LineSegments2(geometry, matLine); - - this.group?.add(line); - } - dispose(): void { this.disposables.forEach((d) => d.dispose()); this.disposables = []; @@ -792,43 +389,10 @@ export class WebGLPreview { this.animationFrameId = undefined; } - private _enableDropHandler() { - console.warn('Drag and drop is deprecated as a library feature. See the demo how to implement your own.'); - this.canvas.addEventListener('dragover', (evt) => { - evt.stopPropagation(); - evt.preventDefault(); - if (evt.dataTransfer) evt.dataTransfer.dropEffect = 'copy'; - this.canvas.classList.add('dragging'); - }); - - this.canvas.addEventListener('dragleave', (evt) => { - evt.stopPropagation(); - evt.preventDefault(); - this.canvas.classList.remove('dragging'); - }); - - this.canvas.addEventListener('drop', async (evt) => { - evt.stopPropagation(); - evt.preventDefault(); - this.canvas.classList.remove('dragging'); - const files: FileList | [] = evt.dataTransfer?.files ?? []; - const file = files[0]; - - this.clear(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await this._readFromStream(file.stream() as unknown as ReadableStream); - this.render(); - }); - } - - private batchGeometries() { + private renderGeometries() { if (Object.keys(this._geometries).length === 0 && this.renderTubes) { - const print = this.interpreter.execute(this.parser.commands); - console.log(this._extrusionColor); - let color: number; - print.paths + this.virtualMachine.paths .filter(({ type }) => { return type === PathType.Extrusion; }) From 1c96b0dcd2abc290a8f441bb94cf08ed8e0c71b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Mon, 19 Aug 2024 16:12:32 -0400 Subject: [PATCH 03/34] units --- src/gcode-parser.ts | 2 ++ src/interpreter.ts | 40 ++++++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index f0b8e2fd..6c0a2f28 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -66,6 +66,8 @@ export enum Code { G1 = 'G1', G2 = 'G2', G3 = 'G3', + G20 = 'G20', + G21 = 'G21', G28 = 'G28', T0 = 'T0', T1 = 'T1', diff --git a/src/interpreter.ts b/src/interpreter.ts index db5081d7..040b7c22 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -5,15 +5,13 @@ export class State { x: number; y: number; z: number; - r: number; e: number; - i: number; - j: number; - t: number; + tool: number; + units: 'mm' | 'in'; static get initial(): State { const state = new State(); - Object.assign(state, { x: 0, y: 0, z: 0, r: 0, e: 0, i: 0, j: 0, t: 0 }); + Object.assign(state, { x: 0, y: 0, z: 0, e: 0, tool: 0, units: 'mm' }); return state; } } @@ -47,7 +45,7 @@ export class Interpreter { const pathType = e ? PathType.Extrusion : PathType.Travel; if (lastPath === undefined || lastPath.type !== pathType) { - lastPath = new Path(pathType, 0.6, 0.2, state.t); + lastPath = new Path(pathType, 0.6, 0.2, state.tool); machine.paths.push(lastPath); lastPath.addPoint(state.x, state.y, state.z); } @@ -71,7 +69,7 @@ export class Interpreter { const pathType = e ? PathType.Extrusion : PathType.Travel; if (lastPath === undefined || lastPath.type !== pathType) { - lastPath = new Path(pathType, 0.6, 0.2, state.t); + lastPath = new Path(pathType, 0.6, 0.2, state.tool); machine.paths.push(lastPath); lastPath.addPoint(state.x, state.y, state.z); } @@ -124,9 +122,9 @@ export class Interpreter { } } let totalSegments = (arcRadius * totalArc) / 1.8; - // if (this.inches) { - // totalSegments *= 25; - // } + if (state.units == 'in') { + totalSegments *= 25; + } if (totalSegments < 1) { totalSegments = 1; } @@ -167,27 +165,33 @@ export class Interpreter { } T0(command: SelectToolCommand, machine: Machine): void { - machine.state.t = 0; + machine.state.tool = 0; } T1(command: SelectToolCommand, machine: Machine): void { - machine.state.t = 1; + machine.state.tool = 1; } T2(command: SelectToolCommand, machine: Machine): void { - machine.state.t = 2; + machine.state.tool = 2; } T3(command: SelectToolCommand, machine: Machine): void { - machine.state.t = 3; + machine.state.tool = 3; } T4(command: SelectToolCommand, machine: Machine): void { - machine.state.t = 4; + machine.state.tool = 4; } T5(command: SelectToolCommand, machine: Machine): void { - machine.state.t = 5; + machine.state.tool = 5; } T6(command: SelectToolCommand, machine: Machine): void { - machine.state.t = 6; + machine.state.tool = 6; } T7(command: SelectToolCommand, machine: Machine): void { - machine.state.t = 7; + machine.state.tool = 7; + } + G20(command: GCodeCommand, machine: Machine): void { + machine.state.units = 'in'; + } + G21(command: GCodeCommand, machine: Machine): void { + machine.state.units = 'mm'; } } From c4f1779a4d435e85c4aef1edcf4b9e78ea046ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Mon, 19 Aug 2024 16:22:37 -0400 Subject: [PATCH 04/34] Simplify toolchange --- src/gcode-parser.ts | 27 --------------------------- src/interpreter.ts | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 37 deletions(-) diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index 6c0a2f28..8d4f51b8 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -136,17 +136,6 @@ export class MoveCommand extends GCodeCommand { } } -export class SelectToolCommand extends GCodeCommand { - constructor( - src: string, - gcode: string, - comment?: string, - public toolIndex?: number - ) { - super(src, gcode, undefined, comment); - } -} - type Metadata = { thumbnails: Record }; export class Layer { @@ -231,22 +220,6 @@ export class Parser { case 'g3': case 'g03': return new MoveCommand(line, gcode, params, comment); - case 't0': - return new SelectToolCommand(line, gcode, comment, 0); - case 't1': - return new SelectToolCommand(line, gcode, comment, 1); - case 't2': - return new SelectToolCommand(line, gcode, comment, 2); - case 't3': - return new SelectToolCommand(line, gcode, comment, 3); - case 't4': - return new SelectToolCommand(line, gcode, comment, 4); - case 't5': - return new SelectToolCommand(line, gcode, comment, 5); - case 't6': - return new SelectToolCommand(line, gcode, comment, 6); - case 't7': - return new SelectToolCommand(line, gcode, comment, 7); default: return new GCodeCommand(line, gcode, params, comment); } diff --git a/src/interpreter.ts b/src/interpreter.ts index 040b7c22..7b9a27af 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1,5 +1,5 @@ import { Path, PathType } from './path'; -import { Code, GCodeCommand, SelectToolCommand } from './gcode-parser'; +import { Code, GCodeCommand } from './gcode-parser'; export class State { x: number; @@ -121,7 +121,7 @@ export class Interpreter { totalArc += 2 * Math.PI; } } - let totalSegments = (arcRadius * totalArc) / 1.8; + let totalSegments = (arcRadius * totalArc) / 0.5; if (state.units == 'in') { totalSegments *= 25; } @@ -164,28 +164,28 @@ export class Interpreter { machine.state.z = 0; } - T0(command: SelectToolCommand, machine: Machine): void { + T0(command: GCodeCommand, machine: Machine): void { machine.state.tool = 0; } - T1(command: SelectToolCommand, machine: Machine): void { + T1(command: GCodeCommand, machine: Machine): void { machine.state.tool = 1; } - T2(command: SelectToolCommand, machine: Machine): void { + T2(command: GCodeCommand, machine: Machine): void { machine.state.tool = 2; } - T3(command: SelectToolCommand, machine: Machine): void { + T3(command: GCodeCommand, machine: Machine): void { machine.state.tool = 3; } - T4(command: SelectToolCommand, machine: Machine): void { + T4(command: GCodeCommand, machine: Machine): void { machine.state.tool = 4; } - T5(command: SelectToolCommand, machine: Machine): void { + T5(command: GCodeCommand, machine: Machine): void { machine.state.tool = 5; } - T6(command: SelectToolCommand, machine: Machine): void { + T6(command: GCodeCommand, machine: Machine): void { machine.state.tool = 6; } - T7(command: SelectToolCommand, machine: Machine): void { + T7(command: GCodeCommand, machine: Machine): void { machine.state.tool = 7; } G20(command: GCodeCommand, machine: Machine): void { From 277866d354c2bffc8e9b4c0ee73009de71fb3e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Mon, 19 Aug 2024 16:31:20 -0400 Subject: [PATCH 05/34] travelType --- src/interpreter.ts | 19 +++++++++++-------- src/path.ts | 6 +++--- src/webgl-preview.ts | 4 ++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/interpreter.ts b/src/interpreter.ts index 7b9a27af..01f52ee7 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -44,10 +44,8 @@ export class Interpreter { let lastPath = machine.paths[machine.paths.length - 1]; const pathType = e ? PathType.Extrusion : PathType.Travel; - if (lastPath === undefined || lastPath.type !== pathType) { - lastPath = new Path(pathType, 0.6, 0.2, state.tool); - machine.paths.push(lastPath); - lastPath.addPoint(state.x, state.y, state.z); + if (lastPath === undefined || lastPath.travelType !== pathType) { + lastPath = this.breakPath(machine, pathType); } state.x = x || state.x; @@ -68,10 +66,8 @@ export class Interpreter { let lastPath = machine.paths[machine.paths.length - 1]; const pathType = e ? PathType.Extrusion : PathType.Travel; - if (lastPath === undefined || lastPath.type !== pathType) { - lastPath = new Path(pathType, 0.6, 0.2, state.tool); - machine.paths.push(lastPath); - lastPath.addPoint(state.x, state.y, state.z); + if (lastPath === undefined || lastPath.travelType !== pathType) { + lastPath = this.breakPath(machine, pathType); } if (r) { @@ -194,4 +190,11 @@ export class Interpreter { G21(command: GCodeCommand, machine: Machine): void { machine.state.units = 'mm'; } + + private breakPath(machine: Machine, newType: PathType): Path { + const lastPath = new Path(newType, 0.6, 0.2, machine.state.tool); + machine.paths.push(lastPath); + lastPath.addPoint(machine.state.x, machine.state.y, machine.state.z); + return lastPath; + } } diff --git a/src/path.ts b/src/path.ts index 69e90657..616bd354 100644 --- a/src/path.ts +++ b/src/path.ts @@ -8,15 +8,15 @@ export enum PathType { } export class Path { - type: PathType; + travelType: PathType; vertices: number[]; extrusionWidth: number; lineHeight: number; geometryCache: BufferGeometry | undefined; tool: number; - constructor(type: PathType, extrusionWidth = 0.6, lineHeight = 0.2, tool = 0) { - this.type = type; + constructor(travelType: PathType, extrusionWidth = 0.6, lineHeight = 0.2, tool = 0) { + this.travelType = travelType; this.vertices = []; this.extrusionWidth = extrusionWidth; this.lineHeight = lineHeight; diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 90a1fbea..5146f7a6 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -393,8 +393,8 @@ export class WebGLPreview { if (Object.keys(this._geometries).length === 0 && this.renderTubes) { let color: number; this.virtualMachine.paths - .filter(({ type }) => { - return type === PathType.Extrusion; + .filter(({ travelType }) => { + return travelType === PathType.Extrusion; }) .forEach((path) => { if (Array.isArray(this._extrusionColor)) { From a60654e9f0e949cf8ec85fa612e32770c3b417c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Mon, 19 Aug 2024 16:31:50 -0400 Subject: [PATCH 06/34] remove setInches --- src/webgl-preview.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 5146f7a6..996206dc 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -330,14 +330,6 @@ export class WebGLPreview { this._lastRenderTime = performance.now() - startRender; } - setInches(): void { - // if (this.beyondFirstMove) { - // console.warn('Switching units after movement is already made is discouraged and is not supported.'); - // return; - // } - // this.inches = true; - } - /** @internal */ drawBuildVolume(): void { if (!this.buildVolume) return; From 58bf83b8b5ec88c69c277637918968fa2d277568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Mon, 19 Aug 2024 16:34:51 -0400 Subject: [PATCH 07/34] remove targetId --- src/webgl-preview.ts | 36 +++++------------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 996206dc..e7580dd0 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -60,10 +60,6 @@ export type GCodePreviewOptions = { * @deprecated Please see the demo how to implement drag and drop. */ allowDragNDrop?: boolean; - /** - * @deprecated Please use the `canvas` param instead. - */ - targetId?: string; /** @experimental */ devMode?: boolean | DevModeOptions; }; @@ -71,10 +67,6 @@ export type GCodePreviewOptions = { export class WebGLPreview { minLayerThreshold = 0.05; parser: Parser; - /** - * @deprecated Please use the `canvas` param instead. - */ - targetId?: string; scene: Scene; camera: PerspectiveCamera; renderer: WebGLRenderer; @@ -134,7 +126,6 @@ export class WebGLPreview { if (opts.backgroundColor !== undefined) { this.backgroundColor = new Color(opts.backgroundColor); } - this.targetId = opts.targetId; this.endLayer = opts.endLayer; this.startLayer = opts.startLayer; this.lineWidth = opts.lineWidth; @@ -178,28 +169,11 @@ export class WebGLPreview { console.info('Using THREE r' + REVISION); console.debug('opts', opts); - if (this.targetId) { - console.warn('`targetId` is deprecated and will removed in the future. Use `canvas` instead.'); - } - - if (!opts.canvas) { - if (!this.targetId) { - throw Error('Set either opts.canvas or opts.targetId'); - } - const container = document.getElementById(this.targetId); - if (!container) throw new Error('Unable to find element ' + this.targetId); - - this.renderer = new WebGLRenderer({ preserveDrawingBuffer: true }); - this.canvas = this.renderer.domElement; - - container.appendChild(this.canvas); - } else { - this.canvas = opts.canvas; - this.renderer = new WebGLRenderer({ - canvas: this.canvas, - preserveDrawingBuffer: true - }); - } + this.canvas = opts.canvas; + this.renderer = new WebGLRenderer({ + canvas: this.canvas, + preserveDrawingBuffer: true + }); this.camera = new PerspectiveCamera(25, this.canvas.offsetWidth / this.canvas.offsetHeight, 10, 5000); this.camera.position.fromArray(this.initialCameraPosition); From 8f21b15d54057f3d764a059b87f02a4965dca22b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Mon, 19 Aug 2024 17:08:35 -0400 Subject: [PATCH 08/34] layers --- src/interpreter.ts | 32 ++++++++++++++++++++++++++++++++ src/path.ts | 2 +- src/webgl-preview.ts | 4 ---- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/interpreter.ts b/src/interpreter.ts index 01f52ee7..c689b3c4 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -24,6 +24,38 @@ export class Machine { this.paths = []; this.state = state || State.initial; } + + isPlanar(): boolean { + return ( + this.paths.find( + (path) => + path.travelType === PathType.Extrusion && path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2]) + ) === undefined + ); + } + + layers(): Path[][] | null { + if (!this.isPlanar()) { + return null; + } + + const layers: Path[][] = []; + let currentLayer: Path[] = []; + + this.paths.forEach((path) => { + if (path.travelType === PathType.Extrusion) { + currentLayer.push(path); + } else { + if (path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2])) { + layers.push(currentLayer); + currentLayer = []; + } + currentLayer.push(path); + } + }); + + return layers; + } } export class Interpreter { diff --git a/src/path.ts b/src/path.ts index 616bd354..53989cd8 100644 --- a/src/path.ts +++ b/src/path.ts @@ -9,7 +9,7 @@ export enum PathType { export class Path { travelType: PathType; - vertices: number[]; + public vertices: number[]; extrusionWidth: number; lineHeight: number; geometryCache: BufferGeometry | undefined; diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index e7580dd0..ca71be53 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -54,7 +54,6 @@ export type GCodePreviewOptions = { toolColors?: Record; disableGradient?: boolean; extrusionWidth?: number; - /** @experimental */ renderTubes?: boolean; /** * @deprecated Please see the demo how to implement drag and drop. @@ -99,7 +98,6 @@ export class WebGLPreview { private animationFrameId?: number; private renderLayerIndex = 0; private _geometries: Record = {}; - paths: Path[]; interpreter: Interpreter; virtualMachine: Machine = new Machine(); @@ -140,7 +138,6 @@ export class WebGLPreview { this.extrusionWidth = opts.extrusionWidth ?? this.extrusionWidth; this.devMode = opts.devMode ?? this.devMode; this.stats = this.devMode ? new Stats() : undefined; - this.paths = []; this.interpreter = new Interpreter(); if (opts.extrusionColor !== undefined) { @@ -330,7 +327,6 @@ export class WebGLPreview { this.singleLayerMode = false; this.devGui?.reset(); this._geometries = {}; - this.paths = []; } resize(): void { From 382a35a78b6e0d50e3ea17f7b677371228d89889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Mon, 19 Aug 2024 17:55:36 -0400 Subject: [PATCH 09/34] render lines --- demo/js/demo.js | 2 +- src/interpreter.ts | 8 ++++++ src/webgl-preview.ts | 64 +++++++++++++++++++++++++++++++++----------- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/demo/js/demo.js b/demo/js/demo.js index e5fb9dc9..e7073267 100644 --- a/demo/js/demo.js +++ b/demo/js/demo.js @@ -55,7 +55,7 @@ const settingsPresets = { lineWidth: 1, singleLayerMode: false, renderExtrusion: true, - renderTubes: true, + renderTubes: false, extrusionColors: ['#CF439D', 'rgb(84,74,187)', 'white', 'rgb(83,209,104)'], travel: false, travelColor: '#00FF00', diff --git a/src/interpreter.ts b/src/interpreter.ts index c689b3c4..040182a5 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -56,6 +56,14 @@ export class Machine { return layers; } + + extrusions(): Path[] { + return this.paths.filter((path) => path.travelType === PathType.Extrusion); + } + + travels(): Path[] { + return this.paths.filter((path) => path.travelType === PathType.Travel); + } } export class Interpreter { diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index ca71be53..1b5f1180 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -5,8 +5,6 @@ import { LineBox } from './lineBox'; import Stats from 'three/examples/jsm/libs/stats.module.js'; import { DevGUI, DevModeOptions } from './dev-gui'; - -import { Path, PathType } from './path'; import { Interpreter, Machine } from './interpreter'; import { @@ -19,6 +17,9 @@ import { Euler, Fog, Group, + Line, + LineBasicMaterial, + LineSegments, MeshLambertMaterial, PerspectiveCamera, PointLight, @@ -295,6 +296,7 @@ export class WebGLPreview { this.initScene(); this.renderGeometries(); + this.renderLines(); this.scene.add(this.group); this.renderer.render(this.scene, this.camera); @@ -351,23 +353,53 @@ export class WebGLPreview { this.animationFrameId = undefined; } + private renderLines() { + if (this.renderTravel) { + const material = new LineBasicMaterial({ color: this._travelColor, linewidth: this.lineWidth }); + this.disposables.push(material); + + this.virtualMachine.travels().forEach((path) => { + const geometry = path.line(); + const line = new Line(geometry, material); + this.group?.add(line); + }); + } + + if (this.renderExtrusion && !this.renderTubes) { + const lineMaterials = {} as Record; + + if (Array.isArray(this._extrusionColor)) { + this._extrusionColor.forEach((color, index) => { + lineMaterials[index] = new LineBasicMaterial({ color, linewidth: this.lineWidth }); + }); + } else { + lineMaterials[0] = new LineBasicMaterial({ + color: this._extrusionColor, + linewidth: this.lineWidth + }); + } + + this.virtualMachine.extrusions().forEach((path) => { + const geometry = path.line(); + const line = new LineSegments(geometry, lineMaterials[path.tool]); + this.group?.add(line); + }); + } + } + private renderGeometries() { if (Object.keys(this._geometries).length === 0 && this.renderTubes) { let color: number; - this.virtualMachine.paths - .filter(({ travelType }) => { - return travelType === PathType.Extrusion; - }) - .forEach((path) => { - if (Array.isArray(this._extrusionColor)) { - color = this._extrusionColor[path.tool].getHex(); - } else { - color = this._extrusionColor.getHex(); - } - - this._geometries[color] ||= []; - this._geometries[color].push(path.geometry()); - }); + this.virtualMachine.extrusions().forEach((path) => { + if (Array.isArray(this._extrusionColor)) { + color = this._extrusionColor[path.tool].getHex(); + } else { + color = this._extrusionColor.getHex(); + } + + this._geometries[color] ||= []; + this._geometries[color].push(path.geometry()); + }); } if (this._geometries) { From 92ea42e0c9e8a6bade122680694040ce35d6c7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Mon, 19 Aug 2024 17:59:00 -0400 Subject: [PATCH 10/34] extract machine --- src/interpreter.ts | 66 +------------------------------------------- src/machine.ts | 66 ++++++++++++++++++++++++++++++++++++++++++++ src/webgl-preview.ts | 3 +- 3 files changed, 69 insertions(+), 66 deletions(-) create mode 100644 src/machine.ts diff --git a/src/interpreter.ts b/src/interpreter.ts index 040182a5..6bee08c9 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1,70 +1,6 @@ import { Path, PathType } from './path'; import { Code, GCodeCommand } from './gcode-parser'; - -export class State { - x: number; - y: number; - z: number; - e: number; - tool: number; - units: 'mm' | 'in'; - - static get initial(): State { - const state = new State(); - Object.assign(state, { x: 0, y: 0, z: 0, e: 0, tool: 0, units: 'mm' }); - return state; - } -} - -export class Machine { - paths: Path[]; - state: State; - - constructor(state?: State) { - this.paths = []; - this.state = state || State.initial; - } - - isPlanar(): boolean { - return ( - this.paths.find( - (path) => - path.travelType === PathType.Extrusion && path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2]) - ) === undefined - ); - } - - layers(): Path[][] | null { - if (!this.isPlanar()) { - return null; - } - - const layers: Path[][] = []; - let currentLayer: Path[] = []; - - this.paths.forEach((path) => { - if (path.travelType === PathType.Extrusion) { - currentLayer.push(path); - } else { - if (path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2])) { - layers.push(currentLayer); - currentLayer = []; - } - currentLayer.push(path); - } - }); - - return layers; - } - - extrusions(): Path[] { - return this.paths.filter((path) => path.travelType === PathType.Extrusion); - } - - travels(): Path[] { - return this.paths.filter((path) => path.travelType === PathType.Travel); - } -} +import { Machine } from './machine'; export class Interpreter { execute(commands: GCodeCommand[], machine = new Machine()): Machine { diff --git a/src/machine.ts b/src/machine.ts new file mode 100644 index 00000000..06b49a3f --- /dev/null +++ b/src/machine.ts @@ -0,0 +1,66 @@ +import { Path, PathType } from './path'; + +export class State { + x: number; + y: number; + z: number; + e: number; + tool: number; + units: 'mm' | 'in'; + + static get initial(): State { + const state = new State(); + Object.assign(state, { x: 0, y: 0, z: 0, e: 0, tool: 0, units: 'mm' }); + return state; + } +} + +export class Machine { + paths: Path[]; + state: State; + + constructor(state?: State) { + this.paths = []; + this.state = state || State.initial; + } + + isPlanar(): boolean { + return ( + this.paths.find( + (path) => + path.travelType === PathType.Extrusion && path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2]) + ) === undefined + ); + } + + layers(): Path[][] | null { + if (!this.isPlanar()) { + return null; + } + + const layers: Path[][] = []; + let currentLayer: Path[] = []; + + this.paths.forEach((path) => { + if (path.travelType === PathType.Extrusion) { + currentLayer.push(path); + } else { + if (path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2])) { + layers.push(currentLayer); + currentLayer = []; + } + currentLayer.push(path); + } + }); + + return layers; + } + + extrusions(): Path[] { + return this.paths.filter((path) => path.travelType === PathType.Extrusion); + } + + travels(): Path[] { + return this.paths.filter((path) => path.travelType === PathType.Travel); + } +} diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 1b5f1180..219aaa56 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -5,7 +5,8 @@ import { LineBox } from './lineBox'; import Stats from 'three/examples/jsm/libs/stats.module.js'; import { DevGUI, DevModeOptions } from './dev-gui'; -import { Interpreter, Machine } from './interpreter'; +import { Interpreter } from './interpreter'; +import { Machine } from './machine'; import { AmbientLight, From 35ae3347427bb8e735ff96a0ba1e939e0f039b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Tue, 8 Oct 2024 19:55:00 -0400 Subject: [PATCH 11/34] wip --- demo/js/demo.js | 2 +- src/webgl-preview.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/demo/js/demo.js b/demo/js/demo.js index e7073267..e5fb9dc9 100644 --- a/demo/js/demo.js +++ b/demo/js/demo.js @@ -55,7 +55,7 @@ const settingsPresets = { lineWidth: 1, singleLayerMode: false, renderExtrusion: true, - renderTubes: false, + renderTubes: true, extrusionColors: ['#CF439D', 'rgb(84,74,187)', 'white', 'rgb(83,209,104)'], travel: false, travelColor: '#00FF00', diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 219aaa56..846067a9 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -355,13 +355,14 @@ export class WebGLPreview { } private renderLines() { + console.log('rendering lines'); if (this.renderTravel) { const material = new LineBasicMaterial({ color: this._travelColor, linewidth: this.lineWidth }); this.disposables.push(material); this.virtualMachine.travels().forEach((path) => { const geometry = path.line(); - const line = new Line(geometry, material); + const line = new LineSegments(geometry, material); this.group?.add(line); }); } From 79751db64e2063d0d026b4bdc2b29155c514867e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Tue, 8 Oct 2024 23:07:29 -0400 Subject: [PATCH 12/34] Make it work with rerenders --- demo/js/app.js | 18 +- demo/js/demo.js | 613 ------------------------------------------- src/webgl-preview.ts | 12 +- 3 files changed, 15 insertions(+), 628 deletions(-) delete mode 100644 demo/js/demo.js diff --git a/demo/js/app.js b/demo/js/app.js index 5972fe7e..f9621e6d 100644 --- a/demo/js/app.js +++ b/demo/js/app.js @@ -9,7 +9,7 @@ const preferDarkMode = window.matchMedia('(prefers-color-scheme: dark)'); const initialBackgroundColor = preferDarkMode.matches ? '#141414' : '#eee'; const statsContainer = () => document.querySelector('.sidebar'); -const loadProgressive = true; +const loadProgressive = false; let observer = null; let preview = null; let firstRender = true; @@ -54,7 +54,7 @@ export const app = (window.app = createApp({ const updateUI = async () => { const { parser, - layers, + // layers, extrusionColor, topLayerColor, lastSegmentColor, @@ -66,16 +66,17 @@ export const app = (window.app = createApp({ renderExtrusion, lineWidth, renderTubes, - extrusionWidth + extrusionWidth, + virtualMachine } = preview; const { thumbnails } = parser.metadata; thumbnail.value = thumbnails['220x124']?.src; - layerCount.value = layers.length; + layerCount.value = virtualMachine.layers().length; const colors = extrusionColor instanceof Array ? extrusionColor : [extrusionColor]; const currentSettings = { - maxLayer: layers.length, - endLayer: layers.length, + maxLayer: virtualMachine.layers().length, + endLayer: virtualMachine.layers().length, singleLayerMode, renderTravel, travelColor: '#' + travelColor.getHexString(), @@ -94,7 +95,7 @@ export const app = (window.app = createApp({ }; Object.assign(settings.value, currentSettings); - preview.endLayer = layers.length; + preview.endLayer = virtualMachine.layers().length; }; const loadGCodeFromServer = async (filename) => { @@ -205,7 +206,8 @@ export const app = (window.app = createApp({ preview.lastSegmentColor = settings.value.highlightLastSegment ? settings.value.lastSegmentColor : undefined; debounce(() => { - preview.renderAnimated(Math.ceil(preview.layers.length / 60)); + // preview.renderAnimated(Math.ceil(preview.layers.length / 60)); + preview.render(); }); }); }); diff --git a/demo/js/demo.js b/demo/js/demo.js deleted file mode 100644 index e5fb9dc9..00000000 --- a/demo/js/demo.js +++ /dev/null @@ -1,613 +0,0 @@ -import * as GCodePreview from 'gcode-preview'; -import * as THREE from 'three'; -import * as Canvas2Image from 'canvas2image'; - -let gcodePreview; -let favIcon; -let thumb; -const maxToolCount = 8; -let toolCount = 4; -let gcode; -let renderProgressive = false; - -const canvasElement = document.querySelector('.gcode-previewer'); -const settingsPreset = document.getElementById('settings-presets'); -const startLayer = document.getElementById('start-layer'); -const startLayerValue = document.getElementById('start-layer-value'); -const endLayer = document.getElementById('end-layer'); -const endLayerValue = document.getElementById('end-layer-value'); -const lineWidth = document.getElementById('line-width'); -const lineWidthValue = document.getElementById('line-width-value'); -const extrusionWidth = document.getElementById('extrusion-width'); -const extrusionWidthValue = document.getElementById('extrusion-width-value'); -const toggleSingleLayerMode = document.getElementById('single-layer-mode'); -const toggleExtrusion = document.getElementById('extrusion'); -const toggleRenderTubes = document.getElementById('render-tubes'); -const extrusionColor = {}; -for (let i = 0; i < maxToolCount; i++) { - extrusionColor[i] = document.getElementById(`extrusion-color-t${i}`); -} -const addColorButton = document.getElementById('add-color'); -const removeColorButton = document.getElementById('remove-color'); - -const backgroundColor = document.getElementById('background-color'); -const toggleTravel = document.getElementById('travel'); -const toggleHighlight = document.getElementById('highlight'); -const topLayerColorInput = document.getElementById('top-layer-color'); -const lastSegmentColorInput = document.getElementById('last-segment-color'); -// const layerCount = document.getElementById('layer-count'); -const fileName = document.getElementById('file-name'); -const fileSelector = document.getElementById('file-selector'); -const fileSize = document.getElementById('file-size'); -const snapshot = document.getElementById('snapshot'); -const buildVolumeX = document.getElementById('buildVolumeX'); -const buildVolumeY = document.getElementById('buildVolumeY'); -const buildVolumeZ = document.getElementById('buildVolumeZ'); -const drawBuildVolume = document.getElementById('drawBuildVolume'); -const travelColor = document.getElementById('travel-color'); -const preferDarkMode = window.matchMedia('(prefers-color-scheme: dark)'); - -const defaultPreset = 'multicolor'; - -const settingsPresets = { - multicolor: { - file: 'gcodes/3DBenchy-Multi-part.gcode', - lineWidth: 1, - singleLayerMode: false, - renderExtrusion: true, - renderTubes: true, - extrusionColors: ['#CF439D', 'rgb(84,74,187)', 'white', 'rgb(83,209,104)'], - travel: false, - travelColor: '#00FF00', - highlightTopLayer: false, - topLayerColor: undefined, - lastSegmentColor: undefined, - drawBuildVolume: true, - buildVolume: { - x: 180, - y: 180, - z: 200 - } - }, - mach3: { - file: 'gcodes/mach3.gcode', - lineWidth: 1, - singleLayerMode: false, - renderExtrusion: false, - renderTubes: false, - extrusionColors: [], - travel: true, - travelColor: '#00FF00', - highlightTopLayer: false, - topLayerColor: undefined, - lastSegmentColor: undefined, - drawBuildVolume: true, - buildVolume: { - x: 20, - y: 20, - z: '' - } - }, - arcs: { - file: 'gcodes/screw.gcode', - lineWidth: 2, - singleLayerMode: true, - renderExtrusion: true, - renderTubes: true, - extrusionColors: ['rgb(83,209,104)'], - travel: false, - travelColor: '#00FF00', - highlightTopLayer: false, - topLayerColor: undefined, - lastSegmentColor: undefined, - drawBuildVolume: true, - buildVolume: { - x: 200, - y: 200, - z: 180 - } - }, - 'vase-mode': { - file: 'gcodes/vase.gcode', - lineWidth: 1, - singleLayerMode: true, - renderExtrusion: true, - renderTubes: true, - extrusionColors: ['rgb(84,74,187)'], - travel: false, - travelColor: '#00FF00', - highlightTopLayer: true, - topLayerColor: '#40BFBF', - lastSegmentColor: '#ffffff', - drawBuildVolume: true, - buildVolume: { - x: 200, - y: 200, - z: 220 - } - }, - 'travel-moves': { - file: 'gcodes/plant-sign.gcode', - lineWidth: 2, - singleLayerMode: false, - renderExtrusion: true, - renderTubes: true, - extrusionColors: ['#777777'], - travel: true, - travelColor: '#00FF00', - highlightTopLayer: true, - topLayerColor: '#aaaaaa', - lastSegmentColor: undefined, - drawBuildVolume: true, - buildVolume: { - x: 200, - y: 200, - z: 220 - } - } -}; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars -export function initDemo() { - // eslint-disable-line no-unused-vars, @typescript-eslint/no-unused-vars - const settings = JSON.parse(localStorage.getItem('settings')); - - const initialBackgroundColor = preferDarkMode.matches ? '#111' : '#eee'; - - const preview = (window.preview = new GCodePreview.init({ - canvas: canvasElement, - buildVolume: settings?.buildVolume || { x: 190, y: 210, z: 0 }, - initialCameraPosition: [180, 150, 300], - backgroundColor: initialBackgroundColor, - lineHeight: 0.3, - devMode: { - camera: true, - renderer: true, - parser: true, - buildVolume: true, - devHelpers: true, - statsContainer: document.querySelector('.sidebar') - } - })); - - backgroundColor.value = initialBackgroundColor; - - loadSettingPreset(defaultPreset); - - settingsPreset.addEventListener('change', function (e) { - loadSettingPreset(e.target.value); - }); - - fileSelector.addEventListener('change', function (e) { - const fileName = e.target.value; - changeFile(fileName); - }); - - startLayer.addEventListener('input', function () { - preview.startLayer = +startLayer.value; - startLayerValue.innerText = startLayer.value; - endLayer.value = preview.endLayer = Math.max(preview.startLayer, preview.endLayer); - endLayerValue.innerText = endLayer.value; - preview.render(); - }); - - endLayer.addEventListener('input', function () { - preview.endLayer = +endLayer.value; - endLayerValue.innerText = endLayer.value; - startLayer.value = preview.startLayer = Math.min(preview.startLayer, preview.endLayer); - startLayerValue.innerText = startLayer.value; - preview.render(); - }); - - lineWidth.addEventListener('input', function () { - changeLineWidth(lineWidth.value); - preview.render(); - }); - - extrusionWidth.addEventListener('input', function () { - preview.extrusionWidth = +extrusionWidth.value; - extrusionWidthValue.innerText = extrusionWidth.value; - preview.render(); - }); - - toggleSingleLayerMode.addEventListener('click', function () { - changeSingleLayerMode(!!toggleSingleLayerMode.checked); - preview.render(); - }); - - toggleExtrusion.addEventListener('click', function () { - changeRenderExtrusion(!!toggleExtrusion.checked); - preview.render(); - }); - - toggleRenderTubes.addEventListener('click', function () { - changeRenderTubes(!!toggleRenderTubes.checked); - startLoadingProgressive(gcode); - }); - - for (let i = 0; i < 8; i++) { - extrusionColor[i].addEventListener('input', () => - debounce(() => { - const colors = preview.extrusionColor; - colors[i] = extrusionColor[i].value; - preview.extrusionColor = colors; - preview.render(); - }) - ); - } - - addColorButton.addEventListener('click', function () { - if (toolCount >= maxToolCount) return; - toolCount++; - showExtrusionColors(); - }); - - removeColorButton.addEventListener('click', function () { - if (toolCount <= 1) return; - toolCount--; - showExtrusionColors(); - }); - - backgroundColor.addEventListener('input', () => - throttle(() => { - changeBackgroundColor(backgroundColor.value); - preview.render(); - }) - ); - - toggleTravel.addEventListener('click', function () { - changeRenderTravel(!!toggleTravel.checked); - preview.render(); - }); - - travelColor.addEventListener('input', () => - throttle(() => { - changeTravelColor(travelColor.value); - preview.render(); - }) - ); - - toggleHighlight.addEventListener('click', function () { - changeHighlightTopLayer(!!toggleHighlight.checked); - preview.render(); - }); - - topLayerColorInput.addEventListener('input', () => - throttle(() => { - changeTopLayerColor(topLayerColorInput.value); - preview.render(); - }) - ); - - lastSegmentColorInput.addEventListener('input', () => - throttle(() => { - changeLastSegmentColor(lastSegmentColorInput.value); - preview.render(); - }) - ); - - canvasElement.addEventListener('dragover', (evt) => { - evt.stopPropagation(); - evt.preventDefault(); - evt.dataTransfer.dropEffect = 'copy'; - canvasElement.classList.add('dragging'); - }); - - canvasElement.addEventListener('dragleave', (evt) => { - evt.stopPropagation(); - evt.preventDefault(); - canvasElement.classList.remove('dragging'); - }); - - canvasElement.addEventListener('drop', async (evt) => { - evt.stopPropagation(); - evt.preventDefault(); - preview.topLayerColor = undefined; - preview.lastSegmentColor = undefined; - canvasElement.classList.remove('dragging'); - const files = evt.dataTransfer.files; - const file = files[0]; - - fileName.innerText = file.name; - fileSize.innerText = humanFileSize(file.size); - - // await preview._readFromStream(file.stream()); - _handleGCode(file.name, await file.text()); - updateUI(); - }); - - function updateBuildVolume() { - const x = parseInt(buildVolumeX.value, 10); - const y = parseInt(buildVolumeY.value, 10); - const z = parseInt(buildVolumeZ.value, 10); - - const draw = !!drawBuildVolume.checked; - - changeDrawBuildVolume(draw); - changeBuildVolume({ x, y, z }); - - preview.render(); - - storeSettings(); - } - - buildVolumeX.addEventListener('input', updateBuildVolume); - buildVolumeY.addEventListener('input', updateBuildVolume); - buildVolumeZ.addEventListener('input', updateBuildVolume); - drawBuildVolume.addEventListener('input', updateBuildVolume); - - // lineWidth.addEventListener('change', function() { - // preview.lineWidth = parseInt(lineWidth.value,10); - // preview.render(); - // }); - - window.addEventListener('resize', function () { - preview.resize(); - }); - - snapshot.addEventListener('click', function (evt) { - evt.stopPropagation(); - evt.preventDefault(); - - Canvas2Image.saveAsJPEG(gcodePreview.canvas, innerWidth, innerHeight, fileName.innerText.replace('.gcode', '.jpg')); - }); - - function changeFile(name) { - fileSelector.value = name; - loadGCodeFromServer(name); - } - - function changeLineWidth(width) { - lineWidthValue.innerText = parseInt(width, 10); - lineWidth.value = parseInt(width, 10); - preview.lineWidth = parseInt(width, 10); - } - - function changeSingleLayerMode(enabled) { - preview.singleLayerMode = enabled; - toggleSingleLayerMode.checked = enabled; - if (preview.singleLayerMode) { - startLayer.setAttribute('disabled', 'disabled'); - } else { - startLayer.removeAttribute('disabled'); - } - } - - function changeRenderExtrusion(enabled) { - preview.renderExtrusion = enabled; - toggleExtrusion.checked = enabled; - if (enabled) { - for (let i = 0; i < 8; i++) { - extrusionColor[i].removeAttribute('disabled'); - } - toggleRenderTubes.removeAttribute('disabled'); - } else { - for (let i = 0; i < 8; i++) { - extrusionColor[i].setAttribute('disabled', 'disabled'); - } - toggleRenderTubes.setAttribute('disabled', 'disabled'); - } - } - - function changeRenderTubes(enabled) { - preview.renderTubes = enabled; - toggleRenderTubes.checked = enabled; - } - - function changeRenderTravel(enabled) { - preview.renderTravel = enabled; - toggleTravel.checked = enabled; - if (enabled) { - travelColor.removeAttribute('disabled'); - } else { - travelColor.setAttribute('disabled', 'disabled'); - } - } - - function changeHighlightTopLayer(enabled) { - toggleHighlight.checked = enabled; - if (enabled) { - changeTopLayerColor(preview.topLayerColor || '#40BFBF'); - changeLastSegmentColor(preview.lastSegmentColor || '#ffffff'); - topLayerColorInput.removeAttribute('disabled'); - lastSegmentColorInput.removeAttribute('disabled'); - } else { - preview.topLayerColor = undefined; - preview.lastSegmentColor = undefined; - topLayerColorInput.setAttribute('disabled', 'disabled'); - lastSegmentColorInput.setAttribute('disabled', 'disabled'); - } - } - - function changeTravelColor(color) { - preview.travelColor = color; - travelColor.value = color; - } - - function changeBackgroundColor(color) { - preview.backgroundColor = color; - backgroundColor.value = color; - } - - function changeTopLayerColor(color) { - topLayerColorInput.value = color; - preview.topLayerColor = color; - } - - function changeLastSegmentColor(color) { - lastSegmentColorInput.value = color; - preview.lastSegmentColor = color; - } - - function changeBuildVolume(volume) { - buildVolumeX.value = volume.x; - buildVolumeY.value = volume.y; - buildVolumeZ.value = volume.z; - preview.buildVolume.x = volume.x; - preview.buildVolume.y = volume.y; - preview.buildVolume.z = volume.z; - } - - function changeDrawBuildVolume(draw) { - drawBuildVolume.checked = draw; - if (draw) { - buildVolumeX.removeAttribute('disabled'); - buildVolumeY.removeAttribute('disabled'); - buildVolumeZ.removeAttribute('disabled'); - } else { - buildVolumeX.setAttribute('disabled', 'disabled'); - buildVolumeY.setAttribute('disabled', 'disabled'); - buildVolumeZ.setAttribute('disabled', 'disabled'); - } - } - - function changeToolColors(colors) { - toolCount = colors.length; - for (let i = 0; i < toolCount; i++) extrusionColor[i].value = '#' + new THREE.Color(colors[i]).getHexString(); - preview.extrusionColor = colors; - showExtrusionColors(); - } - - function loadSettingPreset(name) { - const preset = settingsPresets[name]; - changeLineWidth(preset.lineWidth); - changeSingleLayerMode(preset.singleLayerMode); - changeRenderExtrusion(preset.renderExtrusion); - changeRenderTubes(preset.renderTubes); - changeRenderTravel(preset.travel); - changeHighlightTopLayer(preset.highlightTopLayer); - changeTravelColor(preset.travelColor); - changeTopLayerColor(preset.topLayerColor); - changeLastSegmentColor(preset.lastSegmentColor); - changeDrawBuildVolume(preset.drawBuildVolume); - changeBuildVolume(preset.buildVolume); - changeToolColors(preset.extrusionColors); - changeFile(preset.file); - } - - gcodePreview = preview; - - updateUI(); - - return preview; -} - -function storeSettings() { - localStorage.setItem( - 'settings', - JSON.stringify({ - buildVolume: { - x: gcodePreview.buildVolume.x, - y: gcodePreview.buildVolume.y, - z: gcodePreview.buildVolume.z - } - }) - ); -} - -function updateUI() { - // startLayer.setAttribute('max', gcodePreview.layers.length); - // endLayer.setAttribute('max', gcodePreview.layers.length); - // endLayer.value = gcodePreview.layers.length; - // endLayerValue.innerText = endLayer.value; - - startLayerValue.innerText = startLayer.value; - - // layerCount.innerText = gcodePreview.layers && gcodePreview.layers.length + ' layers'; - - if (favIcon != gcodePreview.parser.metadata.thumbnails['16x16']) { - favIcon = gcodePreview.parser.metadata.thumbnails['16x16']; - setFavicons(favIcon?.src); - } - - if (thumb != gcodePreview.parser.metadata.thumbnails['220x124']) { - thumb = gcodePreview.parser.metadata.thumbnails['220x124']; - document.getElementById('thumb').src = thumb?.src ?? 'https://via.placeholder.com/120x60?text=noThumbnail'; - } - - showExtrusionColors(); -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars -async function loadGCodeFromServer(filename) { - const response = await fetch(filename); - if (response.status !== 200) { - console.error('ERROR. Status Code: ' + response.status); - return; - } - - const gcode = await response.text(); - _handleGCode(filename, gcode); - fileName.setAttribute('href', filename); -} - -function _handleGCode(filename, text) { - gcode = text; - fileName.innerText = filename; - fileSize.innerText = humanFileSize(text.length); - - updateUI(); - - startLoadingProgressive(text); -} - -async function startLoadingProgressive(gcode) { - startLayer.setAttribute('disabled', 'disabled'); - endLayer.setAttribute('disabled', 'disabled'); - - gcodePreview.clear(); - if (renderProgressive) { - gcodePreview.parser.parseGCode(gcode); - updateUI(); - // await gcodePreview.renderAnimated(Math.ceil(gcodePreview.layers.length / 60)); - } else { - gcodePreview.processGCode(gcode); - } - updateUI(); - - startLayer.removeAttribute('disabled'); - endLayer.removeAttribute('disabled'); -} - -function humanFileSize(size) { - var i = Math.floor(Math.log(size) / Math.log(1024)); - return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; -} - -function setFavicons(favImg) { - const headTitle = document.querySelector('head'); - const setFavicon = document.createElement('link'); - setFavicon.setAttribute('rel', 'shortcut icon'); - setFavicon.setAttribute('href', favImg); - headTitle.appendChild(setFavicon); -} - -let throttleTimer; -const throttle = (callback, time) => { - if (throttleTimer) return; - throttleTimer = true; - setTimeout(() => { - callback(); - throttleTimer = false; - }, time); -}; - -// debounce function -let debounceTimer; -const debounce = (callback) => { - if (debounceTimer) clearTimeout(debounceTimer); - debounceTimer = setTimeout(callback, 300); -}; - -function showExtrusionColors() { - // loop through inputs and show/hide them - for (let i = 0; i < 8; i++) { - // find parent element - const parent = extrusionColor[i].parentNode; - if (i < toolCount) { - parent.style.display = 'flex'; - } else { - parent.style.display = 'none'; - } - } -} diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 846067a9..c9467e4f 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -18,7 +18,6 @@ import { Euler, Fog, Group, - Line, LineBasicMaterial, LineSegments, MeshLambertMaterial, @@ -100,7 +99,7 @@ export class WebGLPreview { private animationFrameId?: number; private renderLayerIndex = 0; private _geometries: Record = {}; - interpreter: Interpreter; + interpreter: Interpreter = new Interpreter(); virtualMachine: Machine = new Machine(); // colors @@ -140,7 +139,6 @@ export class WebGLPreview { this.extrusionWidth = opts.extrusionWidth ?? this.extrusionWidth; this.devMode = opts.devMode ?? this.devMode; this.stats = this.devMode ? new Stats() : undefined; - this.interpreter = new Interpreter(); if (opts.extrusionColor !== undefined) { this.extrusionColor = opts.extrusionColor; @@ -242,7 +240,6 @@ export class WebGLPreview { } processGCode(gcode: string | string[]): void { - console.log('asdf'); const { commands } = this.parser.parseGCode(gcode); this.interpreter.execute(commands, this.virtualMachine); this.render(); @@ -390,6 +387,7 @@ export class WebGLPreview { } private renderGeometries() { + this._geometries = {}; if (Object.keys(this._geometries).length === 0 && this.renderTubes) { let color: number; this.virtualMachine.extrusions().forEach((path) => { @@ -407,11 +405,11 @@ export class WebGLPreview { if (this._geometries) { for (const color in this._geometries) { const batchedMesh = this.createBatchMesh(parseInt(color)); - while (this._geometries[color].length > 0) { - const geometry = this._geometries[color].pop(); + this._geometries[color].forEach((geometry) => { + this.disposables.push(geometry); const geometryId = batchedMesh.addGeometry(geometry); batchedMesh.addInstance(geometryId); - } + }); } } } From 968926f7369132d6a3d518b29cc04c2e0c3443a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Wed, 9 Oct 2024 20:34:59 -0400 Subject: [PATCH 13/34] Remove some layer references --- src/dev-gui.ts | 9 ++-- src/gcode-parser.ts | 97 +------------------------------------------- src/webgl-preview.ts | 8 +--- 3 files changed, 8 insertions(+), 106 deletions(-) diff --git a/src/dev-gui.ts b/src/dev-gui.ts index 9f2b51c4..8393cd2e 100644 --- a/src/dev-gui.ts +++ b/src/dev-gui.ts @@ -116,11 +116,10 @@ class DevGUI { parser.onOpenClose(() => { this.saveOpenFolders(); }); - parser.add(this.watchedObject.parser, 'curZ').listen(); - parser.add(this.watchedObject.parser, 'maxZ').listen(); - parser.add(this.watchedObject.parser, 'tolerance').listen(); - parser.add(this.watchedObject.parser.layers, 'length').name('layers.count').listen(); - parser.add(this.watchedObject.parser.lines, 'length').name('lines.count').listen(); + // parser.add(this.watchedObject.parser, 'curZ').listen(); + // parser.add(this.watchedObject.parser, 'maxZ').listen(); + // parser.add(this.watchedObject.parser, 'tolerance').listen(); + // parser.add(this.watchedObject.parser.lines, 'length').name('lines.count').listen(); } private setupBuildVolumeFolder(): void { diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index 8d4f51b8..e0065342 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -56,18 +56,11 @@ type singleLetter = | 'Z'; type CommandParams = { [key in singleLetter]?: number }; -type MoveCommandParamName = 'x' | 'y' | 'z' | 'r' | 'e' | 'f' | 'i' | 'j'; -type MoveCommandParams = { - [key in MoveCommandParamName]?: number; -}; - export enum Code { G0 = 'G0', G1 = 'G1', G2 = 'G2', G3 = 'G3', - G20 = 'G20', - G21 = 'G21', G28 = 'G28', T0 = 'T0', T1 = 'T1', @@ -125,54 +118,15 @@ export class GCodeCommand { } } -export class MoveCommand extends GCodeCommand { - constructor( - src: string, - gcode: string, - public params: MoveCommandParams, - comment?: string - ) { - super(src, gcode, params, comment); - } -} - type Metadata = { thumbnails: Record }; -export class Layer { - constructor( - public layer: number, - public commands: GCodeCommand[], - public lineNumber: number, - public height: number = 0 - ) {} -} - export class Parser { lines: string[] = []; commands: GCodeCommand[] = []; - /** - * @experimental GCode commands before extrusion starts. - */ - preamble = new Layer(-1, [], 0); // TODO: remove preamble and treat as a regular layer? Unsure of the benefit - layers: Layer[] = []; - curZ = 0; - maxZ = 0; metadata: Metadata = { thumbnails: {} }; - tolerance = 0; // The higher the tolerance, the fewer layers are created, so performance will improve. - - /** - * Create a new Parser instance. - * - * @param minLayerThreshold - If specified, the minimum layer height to be considered a new layer. If not specified, the default value is 0. - * @returns A new Parser instance. - */ - constructor(minLayerThreshold: number) { - this.tolerance = minLayerThreshold ?? this.tolerance; - } parseGCode(input: string | string[]): { - layers: Layer[]; metadata: Metadata; commands: GCodeCommand[]; } { @@ -182,15 +136,13 @@ export class Parser { this.commands = this.lines2commands(lines); - // this.groupIntoLayers(this.commands); - // merge thumbs const thumbs = this.parseMetadata(this.commands.filter((cmd) => cmd.comment)).thumbnails; for (const [key, value] of Object.entries(thumbs)) { this.metadata.thumbnails[key] = value; } - return { layers: this.layers, metadata: this.metadata, commands: this.commands }; + return { metadata: this.metadata, commands: this.commands }; } private lines2commands(lines: string[]) { @@ -210,19 +162,7 @@ export class Parser { const gcode = !parts.length ? '' : `${parts[0]?.toLowerCase()}${parts[1]}`; const params = this.parseParams(parts.slice(2)); - switch (gcode) { - case 'g0': - case 'g00': - case 'g1': - case 'g01': - case 'g2': - case 'g02': - case 'g3': - case 'g03': - return new MoveCommand(line, gcode, params, comment); - default: - return new GCodeCommand(line, gcode, params, comment); - } + return new GCodeCommand(line, gcode, params, comment); } private isAlpha(char: string | singleLetter): char is singleLetter { @@ -243,39 +183,6 @@ export class Parser { }, {}); } - private groupIntoLayers(commands: GCodeCommand[]): Layer[] { - for (let lineNumber = 0; lineNumber < commands.length; lineNumber++) { - const cmd = commands[lineNumber]; - - if (cmd instanceof MoveCommand) { - const params = cmd.params; - - // update current z? - if (params.z) { - this.curZ = params.z; // abs mode - } - - if ( - (params.e ?? 0) > 0 && // extruding? - (params.x != undefined || params.y != undefined) && // moving? - Math.abs(this.curZ - (this.maxZ || -Infinity)) > this.tolerance // new layer? - ) { - const layerHeight = Math.abs(this.curZ - this.maxZ); - this.maxZ = this.curZ; - this.layers.push(new Layer(this.layers.length, [], lineNumber, layerHeight)); - } - } - - this.maxLayer.commands.push(cmd); - } - - return this.layers; - } - - get maxLayer(): Layer { - return this.layers[this.layers.length - 1] ?? this.preamble; - } - parseMetadata(metadata: GCodeCommand[]): Metadata { const thumbnails: Record = {}; diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index c9467e4f..6e84399e 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -46,7 +46,6 @@ export type GCodePreviewOptions = { lineWidth?: number; lineHeight?: number; nonTravelMoves?: string[]; - minLayerThreshold?: number; renderExtrusion?: boolean; renderTravel?: boolean; startLayer?: number; @@ -65,7 +64,6 @@ export type GCodePreviewOptions = { }; export class WebGLPreview { - minLayerThreshold = 0.05; parser: Parser; scene: Scene; camera: PerspectiveCamera; @@ -97,7 +95,6 @@ export class WebGLPreview { static readonly defaultExtrusionColor = new Color('hotpink'); private _extrusionColor: Color | Color[] = WebGLPreview.defaultExtrusionColor; private animationFrameId?: number; - private renderLayerIndex = 0; private _geometries: Record = {}; interpreter: Interpreter = new Interpreter(); virtualMachine: Machine = new Machine(); @@ -118,8 +115,7 @@ export class WebGLPreview { private devGui?: DevGUI; constructor(opts: GCodePreviewOptions) { - this.minLayerThreshold = opts.minLayerThreshold ?? this.minLayerThreshold; - this.parser = new Parser(this.minLayerThreshold); + this.parser = new Parser(); this.scene = new Scene(); this.scene.background = this._backgroundColor; if (opts.backgroundColor !== undefined) { @@ -316,7 +312,7 @@ export class WebGLPreview { // reset parser & processing state clear(): void { this.resetState(); - this.parser = new Parser(this.minLayerThreshold); + this.parser = new Parser(); this.virtualMachine = new Machine(); } From 2063445fbdc58cbf66e2f9a685f23607a460b33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Wed, 9 Oct 2024 20:37:28 -0400 Subject: [PATCH 14/34] Rename Machine to Job --- demo/js/app.js | 10 ++--- src/interpreter.ts | 80 +++++++++++++++++++------------------- src/{machine.ts => job.ts} | 2 +- src/webgl-preview.ts | 14 +++---- 4 files changed, 53 insertions(+), 53 deletions(-) rename src/{machine.ts => job.ts} (98%) diff --git a/demo/js/app.js b/demo/js/app.js index f9621e6d..30f59f12 100644 --- a/demo/js/app.js +++ b/demo/js/app.js @@ -67,16 +67,16 @@ export const app = (window.app = createApp({ lineWidth, renderTubes, extrusionWidth, - virtualMachine + job } = preview; const { thumbnails } = parser.metadata; thumbnail.value = thumbnails['220x124']?.src; - layerCount.value = virtualMachine.layers().length; + layerCount.value = job.layers().length; const colors = extrusionColor instanceof Array ? extrusionColor : [extrusionColor]; const currentSettings = { - maxLayer: virtualMachine.layers().length, - endLayer: virtualMachine.layers().length, + maxLayer: job.layers().length, + endLayer: job.layers().length, singleLayerMode, renderTravel, travelColor: '#' + travelColor.getHexString(), @@ -95,7 +95,7 @@ export const app = (window.app = createApp({ }; Object.assign(settings.value, currentSettings); - preview.endLayer = virtualMachine.layers().length; + preview.endLayer = job.layers().length; }; const loadGCodeFromServer = async (filename) => { diff --git a/src/interpreter.ts b/src/interpreter.ts index 6bee08c9..029843bb 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1,27 +1,27 @@ import { Path, PathType } from './path'; import { Code, GCodeCommand } from './gcode-parser'; -import { Machine } from './machine'; +import { Job } from './job'; export class Interpreter { - execute(commands: GCodeCommand[], machine = new Machine()): Machine { + execute(commands: GCodeCommand[], job = new Job()): Job { commands.forEach((command) => { if (command.code !== undefined) { - this[command.code](command, machine); + this[command.code](command, job); } }); - return machine; + return job; } - G0(command: GCodeCommand, machine: Machine): void { + G0(command: GCodeCommand, job: Job): void { const { x, y, z, e } = command.params; - const { state } = machine; + const { state } = job; - let lastPath = machine.paths[machine.paths.length - 1]; + let lastPath = job.paths[job.paths.length - 1]; const pathType = e ? PathType.Extrusion : PathType.Travel; if (lastPath === undefined || lastPath.travelType !== pathType) { - lastPath = this.breakPath(machine, pathType); + lastPath = this.breakPath(job, pathType); } state.x = x || state.x; @@ -33,17 +33,17 @@ export class Interpreter { G1 = this.G0; - G2(command: GCodeCommand, machine: Machine): void { + G2(command: GCodeCommand, job: Job): void { const { x, y, z, e } = command.params; let { i, j, r } = command.params; - const { state } = machine; + const { state } = job; const cw = command.code === Code.G2; - let lastPath = machine.paths[machine.paths.length - 1]; + let lastPath = job.paths[job.paths.length - 1]; const pathType = e ? PathType.Extrusion : PathType.Travel; if (lastPath === undefined || lastPath.travelType !== pathType) { - lastPath = this.breakPath(machine, pathType); + lastPath = this.breakPath(job, pathType); } if (r) { @@ -130,47 +130,47 @@ export class Interpreter { G3 = this.G2; - G28(command: GCodeCommand, machine: Machine): void { - machine.state.x = 0; - machine.state.y = 0; - machine.state.z = 0; + G28(command: GCodeCommand, job: Job): void { + job.state.x = 0; + job.state.y = 0; + job.state.z = 0; } - T0(command: GCodeCommand, machine: Machine): void { - machine.state.tool = 0; + T0(command: GCodeCommand, job: Job): void { + job.state.tool = 0; } - T1(command: GCodeCommand, machine: Machine): void { - machine.state.tool = 1; + T1(command: GCodeCommand, job: Job): void { + job.state.tool = 1; } - T2(command: GCodeCommand, machine: Machine): void { - machine.state.tool = 2; + T2(command: GCodeCommand, job: Job): void { + job.state.tool = 2; } - T3(command: GCodeCommand, machine: Machine): void { - machine.state.tool = 3; + T3(command: GCodeCommand, job: Job): void { + job.state.tool = 3; } - T4(command: GCodeCommand, machine: Machine): void { - machine.state.tool = 4; + T4(command: GCodeCommand, job: Job): void { + job.state.tool = 4; } - T5(command: GCodeCommand, machine: Machine): void { - machine.state.tool = 5; + T5(command: GCodeCommand, job: Job): void { + job.state.tool = 5; } - T6(command: GCodeCommand, machine: Machine): void { - machine.state.tool = 6; + T6(command: GCodeCommand, job: Job): void { + job.state.tool = 6; } - T7(command: GCodeCommand, machine: Machine): void { - machine.state.tool = 7; + T7(command: GCodeCommand, job: Job): void { + job.state.tool = 7; } - G20(command: GCodeCommand, machine: Machine): void { - machine.state.units = 'in'; + G20(command: GCodeCommand, job: Job): void { + job.state.units = 'in'; } - G21(command: GCodeCommand, machine: Machine): void { - machine.state.units = 'mm'; + G21(command: GCodeCommand, job: Job): void { + job.state.units = 'mm'; } - private breakPath(machine: Machine, newType: PathType): Path { - const lastPath = new Path(newType, 0.6, 0.2, machine.state.tool); - machine.paths.push(lastPath); - lastPath.addPoint(machine.state.x, machine.state.y, machine.state.z); + private breakPath(job: Job, newType: PathType): Path { + const lastPath = new Path(newType, 0.6, 0.2, job.state.tool); + job.paths.push(lastPath); + lastPath.addPoint(job.state.x, job.state.y, job.state.z); return lastPath; } } diff --git a/src/machine.ts b/src/job.ts similarity index 98% rename from src/machine.ts rename to src/job.ts index 06b49a3f..31bed313 100644 --- a/src/machine.ts +++ b/src/job.ts @@ -15,7 +15,7 @@ export class State { } } -export class Machine { +export class Job { paths: Path[]; state: State; diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 6e84399e..a19beade 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -6,7 +6,7 @@ import Stats from 'three/examples/jsm/libs/stats.module.js'; import { DevGUI, DevModeOptions } from './dev-gui'; import { Interpreter } from './interpreter'; -import { Machine } from './machine'; +import { Job } from './job'; import { AmbientLight, @@ -97,7 +97,7 @@ export class WebGLPreview { private animationFrameId?: number; private _geometries: Record = {}; interpreter: Interpreter = new Interpreter(); - virtualMachine: Machine = new Machine(); + job: Job = new Job(); // colors private _backgroundColor = new Color(0xe0e0e0); @@ -237,7 +237,7 @@ export class WebGLPreview { processGCode(gcode: string | string[]): void { const { commands } = this.parser.parseGCode(gcode); - this.interpreter.execute(commands, this.virtualMachine); + this.interpreter.execute(commands, this.job); this.render(); } @@ -313,7 +313,7 @@ export class WebGLPreview { clear(): void { this.resetState(); this.parser = new Parser(); - this.virtualMachine = new Machine(); + this.job = new Job(); } // reset processing state @@ -353,7 +353,7 @@ export class WebGLPreview { const material = new LineBasicMaterial({ color: this._travelColor, linewidth: this.lineWidth }); this.disposables.push(material); - this.virtualMachine.travels().forEach((path) => { + this.job.travels().forEach((path) => { const geometry = path.line(); const line = new LineSegments(geometry, material); this.group?.add(line); @@ -374,7 +374,7 @@ export class WebGLPreview { }); } - this.virtualMachine.extrusions().forEach((path) => { + this.job.extrusions().forEach((path) => { const geometry = path.line(); const line = new LineSegments(geometry, lineMaterials[path.tool]); this.group?.add(line); @@ -386,7 +386,7 @@ export class WebGLPreview { this._geometries = {}; if (Object.keys(this._geometries).length === 0 && this.renderTubes) { let color: number; - this.virtualMachine.extrusions().forEach((path) => { + this.job.extrusions().forEach((path) => { if (Array.isArray(this._extrusionColor)) { color = this._extrusionColor[path.tool].getHex(); } else { From a5069bf7eba543cb9a47804230db26cab5e08dac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Wed, 9 Oct 2024 20:50:23 -0400 Subject: [PATCH 15/34] Simplify parsing attributes --- src/gcode-parser.ts | 57 ++------------------------------------------- 1 file changed, 2 insertions(+), 55 deletions(-) diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index e0065342..3d954b28 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -1,60 +1,7 @@ /* eslint-disable no-unused-vars */ import { Thumbnail } from './thumbnail'; -type singleLetter = - | 'a' - | 'b' - | 'c' - | 'd' - | 'e' - | 'f' - | 'g' - | 'h' - | 'i' - | 'j' - | 'k' - | 'l' - | 'm' - | 'n' - | 'o' - | 'p' - | 'q' - | 'r' - | 's' - | 't' - | 'u' - | 'v' - | 'w' - | 'x' - | 'y' - | 'z' - | 'A' - | 'B' - | 'C' - | 'D' - | 'E' - | 'F' - | 'G' - | 'H' - | 'I' - | 'J' - | 'K' - | 'L' - | 'M' - | 'N' - | 'O' - | 'P' - | 'Q' - | 'R' - | 'S' - | 'T' - | 'U' - | 'V' - | 'W' - | 'X' - | 'Y' - | 'Z'; -type CommandParams = { [key in singleLetter]?: number }; +type CommandParams = Record; export enum Code { G0 = 'G0', @@ -165,7 +112,7 @@ export class Parser { return new GCodeCommand(line, gcode, params, comment); } - private isAlpha(char: string | singleLetter): char is singleLetter { + private isAlpha(char: string): char is string { const code = char.charCodeAt(0); return (code >= 97 && code <= 122) || (code >= 65 && code <= 90); } From 0ab74da5858b740faa65f25dbff0600ba6cf5ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Wed, 9 Oct 2024 20:59:19 -0400 Subject: [PATCH 16/34] Am I going too far? --- src/gcode-parser.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index 3d954b28..ecd2f60a 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -68,9 +68,6 @@ export class GCodeCommand { type Metadata = { thumbnails: Record }; export class Parser { - lines: string[] = []; - commands: GCodeCommand[] = []; - metadata: Metadata = { thumbnails: {} }; parseGCode(input: string | string[]): { @@ -78,18 +75,15 @@ export class Parser { commands: GCodeCommand[]; } { const lines = Array.isArray(input) ? input : input.split('\n'); - - this.lines = this.lines.concat(lines); - - this.commands = this.lines2commands(lines); + const commands = this.lines2commands(lines); // merge thumbs - const thumbs = this.parseMetadata(this.commands.filter((cmd) => cmd.comment)).thumbnails; + const thumbs = this.parseMetadata(commands.filter((cmd) => cmd.comment)).thumbnails; for (const [key, value] of Object.entries(thumbs)) { this.metadata.thumbnails[key] = value; } - return { metadata: this.metadata, commands: this.commands }; + return { metadata: this.metadata, commands: commands }; } private lines2commands(lines: string[]) { From 2416e7baef92e88669b81ac3bccfa83ec3cbc4f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Wed, 9 Oct 2024 21:20:40 -0400 Subject: [PATCH 17/34] get rid of geometries once used --- src/path.ts | 14 +++++--------- src/webgl-preview.ts | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/path.ts b/src/path.ts index 53989cd8..8a89bdf3 100644 --- a/src/path.ts +++ b/src/path.ts @@ -8,11 +8,10 @@ export enum PathType { } export class Path { - travelType: PathType; + public travelType: PathType; public vertices: number[]; extrusionWidth: number; lineHeight: number; - geometryCache: BufferGeometry | undefined; tool: number; constructor(travelType: PathType, extrusionWidth = 0.6, lineHeight = 0.2, tool = 0) { @@ -49,14 +48,11 @@ export class Path { } geometry(): BufferGeometry { - if (!this.geometryCache) { - if (this.vertices.length < 3) { - return new BufferGeometry(); - } - - this.geometryCache = new ExtrusionGeometry(this.path(), this.extrusionWidth, this.lineHeight, 4); + if (this.vertices.length < 3) { + return new BufferGeometry(); } - return this.geometryCache; + + return new ExtrusionGeometry(this.path(), this.extrusionWidth, this.lineHeight, 4); } line(): BufferGeometry { diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index a19beade..14bd9884 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -383,7 +383,6 @@ export class WebGLPreview { } private renderGeometries() { - this._geometries = {}; if (Object.keys(this._geometries).length === 0 && this.renderTubes) { let color: number; this.job.extrusions().forEach((path) => { @@ -408,6 +407,7 @@ export class WebGLPreview { }); } } + this._geometries = {}; } private createBatchMesh(color: number): BatchedMesh { From 0b3fa37df048fd14c2513fedec07c4ddec6f7c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Wed, 9 Oct 2024 22:04:23 -0400 Subject: [PATCH 18/34] the geometries disposition is handled by the batchMesh --- src/webgl-preview.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 14bd9884..5466a2a7 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -397,15 +397,13 @@ export class WebGLPreview { }); } - if (this._geometries) { - for (const color in this._geometries) { - const batchedMesh = this.createBatchMesh(parseInt(color)); - this._geometries[color].forEach((geometry) => { - this.disposables.push(geometry); - const geometryId = batchedMesh.addGeometry(geometry); - batchedMesh.addInstance(geometryId); - }); - } + for (const color in this._geometries) { + const batchedMesh = this.createBatchMesh(parseInt(color)); + this._geometries[color].forEach((geometry) => { + const geometryId = batchedMesh.addGeometry(geometry); + batchedMesh.addInstance(geometryId); + }); + this._geometries[color] = []; } this._geometries = {}; } From f15421b4c187819e36f4ffce95f720c0862a7b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Thu, 10 Oct 2024 17:17:23 -0400 Subject: [PATCH 19/34] Fix tests --- demo/js/app.js | 4 ++-- src/__tests__/webgl-preview.ts | 24 ++---------------------- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/demo/js/app.js b/demo/js/app.js index 30f59f12..8552ad37 100644 --- a/demo/js/app.js +++ b/demo/js/app.js @@ -115,8 +115,8 @@ export const app = (window.app = createApp({ preview.clear(); preview.devMode = prevDevMode; if (loadProgressive) { - preview.parser.parseGCode(gcode); - // await preview.renderAnimated(Math.ceil(preview.layers.length / 60)); + preview.processGCode(gcode); + // await preview.renderAnimated(Math.ceil(preview.job.layers().length / 60)); } else { preview.processGCode(gcode); } diff --git a/src/__tests__/webgl-preview.ts b/src/__tests__/webgl-preview.ts index c89985fd..e06f0731 100644 --- a/src/__tests__/webgl-preview.ts +++ b/src/__tests__/webgl-preview.ts @@ -2,29 +2,9 @@ import { test, expect, vi, assert } from 'vitest'; -import { State, WebGLPreview } from '../webgl-preview'; +import { WebGLPreview } from '../webgl-preview'; import { GCodeCommand } from '../gcode-parser'; -test('in gcode x,y,z params should update the state', () => { - const mock = createMockPreview(); - mock.layers[0].commands.push(new GCodeCommand('', 'g0', { x: 1, y: 1, z: 1, e: 1 }, undefined)); - const layerIndex = 0; - WebGLPreview.prototype.renderLayer.call(mock, layerIndex); - expect(mock.state.x).toBe(1); - expect(mock.state.y).toBe(1); - expect(mock.state.z).toBe(1); -}); - -test('x,y,z params can go to 0', () => { - const mock = createMockPreview(); - mock.layers[0].commands.push(new GCodeCommand('', 'g0', { x: 0, y: 0, z: 0, e: 0 }, undefined)); - const layerIndex = 0; - WebGLPreview.prototype.renderLayer.call(mock, layerIndex); - expect(mock.state.x).toBe(0); - expect(mock.state.y).toBe(0); - expect(mock.state.z).toBe(0); -}); - // add a test for destroying the preview which should cancel the render loop. test('destroying the preview should dispose renderer and controls', async () => { const mock = createMockPreview(); @@ -86,7 +66,7 @@ test('cancelAnimation should cancel the render loop', async () => { function createMockPreview() { return { - state: State.initial, + // state: State.initial, minLayerIndex: 0, maxLayerIndex: Infinity, disposables: [ From 7f11d9de8dd65731647a9b9469e987bd394a7bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Thu, 10 Oct 2024 18:16:50 -0400 Subject: [PATCH 20/34] Bring back some code --- src/__tests__/gcode-parser.ts | 217 ++++++++-------------------------- src/gcode-parser.ts | 62 +++++++++- 2 files changed, 108 insertions(+), 171 deletions(-) diff --git a/src/__tests__/gcode-parser.ts b/src/__tests__/gcode-parser.ts index c6177f78..1af316ab 100644 --- a/src/__tests__/gcode-parser.ts +++ b/src/__tests__/gcode-parser.ts @@ -1,243 +1,126 @@ import { test, expect } from 'vitest'; -import { GCodeCommand, MoveCommand, Parser, SelectToolCommand } from '../gcode-parser'; +import { GCodeCommand, Parser } from '../gcode-parser'; -test('a single extrusion cmd should result in 1 layer with 1 command', () => { - const parser = new Parser(0); +test('a single extrusion cmd should result in 1 command', () => { + const parser = new Parser(); const gcode = `G1 X0 Y0 Z1 E1`; const parsed = parser.parseGCode(gcode); expect(parsed).not.toBeNull(); - expect(parsed.layers).not.toBeNull(); - expect(parsed.layers.length).toEqual(1); - expect(parsed.layers[0].commands).not.toBeNull(); - expect(parsed.layers[0].commands.length).toEqual(1); + expect(parsed.commands).not.toBeNull(); + expect(parsed.commands.length).toEqual(1); }); -test('a gcode cmd w/o extrusion should not result in a layer', () => { - const parser = new Parser(0); - const gcode = `G1 X0 Y0 Z1`; +test('a single extrusion cmd should parse attributes', () => { + const parser = new Parser(); + const gcode = `G1 X5 Y6 Z3 E1.9`; const parsed = parser.parseGCode(gcode); - expect(parsed).not.toBeNull(); - expect(parsed.layers).not.toBeNull(); - expect(parsed.layers.length).toEqual(0); -}); - -test('a gcode cmd with 0 extrusion should not result in a layer', () => { - const parser = new Parser(0); - const gcode = `G1 X0 Y0 Z1 E0`; - const parsed = parser.parseGCode(gcode); - expect(parsed).not.toBeNull(); - expect(parsed.layers).not.toBeNull(); - expect(parsed.layers.length).toEqual(0); + const cmd = parsed.commands[0]; + expect(cmd.params.x).toEqual(5); + expect(cmd.params.y).toEqual(6); + expect(cmd.params.z).toEqual(3); + expect(cmd.params.e).toEqual(1.9); }); -test('2 horizontal extrusion moves should result in 1 layer with 2 commands', () => { - const parser = new Parser(0); - const gcode = `G1 X0 Y0 Z1 E1 - G1 X10 Y10 Z1 E2`; +test('multiple cmd results in an array of commands', () => { + const parser = new Parser(); + const gcode = `G1 X5 Y6 Z3 E1.9 + G1 X6 Y6 E1.9 + G1 X5 Y7 E1.9`; const parsed = parser.parseGCode(gcode); - expect(parsed).not.toBeNull(); - expect(parsed.layers).not.toBeNull(); - expect(parsed.layers.length).toEqual(1); - expect(parsed.layers[0].commands).not.toBeNull(); - expect(parsed.layers[0].commands.length).toEqual(2); -}); - -test('2 vertical extrusion moves should result in 2 layers with 1 command', () => { - const parser = new Parser(0); - const gcode = `G1 X0 Y0 Z1 E1 - G1 X0 Y0 Z2 E2`; - const parsed = parser.parseGCode(gcode); - expect(parsed).not.toBeNull(); - expect(parsed.layers).not.toBeNull(); - expect(parsed.layers.length).toEqual(2); - expect(parsed.layers[0].commands).not.toBeNull(); - expect(parsed.layers[0].commands.length).toEqual(1); -}); - -test('2 vertical extrusion moves in consecutive gcode chunks should result in 2 layers with 1 command', () => { - const parser = new Parser(0); - const gcode1 = 'G1 X0 Y0 Z1 E1'; - const gcode2 = 'G1 X0 Y0 Z2 E2'; - const parsed = parser.parseGCode(gcode1); - parser.parseGCode(gcode2); - expect(parsed).not.toBeNull(); - expect(parsed.layers).not.toBeNull(); - expect(parsed.layers.length).toEqual(2); - expect(parsed.layers[0].commands).not.toBeNull(); - expect(parsed.layers[0].commands.length).toEqual(1); -}); - -test('2 vertical extrusion moves in consecutive gcode chunks as string arrays should result in 2 layers with 1 command', () => { - const parser = new Parser(0); - const gcode1 = ['G1 X0 Y0 Z1 E1']; - const gcode2 = ['G1 X0 Y0 Z2 E2']; - const parsed = parser.parseGCode(gcode1); - parser.parseGCode(gcode2); - expect(parsed).not.toBeNull(); - expect(parsed.layers).not.toBeNull(); - expect(parsed.layers.length).toEqual(2); - expect(parsed.layers[0].commands).not.toBeNull(); - expect(parsed.layers[0].commands.length).toEqual(1); -}); - -test('2 extrusion moves with a z difference below the threshold should result in only 1 layer', () => { - const threshold = 1; - const parser = new Parser(threshold); - const gcode = `G1 X0 Y0 Z1 E1 - G1 X10 Y10 Z1.5 E2`; - const parsed = parser.parseGCode(gcode); - expect(parsed).not.toBeNull(); - expect(parsed.layers).not.toBeNull(); - expect(parsed.layers.length).toEqual(1); - expect(parsed.layers[0].commands).not.toBeNull(); - expect(parsed.layers[0].commands.length).toEqual(2); -}); - -test('2 extrusion moves with a z difference above the threshold should result in 2 layers', () => { - const threshold = 1; - const parser = new Parser(threshold); - const gcode = `G1 X0 Y0 Z1 E1 - G1 X10 Y10 Z3 E2`; - const parsed = parser.parseGCode(gcode); - expect(parsed).not.toBeNull(); - expect(parsed.layers).not.toBeNull(); - expect(parsed.layers.length).toEqual(2); - expect(parsed.layers[0].commands).not.toBeNull(); - expect(parsed.layers[0].commands.length).toEqual(1); - expect(parsed.layers[0].commands).not.toBeNull(); - expect(parsed.layers[0].commands.length).toEqual(1); -}); - -test('2 extrusion moves with a z diff exactly at the threshold should result in 1 layer', () => { - const threshold = 1; - const parser = new Parser(threshold); - const gcode = `G1 X0 Y0 Z1 E1 - G1 X10 Y10 Z2 E2`; - const parsed = parser.parseGCode(gcode); - expect(parsed).not.toBeNull(); - expect(parsed.layers).not.toBeNull(); - expect(parsed.layers.length).toEqual(1); - expect(parsed.layers[0].commands).not.toBeNull(); - expect(parsed.layers[0].commands.length).toEqual(2); -}); - -test('Layers should have calculated heights', () => { - const threshold = 0.05; - const parser = new Parser(threshold); - const gcode = `G0 X0 Y0 Z0.1 E1 - G1 X10 Y10 Z0.2 E2 - G1 X20 Y20 Z0.3 E3 - G1 X30 Y30 Z0.5 E4 - G1 X40 Y40 Z0.8 E5 - `; - const parsed = parser.parseGCode(gcode); - expect(parsed).not.toBeNull(); - expect(parsed.layers).not.toBeNull(); - expect(parsed.layers.length).toEqual(5); - expect(parsed.layers[0].height).toEqual(expect.closeTo(0.1, 3)); - expect(parsed.layers[1].height).toEqual(expect.closeTo(0.1, 3)); - expect(parsed.layers[2].height).toEqual(expect.closeTo(0.1, 3)); - expect(parsed.layers[3].height).toEqual(expect.closeTo(0.2, 3)); + expect(parsed.commands).not.toBeNull(); + expect(parsed.commands.length).toEqual(3); }); -test('T0 command should result in a tool change to tool with index 0', () => { - const parser = new Parser(0); +test('T0 command should result in a tool change', () => { + const parser = new Parser(); const gcode = `G1 X0 Y0 Z1 E1 T0`; const parsed = parser.parseGCode(gcode); expect(parsed).not.toBeNull(); - expect(parsed.layers).not.toBeNull(); - expect(parsed.layers.length).toEqual(1); - expect(parsed.layers[0].commands).not.toBeNull(); + expect(parsed.commands).not.toBeNull(); + expect(parsed.commands.length).toEqual(2); - const cmd = parsed.layers[0].commands[1] as SelectToolCommand; + const cmd = parsed.commands[1]; expect(cmd.gcode).toEqual('t0'); - expect(cmd.toolIndex).toEqual(0); }); -test('T1 command should result in a tool change to tool with index 0', () => { - const parser = new Parser(0); +test('T1 command should result in a tool change', () => { + const parser = new Parser(); const gcode = `G1 X0 Y0 Z1 E1 T1`; const parsed = parser.parseGCode(gcode); - const cmd = parsed.layers[0].commands[1] as SelectToolCommand; + const cmd = parsed.commands[1]; expect(cmd.gcode).toEqual('t1'); - expect(cmd.toolIndex).toEqual(1); }); -test('T2 command should result in a tool change to tool with index 0', () => { - const parser = new Parser(0); +test('T2 command should result in a tool change', () => { + const parser = new Parser(); const gcode = `G1 X0 Y0 Z1 E1 T2`; const parsed = parser.parseGCode(gcode); - const cmd = parsed.layers[0].commands[1] as SelectToolCommand; + const cmd = parsed.commands[1]; expect(cmd.gcode).toEqual('t2'); - expect(cmd.toolIndex).toEqual(2); }); -test('T3 command should result in a tool change to tool with index 0', () => { - const parser = new Parser(0); +test('T3 command should result in a tool change', () => { + const parser = new Parser(); const gcode = `G1 X0 Y0 Z1 E1 T3`; const parsed = parser.parseGCode(gcode); - const cmd = parsed.layers[0].commands[1] as SelectToolCommand; + const cmd = parsed.commands[1]; expect(cmd.gcode).toEqual('t3'); - expect(cmd.toolIndex).toEqual(3); }); // repeat fot T4 .. T7 -test('T4 command should result in a tool change to tool with index 0', () => { - const parser = new Parser(0); +test('T4 command should result in a tool change', () => { + const parser = new Parser(); const gcode = `G1 X0 Y0 Z1 E1 T4`; const parsed = parser.parseGCode(gcode); - const cmd = parsed.layers[0].commands[1] as SelectToolCommand; + const cmd = parsed.commands[1]; expect(cmd.gcode).toEqual('t4'); - expect(cmd.toolIndex).toEqual(4); }); -test('T5 command should result in a tool change to tool with index 0', () => { - const parser = new Parser(0); +test('T5 command should result in a tool change', () => { + const parser = new Parser(); const gcode = `G1 X0 Y0 Z1 E1 T5`; const parsed = parser.parseGCode(gcode); - const cmd = parsed.layers[0].commands[1] as SelectToolCommand; + const cmd = parsed.commands[1]; expect(cmd.gcode).toEqual('t5'); - expect(cmd.toolIndex).toEqual(5); }); -test('T6 command should result in a tool change to tool with index 0', () => { - const parser = new Parser(0); +test('T6 command should result in a tool change', () => { + const parser = new Parser(); const gcode = `G1 X0 Y0 Z1 E1 T6`; const parsed = parser.parseGCode(gcode); - const cmd = parsed.layers[0].commands[1] as SelectToolCommand; + const cmd = parsed.commands[1]; expect(cmd.gcode).toEqual('t6'); - expect(cmd.toolIndex).toEqual(6); }); -test('T7 command should result in a tool change to tool with index 0', () => { - const parser = new Parser(0); +test('T7 command should result in a tool change', () => { + const parser = new Parser(); const gcode = `G1 X0 Y0 Z1 E1 T7`; const parsed = parser.parseGCode(gcode); - const cmd = parsed.layers[0].commands[1] as SelectToolCommand; + const cmd = parsed.commands[1]; expect(cmd.gcode).toEqual('t7'); - expect(cmd.toolIndex).toEqual(7); }); test('gcode commands with spaces between letters and numbers should be parsed correctly', () => { - const parser = new Parser(0); + const parser = new Parser(); const gcode = `G 1 E 42 X 42`; const parsed = parser.parseGCode(gcode); - const cmd = parsed.layers[0].commands[0] as MoveCommand; + const cmd = parsed.commands[0]; expect(cmd.gcode).toEqual('g1'); expect(cmd.params.x).toEqual(42); + expect(cmd.params.e).toEqual(42); }); // test that a line withouth a gcode command results in a command with empty string gcode test('gcode commands without gcode should result in a command with empty string gcode', () => { - const parser = new Parser(0); + const parser = new Parser(); const gcode = ` ; comment`; const cmd = parser.parseCommand(gcode) as GCodeCommand; expect(cmd.gcode).toEqual(''); diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index ecd2f60a..0470d12a 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -1,7 +1,60 @@ /* eslint-disable no-unused-vars */ import { Thumbnail } from './thumbnail'; -type CommandParams = Record; +type singleLetter = + | 'a' + | 'b' + | 'c' + | 'd' + | 'e' + | 'f' + | 'g' + | 'h' + | 'i' + | 'j' + | 'k' + | 'l' + | 'm' + | 'n' + | 'o' + | 'p' + | 'q' + | 'r' + | 's' + | 't' + | 'u' + | 'v' + | 'w' + | 'x' + | 'y' + | 'z' + | 'A' + | 'B' + | 'C' + | 'D' + | 'E' + | 'F' + | 'G' + | 'H' + | 'I' + | 'J' + | 'K' + | 'L' + | 'M' + | 'N' + | 'O' + | 'P' + | 'Q' + | 'R' + | 'S' + | 'T' + | 'U' + | 'V' + | 'W' + | 'X' + | 'Y' + | 'Z'; +type CommandParams = { [key in singleLetter]?: number }; export enum Code { G0 = 'G0', @@ -69,13 +122,14 @@ type Metadata = { thumbnails: Record }; export class Parser { metadata: Metadata = { thumbnails: {} }; + lines: string[] = []; parseGCode(input: string | string[]): { metadata: Metadata; commands: GCodeCommand[]; } { - const lines = Array.isArray(input) ? input : input.split('\n'); - const commands = this.lines2commands(lines); + this.lines = Array.isArray(input) ? input : input.split('\n'); + const commands = this.lines2commands(this.lines); // merge thumbs const thumbs = this.parseMetadata(commands.filter((cmd) => cmd.comment)).thumbnails; @@ -106,7 +160,7 @@ export class Parser { return new GCodeCommand(line, gcode, params, comment); } - private isAlpha(char: string): char is string { + private isAlpha(char: string | singleLetter): char is singleLetter { const code = char.charCodeAt(0); return (code >= 97 && code <= 122) || (code >= 65 && code <= 90); } From b313c4a562051eab13be65575bdc9e07a202aa09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Thu, 10 Oct 2024 23:10:30 -0400 Subject: [PATCH 21/34] Bring back progressive rendering --- demo/js/app.js | 38 ++++++++++++++---------- src/webgl-preview.ts | 70 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 82 insertions(+), 26 deletions(-) diff --git a/demo/js/app.js b/demo/js/app.js index 8552ad37..00a163fb 100644 --- a/demo/js/app.js +++ b/demo/js/app.js @@ -9,10 +9,10 @@ const preferDarkMode = window.matchMedia('(prefers-color-scheme: dark)'); const initialBackgroundColor = preferDarkMode.matches ? '#141414' : '#eee'; const statsContainer = () => document.querySelector('.sidebar'); -const loadProgressive = false; +const loadProgressive = true; let observer = null; let preview = null; -let firstRender = true; +let activeRendering = true; export const app = (window.app = createApp({ setup() { @@ -54,7 +54,6 @@ export const app = (window.app = createApp({ const updateUI = async () => { const { parser, - // layers, extrusionColor, topLayerColor, lastSegmentColor, @@ -72,11 +71,11 @@ export const app = (window.app = createApp({ const { thumbnails } = parser.metadata; thumbnail.value = thumbnails['220x124']?.src; - layerCount.value = job.layers().length; + layerCount.value = job.layers()?.length; const colors = extrusionColor instanceof Array ? extrusionColor : [extrusionColor]; const currentSettings = { - maxLayer: job.layers().length, - endLayer: job.layers().length, + maxLayer: job.layers()?.length, + endLayer: job.layers()?.length, singleLayerMode, renderTravel, travelColor: '#' + travelColor.getHexString(), @@ -95,7 +94,7 @@ export const app = (window.app = createApp({ }; Object.assign(settings.value, currentSettings); - preview.endLayer = job.layers().length; + preview.endLayer = job.layers()?.length; }; const loadGCodeFromServer = async (filename) => { @@ -114,11 +113,19 @@ export const app = (window.app = createApp({ const prevDevMode = preview.devMode; preview.clear(); preview.devMode = prevDevMode; + const { commands } = preview.parser.parseGCode(gcode); + preview.interpreter.execute(commands, preview.job); if (loadProgressive) { - preview.processGCode(gcode); - // await preview.renderAnimated(Math.ceil(preview.job.layers().length / 60)); + if (preview.job.layers() === null) { + console.warn('Job is not planar'); + preview.render(); + return; + } + activeRendering = true; + await preview.renderAnimated(Math.ceil(preview.job.layers().length / 60)); + activeRendering = false; } else { - preview.processGCode(gcode); + preview.render(); } }; @@ -132,7 +139,7 @@ export const app = (window.app = createApp({ }; const selectPreset = async (presetName) => { - firstRender = true; + activeRendering = true; const canvas = document.querySelector('canvas.preview'); const preset = presets[presetName]; fileName.value = preset.file.replace(/^.*?\//, ''); @@ -182,10 +189,8 @@ export const app = (window.app = createApp({ preview.buildVolume = settings.value.drawBuildVolume ? settings.value.buildVolume : undefined; preview.backgroundColor = settings.value.backgroundColor; - if (!firstRender) { + if (!activeRendering) { preview.render(); - } else { - firstRender = false; } }); @@ -206,8 +211,9 @@ export const app = (window.app = createApp({ preview.lastSegmentColor = settings.value.highlightLastSegment ? settings.value.lastSegmentColor : undefined; debounce(() => { - // preview.renderAnimated(Math.ceil(preview.layers.length / 60)); - preview.render(); + if (!activeRendering) { + preview.render(); + } }); }); }); diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 5466a2a7..d07358ff 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -64,7 +64,6 @@ export type GCodePreviewOptions = { }; export class WebGLPreview { - parser: Parser; scene: Scene; camera: PerspectiveCamera; renderer: WebGLRenderer; @@ -89,15 +88,18 @@ export class WebGLPreview { nonTravelmoves: string[] = []; disableGradient = false; + interpreter = new Interpreter(); + job = new Job(); + parser = new Parser(); + // rendering private group?: Group; private disposables: { dispose(): void }[] = []; static readonly defaultExtrusionColor = new Color('hotpink'); private _extrusionColor: Color | Color[] = WebGLPreview.defaultExtrusionColor; private animationFrameId?: number; + private renderLayerIndex?: number; private _geometries: Record = {}; - interpreter: Interpreter = new Interpreter(); - job: Job = new Job(); // colors private _backgroundColor = new Color(0xe0e0e0); @@ -115,7 +117,6 @@ export class WebGLPreview { private devGui?: DevGUI; constructor(opts: GCodePreviewOptions) { - this.parser = new Parser(); this.scene = new Scene(); this.scene.background = this._backgroundColor; if (opts.backgroundColor !== undefined) { @@ -297,6 +298,56 @@ export class WebGLPreview { this._lastRenderTime = performance.now() - startRender; } + // create a new render method to use an animation loop to render the layers incrementally + /** @experimental */ + async renderAnimated(layerCount = 1): Promise { + this.initScene(); + + this.renderLayerIndex = 0; + + if (this.job.layers() === null) { + console.warn('Job is not planar'); + this.render(); + return; + } + + return this.renderFrameLoop(layerCount > 0 ? layerCount : 1); + } + + private renderFrameLoop(layerCount: number): Promise { + return new Promise((resolve) => { + const loop = () => { + if (this.renderLayerIndex >= this.job.layers().length - 1) { + resolve(); + } else { + this.renderFrame(layerCount); + requestAnimationFrame(loop); + } + }; + loop(); + }); + } + + private renderFrame(layerCount: number): void { + this.group = this.createGroup('layer' + this.renderLayerIndex); + + const endIndex = Math.min(this.renderLayerIndex + layerCount, this.job.layers().length - 1); + const layersToRender = this.job + .layers() + .slice(this.renderLayerIndex, endIndex) + .flatMap((l) => l); + + this.renderGeometries(layersToRender.filter((path) => path.travelType === 'Extrusion')); + this.renderLines( + layersToRender.filter((path) => path.travelType === 'Travel'), + layersToRender.filter((path) => path.travelType === 'Extrusion') + ); + + this.renderLayerIndex = endIndex; + + this.scene.add(this.group); + } + /** @internal */ drawBuildVolume(): void { if (!this.buildVolume) return; @@ -347,13 +398,12 @@ export class WebGLPreview { this.animationFrameId = undefined; } - private renderLines() { - console.log('rendering lines'); + private renderLines(travels = this.job.travels(), extrusions = this.job.extrusions()): void { if (this.renderTravel) { const material = new LineBasicMaterial({ color: this._travelColor, linewidth: this.lineWidth }); this.disposables.push(material); - this.job.travels().forEach((path) => { + travels.forEach((path) => { const geometry = path.line(); const line = new LineSegments(geometry, material); this.group?.add(line); @@ -374,7 +424,7 @@ export class WebGLPreview { }); } - this.job.extrusions().forEach((path) => { + extrusions.forEach((path) => { const geometry = path.line(); const line = new LineSegments(geometry, lineMaterials[path.tool]); this.group?.add(line); @@ -382,10 +432,10 @@ export class WebGLPreview { } } - private renderGeometries() { + private renderGeometries(paths = this.job.extrusions()): void { if (Object.keys(this._geometries).length === 0 && this.renderTubes) { let color: number; - this.job.extrusions().forEach((path) => { + paths.forEach((path) => { if (Array.isArray(this._extrusionColor)) { color = this._extrusionColor[path.tool].getHex(); } else { From a05869bacb4cf3aeb5934c44f74db1e895b6db0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Thu, 10 Oct 2024 23:23:56 -0400 Subject: [PATCH 22/34] update dev-gui with job --- src/dev-gui.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/dev-gui.ts b/src/dev-gui.ts index 8393cd2e..5ec5a899 100644 --- a/src/dev-gui.ts +++ b/src/dev-gui.ts @@ -109,17 +109,18 @@ class DevGUI { } private setupParserFolder(): void { - const parser = this.gui.addFolder('Parser'); - if (!this.openFolders.includes('Parser')) { + const parser = this.gui.addFolder('Job'); + if (!this.openFolders.includes('Job')) { parser.close(); } parser.onOpenClose(() => { this.saveOpenFolders(); }); - // parser.add(this.watchedObject.parser, 'curZ').listen(); - // parser.add(this.watchedObject.parser, 'maxZ').listen(); - // parser.add(this.watchedObject.parser, 'tolerance').listen(); - // parser.add(this.watchedObject.parser.lines, 'length').name('lines.count').listen(); + parser.add(this.watchedObject.job.state, 'x').listen(); + parser.add(this.watchedObject.job.state, 'y').listen(); + parser.add(this.watchedObject.job.state, 'z').listen(); + parser.add(this.watchedObject.job.paths, 'length').name('paths.count').listen(); + parser.add(this.watchedObject.parser.lines, 'length').name('lines.count').listen(); } private setupBuildVolumeFolder(): void { From 605f133c2914ca3d2a47b8c7bb7ae9f91d13f7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Thu, 10 Oct 2024 23:54:59 -0400 Subject: [PATCH 23/34] Test Path --- src/__tests__/path.ts | 129 +++++++++++++++++++++++++++++ src/__tests__/preserving-parser.ts | 6 +- 2 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/path.ts diff --git a/src/__tests__/path.ts b/src/__tests__/path.ts new file mode 100644 index 00000000..af040e0b --- /dev/null +++ b/src/__tests__/path.ts @@ -0,0 +1,129 @@ +import { test, expect } from 'vitest'; +import { Path, PathType } from '../path'; +import { ExtrusionGeometry } from '../extrusion-geometry'; +import { BufferGeometry } from 'three'; + +test('.addPoint adds a point to the vertices', () => { + const path = new Path(PathType.Travel, undefined, undefined, undefined); + + path.addPoint(1, 2, 3); + + expect(path.vertices).not.toBeNull(); + expect(path.vertices.length).toEqual(3); +}); + +test('.addPoint adds points at the end of vertices', () => { + const path = new Path(PathType.Travel, undefined, undefined, undefined); + + path.addPoint(0, 0, 0); + path.addPoint(1, 2, 3); + path.addPoint(5, 6, 7); + + expect(path.vertices).not.toBeNull(); + expect(path.vertices.length).toEqual(9); + expect(path.vertices[6]).toEqual(5); + expect(path.vertices[7]).toEqual(6); + expect(path.vertices[8]).toEqual(7); +}); + +test('.checkLineContinuity returns false if there are less than 3 vertices', () => { + const path = new Path(PathType.Travel, undefined, undefined, undefined); + + expect(path.checkLineContinuity(0, 0, 0)).toBeFalsy(); +}); + +test('.checkLineContinuity returns false if the last point is different', () => { + const path = new Path(PathType.Travel, undefined, undefined, undefined); + + path.addPoint(0, 0, 0); + path.addPoint(1, 2, 3); + + expect(path.checkLineContinuity(1, 2, 4)).toBeFalsy(); +}); + +test('.checkLineContinuity returns true if the last point is the same', () => { + const path = new Path(PathType.Travel, undefined, undefined, undefined); + + path.addPoint(0, 0, 0); + path.addPoint(1, 2, 3); + + expect(path.checkLineContinuity(1, 2, 3)).toBeTruthy(); +}); + +test('.path returns an array of Vector3', () => { + const path = new Path(PathType.Travel, undefined, undefined, undefined); + + path.addPoint(0, 0, 0); + path.addPoint(1, 2, 3); + + const result = path.path(); + + expect(result).not.toBeNull(); + expect(result.length).toEqual(2); + expect(result[0]).toEqual({ x: 0, y: 0, z: 0 }); + expect(result[1]).toEqual({ x: 1, y: 2, z: 3 }); +}); + +test('.geometry returns an ExtrusionGeometry from the path', () => { + const path = new Path(PathType.Travel, undefined, undefined, undefined); + + path.vertices = [0, 0, 0, 1, 2, 3]; + + const result = path.geometry() as ExtrusionGeometry; + + expect(result).not.toBeNull(); + expect(result).toBeInstanceOf(ExtrusionGeometry); + expect(result.parameters.points.length).toEqual(2); + expect(result.parameters.closed).toEqual(false); +}); + +test('.geometry returns an ExtrusionGeometry with the path extrusion width', () => { + const path = new Path(PathType.Travel, 9, undefined, undefined); + + path.vertices = [0, 0, 0, 1, 2, 3]; + + const result = path.geometry() as ExtrusionGeometry; + + expect(result.parameters.lineWidth).toEqual(9); +}); + +test('.geometry returns an ExtrusionGeometry with the path line height', () => { + const path = new Path(PathType.Travel, undefined, 5, undefined); + + path.vertices = [0, 0, 0, 1, 2, 3]; + + const result = path.geometry() as ExtrusionGeometry; + + expect(result.parameters.lineHeight).toEqual(5); +}); + +test('.geometry returns an empty BufferGeometry if there are less than 3 vertices', () => { + const path = new Path(PathType.Travel, undefined, undefined, undefined); + + const result = path.geometry(); + + expect(result).not.toBeNull(); + expect(result).toBeInstanceOf(BufferGeometry); +}); + +test('.line returns a BufferGeometry from the path', () => { + const path = new Path(PathType.Travel, undefined, undefined, undefined); + + path.vertices = [0, 0, 0, 1, 2, 3]; + + const result = path.line(); + + expect(result).not.toBeNull(); + expect(result).toBeInstanceOf(BufferGeometry); + expect(result.getAttribute('position').count).toEqual(2); +}); + +test('.line returns a BufferGeometry when there are no vertices', () => { + const path = new Path(PathType.Travel, undefined, undefined, undefined); + + const result = path.line(); + + expect(result).not.toBeNull(); + expect(result).toBeInstanceOf(BufferGeometry); + expect(result.getAttribute('position').count).toEqual(0); +}); diff --git a/src/__tests__/preserving-parser.ts b/src/__tests__/preserving-parser.ts index b00a6a18..7171af75 100644 --- a/src/__tests__/preserving-parser.ts +++ b/src/__tests__/preserving-parser.ts @@ -3,7 +3,7 @@ import { test, expect } from 'vitest'; import { Parser } from '../gcode-parser'; test('all input should be preserved', () => { - const parser = new Parser(0); + const parser = new Parser(); const gcode = `G1 X0 Y0 Z1 E1`; const parsed = parser.parseGCode(gcode); expect(parsed).not.toBeNull(); @@ -12,7 +12,7 @@ test('all input should be preserved', () => { }); test('multiple lines should be preserved', () => { - const parser = new Parser(0); + const parser = new Parser(); const gcode = `G1 X0 Y0 Z1 E1\nG1 X10 Y10 E10`; const parsed = parser.parseGCode(gcode); expect(parsed).not.toBeNull(); @@ -21,7 +21,7 @@ test('multiple lines should be preserved', () => { }); test('comments should be preserved', () => { - const parser = new Parser(0); + const parser = new Parser(); const gcode = `G1 X0 Y0 Z1 E1; this is a comment`; const parsed = parser.parseGCode(gcode); expect(parsed).not.toBeNull(); From 1ff8be8798bf6e25c5366ba5472b1706b6e5d46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Fri, 11 Oct 2024 00:18:43 -0400 Subject: [PATCH 24/34] First interpreter tests --- src/__tests__/interpreter.ts | 57 ++++++++++++++++++++++++++++++++++++ src/gcode-parser.ts | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/interpreter.ts diff --git a/src/__tests__/interpreter.ts b/src/__tests__/interpreter.ts new file mode 100644 index 00000000..9aab842d --- /dev/null +++ b/src/__tests__/interpreter.ts @@ -0,0 +1,57 @@ +import { test, expect } from 'vitest'; +import { GCodeCommand } from '../gcode-parser'; +import { Interpreter } from '../interpreter'; +import { Job } from '../job'; + +test('.execute returns a stateful job', () => { + const command = new GCodeCommand('G0 X1 Y2 Z3', 'g0', { x: 1, y: 2, z: 3 }); + const interpreter = new Interpreter(); + + const result = interpreter.execute([command]); + + expect(result).not.toBeNull(); + expect(result).toBeInstanceOf(Job); + expect(result.state.x).toEqual(1); + expect(result.state.y).toEqual(2); + expect(result.state.z).toEqual(3); +}); + +test('.execute ignores unknown commands', () => { + const command = new GCodeCommand('G42', 'g42', {}); + const interpreter = new Interpreter(); + + const result = interpreter.execute([command]); + + expect(result).not.toBeNull(); + expect(result).toBeInstanceOf(Job); + expect(result.state.x).toEqual(0); + expect(result.state.y).toEqual(0); + expect(result.state.z).toEqual(0); +}); + +test('.execute runs multiple commands', () => { + const command1 = new GCodeCommand('G0 X1 Y2 Z3', 'g0', { x: 1, y: 2, z: 3 }); + const command2 = new GCodeCommand('G0 X4 Y5 Z6', 'g0', { x: 4, y: 5, z: 6 }); + const interpreter = new Interpreter(); + + const result = interpreter.execute([command1, command2, command1, command2]); + + expect(result).not.toBeNull(); + expect(result).toBeInstanceOf(Job); + expect(result.state.x).toEqual(4); + expect(result.state.y).toEqual(5); + expect(result.state.z).toEqual(6); +}); + +test('.execute runs on an existing job', () => { + const job = new Job(); + const command = new GCodeCommand('G0 X1 Y2 Z3', 'g0', { x: 1, y: 2, z: 3 }); + const interpreter = new Interpreter(); + + const result = interpreter.execute([command], job); + + expect(result).toEqual(job); + expect(result.state.x).toEqual(1); + expect(result.state.y).toEqual(2); + expect(result.state.z).toEqual(3); +}); diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index 0470d12a..9edd46fc 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -141,7 +141,7 @@ export class Parser { } private lines2commands(lines: string[]) { - return lines.map((l) => this.parseCommand(l)) as GCodeCommand[]; + return lines.map((l) => this.parseCommand(l)); } parseCommand(line: string, keepComments = true): GCodeCommand | null { From 6d5f9584aa84e4fde9ee624855ef8e33188d2a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Fri, 11 Oct 2024 00:31:34 -0400 Subject: [PATCH 25/34] Test G0 and G1 --- src/__tests__/interpreter.ts | 104 +++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/src/__tests__/interpreter.ts b/src/__tests__/interpreter.ts index 9aab842d..664440b9 100644 --- a/src/__tests__/interpreter.ts +++ b/src/__tests__/interpreter.ts @@ -55,3 +55,107 @@ test('.execute runs on an existing job', () => { expect(result.state.y).toEqual(2); expect(result.state.z).toEqual(3); }); + +test('.G0 moves the state to the new position', () => { + const command = new GCodeCommand('G0 X1 Y2 Z3', 'g0', { x: 1, y: 2, z: 3 }); + const interpreter = new Interpreter(); + + const job = interpreter.execute([command]); + + expect(job.state.x).toEqual(1); + expect(job.state.y).toEqual(2); + expect(job.state.z).toEqual(3); +}); + +test('.G0 starts a path if the job has none', () => { + const command = new GCodeCommand('G0 X1 Y2', 'g0', { x: 1, y: 2 }); + const interpreter = new Interpreter(); + + const job = interpreter.execute([command]); + + expect(job.paths.length).toEqual(1); + expect(job.paths[0].vertices.length).toEqual(6); + expect(job.paths[0].vertices[0]).toEqual(0); + expect(job.paths[0].vertices[1]).toEqual(0); + expect(job.paths[0].vertices[2]).toEqual(0); +}); + +test('.G0 starts a path if the job has none, starting at the job current state', () => { + const command = new GCodeCommand('G0 X1 Y2', 'g0', { x: 1, y: 2 }); + const interpreter = new Interpreter(); + const job = new Job(); + job.state.x = 3; + job.state.y = 4; + + interpreter.execute([command], job); + + expect(job.paths.length).toEqual(1); + expect(job.paths[0].vertices.length).toEqual(6); + expect(job.paths[0].vertices[0]).toEqual(3); + expect(job.paths[0].vertices[1]).toEqual(4); + expect(job.paths[0].vertices[2]).toEqual(0); +}); + +test('.G0 continues the path if the job has one', () => { + const command1 = new GCodeCommand('G0 X1 Y2', 'g0', { x: 1, y: 2 }); + const command2 = new GCodeCommand('G0 X3 Y4', 'g0', { x: 3, y: 4 }); + const interpreter = new Interpreter(); + + const job = interpreter.execute([command1, command2]); + + expect(job.paths.length).toEqual(1); + expect(job.paths[0].vertices.length).toEqual(9); + expect(job.paths[0].vertices[6]).toEqual(3); + expect(job.paths[0].vertices[7]).toEqual(4); + expect(job.paths[0].vertices[8]).toEqual(0); +}); + +test(".G0 assigns the travel type if there's no extrusion", () => { + const command = new GCodeCommand('G0 X1 Y2', 'g0', { x: 1, y: 2 }); + const interpreter = new Interpreter(); + + const job = interpreter.execute([command]); + + expect(job.paths.length).toEqual(1); + expect(job.paths[0].travelType).toEqual('Travel'); +}); + +test(".G0 assigns the extrusion type if there's extrusion", () => { + const command = new GCodeCommand('G1 X1 Y2 E3', 'g1', { x: 1, y: 2, e: 3 }); + const interpreter = new Interpreter(); + + const job = interpreter.execute([command]); + + expect(job.paths.length).toEqual(1); + expect(job.paths[0].travelType).toEqual('Extrusion'); +}); + +test('.G0 starts a new path if the travel type changes from Travel to Extrusion', () => { + const command1 = new GCodeCommand('G0 X1 Y2', 'g0', { x: 1, y: 2 }); + const command2 = new GCodeCommand('G1 X3 Y4 E5', 'g1', { x: 3, y: 4, e: 5 }); + const interpreter = new Interpreter(); + + const job = interpreter.execute([command1, command2]); + + expect(job.paths.length).toEqual(2); + expect(job.paths[0].travelType).toEqual('Travel'); + expect(job.paths[1].travelType).toEqual('Extrusion'); +}); + +test('.G0 starts a new path if the travel type changes from Extrusion to Travel', () => { + const command1 = new GCodeCommand('G1 X1 Y2 E3', 'g1', { x: 1, y: 2, e: 3 }); + const command2 = new GCodeCommand('G0 X3 Y4', 'g0', { x: 3, y: 4 }); + const interpreter = new Interpreter(); + + const job = interpreter.execute([command1, command2]); + + expect(job.paths.length).toEqual(2); + expect(job.paths[0].travelType).toEqual('Extrusion'); + expect(job.paths[1].travelType).toEqual('Travel'); +}); + +test('.G1 is an alias to .G0', () => { + const interpreter = new Interpreter(); + + expect(interpreter.G1).toEqual(interpreter.G0); +}); From 4f0be28d998228aa39da8b38ccb44234faf1ac43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Fri, 11 Oct 2024 00:44:59 -0400 Subject: [PATCH 26/34] Test everything by G2 --- src/__tests__/interpreter.ts | 142 +++++++++++++++++++++++++++++++++-- src/interpreter.ts | 14 ++-- 2 files changed, 145 insertions(+), 11 deletions(-) diff --git a/src/__tests__/interpreter.ts b/src/__tests__/interpreter.ts index 664440b9..f2ce4511 100644 --- a/src/__tests__/interpreter.ts +++ b/src/__tests__/interpreter.ts @@ -86,6 +86,7 @@ test('.G0 starts a path if the job has none, starting at the job current state', const job = new Job(); job.state.x = 3; job.state.y = 4; + job.state.tool = 5; interpreter.execute([command], job); @@ -94,14 +95,17 @@ test('.G0 starts a path if the job has none, starting at the job current state', expect(job.paths[0].vertices[0]).toEqual(3); expect(job.paths[0].vertices[1]).toEqual(4); expect(job.paths[0].vertices[2]).toEqual(0); + expect(job.paths[0].tool).toEqual(5); }); test('.G0 continues the path if the job has one', () => { const command1 = new GCodeCommand('G0 X1 Y2', 'g0', { x: 1, y: 2 }); const command2 = new GCodeCommand('G0 X3 Y4', 'g0', { x: 3, y: 4 }); const interpreter = new Interpreter(); + const job = new Job(); + interpreter.execute([command1], job); - const job = interpreter.execute([command1, command2]); + interpreter.G0(command2, job); expect(job.paths.length).toEqual(1); expect(job.paths[0].vertices.length).toEqual(9); @@ -113,8 +117,9 @@ test('.G0 continues the path if the job has one', () => { test(".G0 assigns the travel type if there's no extrusion", () => { const command = new GCodeCommand('G0 X1 Y2', 'g0', { x: 1, y: 2 }); const interpreter = new Interpreter(); + const job = new Job(); - const job = interpreter.execute([command]); + interpreter.G0(command, job); expect(job.paths.length).toEqual(1); expect(job.paths[0].travelType).toEqual('Travel'); @@ -123,8 +128,9 @@ test(".G0 assigns the travel type if there's no extrusion", () => { test(".G0 assigns the extrusion type if there's extrusion", () => { const command = new GCodeCommand('G1 X1 Y2 E3', 'g1', { x: 1, y: 2, e: 3 }); const interpreter = new Interpreter(); + const job = new Job(); - const job = interpreter.execute([command]); + interpreter.G0(command, job); expect(job.paths.length).toEqual(1); expect(job.paths[0].travelType).toEqual('Extrusion'); @@ -134,8 +140,10 @@ test('.G0 starts a new path if the travel type changes from Travel to Extrusion' const command1 = new GCodeCommand('G0 X1 Y2', 'g0', { x: 1, y: 2 }); const command2 = new GCodeCommand('G1 X3 Y4 E5', 'g1', { x: 3, y: 4, e: 5 }); const interpreter = new Interpreter(); + const job = new Job(); + interpreter.execute([command1], job); - const job = interpreter.execute([command1, command2]); + interpreter.G0(command2, job); expect(job.paths.length).toEqual(2); expect(job.paths[0].travelType).toEqual('Travel'); @@ -146,8 +154,10 @@ test('.G0 starts a new path if the travel type changes from Extrusion to Travel' const command1 = new GCodeCommand('G1 X1 Y2 E3', 'g1', { x: 1, y: 2, e: 3 }); const command2 = new GCodeCommand('G0 X3 Y4', 'g0', { x: 3, y: 4 }); const interpreter = new Interpreter(); + const job = new Job(); + interpreter.execute([command1], job); - const job = interpreter.execute([command1, command2]); + interpreter.G0(command2, job); expect(job.paths.length).toEqual(2); expect(job.paths[0].travelType).toEqual('Extrusion'); @@ -159,3 +169,125 @@ test('.G1 is an alias to .G0', () => { expect(interpreter.G1).toEqual(interpreter.G0); }); + +test('.G20 sets the units to inches', () => { + const command = new GCodeCommand('G20', 'g20', {}); + const interpreter = new Interpreter(); + const job = new Job(); + + interpreter.G20(command, job); + + expect(job.state.units).toEqual('in'); +}); + +test('.G21 sets the units to millimeters', () => { + const command = new GCodeCommand('G21', 'g21', {}); + const interpreter = new Interpreter(); + const job = new Job(); + + interpreter.G21(command, job); + + expect(job.state.units).toEqual('mm'); +}); + +test('.g28 moves the state to the origin', () => { + const command = new GCodeCommand('G28', 'g28', {}); + const interpreter = new Interpreter(); + const job = new Job(); + job.state.x = 3; + job.state.y = 4; + + interpreter.G28(command, job); + + expect(job.state.x).toEqual(0); + expect(job.state.y).toEqual(0); + expect(job.state.z).toEqual(0); +}); + +test('.t0 sets the tool to 0', () => { + const command = new GCodeCommand('T0', 't0', {}); + const interpreter = new Interpreter(); + const job = new Job(); + job.state.tool = 3; + + interpreter.T0(command, job); + + expect(job.state.tool).toEqual(0); +}); + +test('.t1 sets the tool to 1', () => { + const command = new GCodeCommand('T1', 't1', {}); + const interpreter = new Interpreter(); + const job = new Job(); + job.state.tool = 3; + + interpreter.T1(command, job); + + expect(job.state.tool).toEqual(1); +}); + +test('.t2 sets the tool to 2', () => { + const command = new GCodeCommand('T2', 't2', {}); + const interpreter = new Interpreter(); + const job = new Job(); + job.state.tool = 3; + + interpreter.T2(command, job); + + expect(job.state.tool).toEqual(2); +}); + +test('.t3 sets the tool to 3', () => { + const command = new GCodeCommand('T3', 't3', {}); + const interpreter = new Interpreter(); + const job = new Job(); + job.state.tool = 3; + + interpreter.T3(command, job); + + expect(job.state.tool).toEqual(3); +}); + +test('.t4 sets the tool to 4', () => { + const command = new GCodeCommand('T4', 't4', {}); + const interpreter = new Interpreter(); + const job = new Job(); + job.state.tool = 3; + + interpreter.T4(command, job); + + expect(job.state.tool).toEqual(4); +}); + +test('.t5 sets the tool to 5', () => { + const command = new GCodeCommand('T5', 't5', {}); + const interpreter = new Interpreter(); + const job = new Job(); + job.state.tool = 3; + + interpreter.T5(command, job); + + expect(job.state.tool).toEqual(5); +}); + +test('.t6 sets the tool to 6', () => { + const command = new GCodeCommand('T6', 't6', {}); + const interpreter = new Interpreter(); + const job = new Job(); + job.state.tool = 3; + + interpreter.T6(command, job); + + expect(job.state.tool).toEqual(6); +}); + +test('.t7 sets the tool to 7', () => { + const command = new GCodeCommand('T7', 't7', {}); + const interpreter = new Interpreter(); + const job = new Job(); + job.state.tool = 3; + + interpreter.T7(command, job); + + expect(job.state.tool).toEqual(7); +}); diff --git a/src/interpreter.ts b/src/interpreter.ts index 029843bb..b5d3b510 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -130,6 +130,14 @@ export class Interpreter { G3 = this.G2; + G20(command: GCodeCommand, job: Job): void { + job.state.units = 'in'; + } + + G21(command: GCodeCommand, job: Job): void { + job.state.units = 'mm'; + } + G28(command: GCodeCommand, job: Job): void { job.state.x = 0; job.state.y = 0; @@ -160,12 +168,6 @@ export class Interpreter { T7(command: GCodeCommand, job: Job): void { job.state.tool = 7; } - G20(command: GCodeCommand, job: Job): void { - job.state.units = 'in'; - } - G21(command: GCodeCommand, job: Job): void { - job.state.units = 'mm'; - } private breakPath(job: Job, newType: PathType): Path { const lastPath = new Path(newType, 0.6, 0.2, job.state.tool); From 6d1f18d656ebc79022509f1468ae5231a8efc0df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Fri, 11 Oct 2024 00:52:35 -0400 Subject: [PATCH 27/34] Adding missing codes --- src/gcode-parser.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index 9edd46fc..27320362 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -61,6 +61,8 @@ export enum Code { G1 = 'G1', G2 = 'G2', G3 = 'G3', + G20 = 'G20', + G21 = 'G21', G28 = 'G28', T0 = 'T0', T1 = 'T1', @@ -96,6 +98,12 @@ export class GCodeCommand { case 'g3': case 'g03': return Code.G3; + case 'g20': + return Code.G20; + case 'g21': + return Code.G21; + case 'g28': + return Code.G28; case 't0': return Code.T0; case 't1': From dec8c3e49c0060c0231efc329d7679418478de04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Fri, 11 Oct 2024 19:26:47 -0400 Subject: [PATCH 28/34] Minimize diff on app.js --- demo/js/app.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/demo/js/app.js b/demo/js/app.js index 00a163fb..753b5778 100644 --- a/demo/js/app.js +++ b/demo/js/app.js @@ -12,7 +12,7 @@ const statsContainer = () => document.querySelector('.sidebar'); const loadProgressive = true; let observer = null; let preview = null; -let activeRendering = true; +let firstRender = true; export const app = (window.app = createApp({ setup() { @@ -113,19 +113,18 @@ export const app = (window.app = createApp({ const prevDevMode = preview.devMode; preview.clear(); preview.devMode = prevDevMode; - const { commands } = preview.parser.parseGCode(gcode); - preview.interpreter.execute(commands, preview.job); + if (loadProgressive) { + const { commands } = preview.parser.parseGCode(gcode); + preview.interpreter.execute(commands, preview.job); if (preview.job.layers() === null) { console.warn('Job is not planar'); preview.render(); return; } - activeRendering = true; await preview.renderAnimated(Math.ceil(preview.job.layers().length / 60)); - activeRendering = false; } else { - preview.render(); + preview.processGCode(gcode); } }; @@ -139,7 +138,7 @@ export const app = (window.app = createApp({ }; const selectPreset = async (presetName) => { - activeRendering = true; + firstRender = true; const canvas = document.querySelector('canvas.preview'); const preset = presets[presetName]; fileName.value = preset.file.replace(/^.*?\//, ''); @@ -189,8 +188,10 @@ export const app = (window.app = createApp({ preview.buildVolume = settings.value.drawBuildVolume ? settings.value.buildVolume : undefined; preview.backgroundColor = settings.value.backgroundColor; - if (!activeRendering) { + if (!firstRender) { preview.render(); + } else { + firstRender = false; } }); @@ -211,9 +212,7 @@ export const app = (window.app = createApp({ preview.lastSegmentColor = settings.value.highlightLastSegment ? settings.value.lastSegmentColor : undefined; debounce(() => { - if (!activeRendering) { - preview.render(); - } + preview.renderAnimated(Math.ceil(preview.job.layers().length / 60)); }); }); }); From 466abb4afd0f4f93cdb3c1527b8a735f8fa89f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Fri, 11 Oct 2024 19:38:52 -0400 Subject: [PATCH 29/34] Leave G21 for a future PR --- src/__tests__/interpreter.ts | 10 ---------- src/gcode-parser.ts | 3 --- src/interpreter.ts | 4 ---- 3 files changed, 17 deletions(-) diff --git a/src/__tests__/interpreter.ts b/src/__tests__/interpreter.ts index f2ce4511..bd2da4a4 100644 --- a/src/__tests__/interpreter.ts +++ b/src/__tests__/interpreter.ts @@ -180,16 +180,6 @@ test('.G20 sets the units to inches', () => { expect(job.state.units).toEqual('in'); }); -test('.G21 sets the units to millimeters', () => { - const command = new GCodeCommand('G21', 'g21', {}); - const interpreter = new Interpreter(); - const job = new Job(); - - interpreter.G21(command, job); - - expect(job.state.units).toEqual('mm'); -}); - test('.g28 moves the state to the origin', () => { const command = new GCodeCommand('G28', 'g28', {}); const interpreter = new Interpreter(); diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index 27320362..0f3f72ff 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -62,7 +62,6 @@ export enum Code { G2 = 'G2', G3 = 'G3', G20 = 'G20', - G21 = 'G21', G28 = 'G28', T0 = 'T0', T1 = 'T1', @@ -100,8 +99,6 @@ export class GCodeCommand { return Code.G3; case 'g20': return Code.G20; - case 'g21': - return Code.G21; case 'g28': return Code.G28; case 't0': diff --git a/src/interpreter.ts b/src/interpreter.ts index b5d3b510..6200c6ea 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -134,10 +134,6 @@ export class Interpreter { job.state.units = 'in'; } - G21(command: GCodeCommand, job: Job): void { - job.state.units = 'mm'; - } - G28(command: GCodeCommand, job: Job): void { job.state.x = 0; job.state.y = 0; From ac930915b82884a669dbbdbcc74fe9fd51d4cca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Fri, 11 Oct 2024 20:30:24 -0400 Subject: [PATCH 30/34] Job tests (and fixes!) --- src/__tests__/job.ts | 164 +++++++++++++++++++++++++++++++++++++++++++ src/job.ts | 7 +- 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/job.ts diff --git a/src/__tests__/job.ts b/src/__tests__/job.ts new file mode 100644 index 00000000..2b723724 --- /dev/null +++ b/src/__tests__/job.ts @@ -0,0 +1,164 @@ +import { test, expect, describe } from 'vitest'; +import { Job } from '../job'; +import { PathType, Path } from '../path'; + +test('it has an initial state', () => { + const job = new Job(); + + expect(job.state.x).toEqual(0); + expect(job.state.y).toEqual(0); + expect(job.state.z).toEqual(0); + expect(job.state.e).toEqual(0); + expect(job.state.tool).toEqual(0); + expect(job.state.units).toEqual('mm'); +}); + +describe('.isPlanar', () => { + test('returns true if all extrusions are on the same plane', () => { + const job = new Job(); + + append_path(job, PathType.Extrusion, [0, 0, 0, 1, 2, 0]); + append_path(job, PathType.Extrusion, [1, 2, 0, 5, 6, 0]); + + expect(job.isPlanar()).toEqual(true); + }); + + test('returns false if any extrusions are on a different plane', () => { + const job = new Job(); + + append_path(job, PathType.Extrusion, [0, 0, 0, 1, 2, 0]); + append_path(job, PathType.Extrusion, [1, 2, 0, 5, 6, 1]); + + expect(job.isPlanar()).toEqual(false); + }); + + test('ignores travel paths', () => { + const job = new Job(); + + append_path(job, PathType.Extrusion, [0, 0, 0, 1, 2, 0]); + append_path(job, PathType.Travel, [5, 6, 0, 5, 6, 1, 1, 2, 0]); + append_path(job, PathType.Extrusion, [1, 2, 0, 5, 6, 0]); + + expect(job.isPlanar()).toEqual(true); + }); +}); + +describe('.layers', () => { + test('returns null if the job is not planar', () => { + const job = new Job(); + + append_path(job, PathType.Extrusion, [0, 0, 0, 1, 2, 0]); + append_path(job, PathType.Extrusion, [5, 6, 0, 5, 6, 1]); + + expect(job.layers()).toEqual(null); + }); + + test('paths without z changes are on the same layer', () => { + const job = new Job(); + + append_path(job, PathType.Extrusion, [0, 0, 0, 1, 2, 0]); + append_path(job, PathType.Travel, [5, 6, 0, 5, 6, 0]); + + const layers = job.layers(); + + expect(layers).not.toBeNull(); + expect(layers).toBeInstanceOf(Array); + expect(layers?.length).toEqual(1); + expect(layers?.[0].length).toEqual(2); + }); + + test('travel paths moving z create a new layer', () => { + const job = new Job(); + + append_path(job, PathType.Extrusion, [0, 0, 0, 1, 2, 0]); + append_path(job, PathType.Travel, [5, 6, 0, 5, 6, 1]); + + const layers = job.layers(); + + expect(layers).not.toBeNull(); + expect(layers).toBeInstanceOf(Array); + expect(layers?.length).toEqual(2); + expect(layers?.[0].length).toEqual(1); + expect(layers?.[1].length).toEqual(1); + }); + + test('multiple travels in a row are on the same layer', () => { + const job = new Job(); + + append_path(job, PathType.Extrusion, [0, 0, 0, 1, 2, 0]); + append_path(job, PathType.Travel, [5, 6, 0, 5, 6, 2]); + append_path(job, PathType.Travel, [5, 6, 2, 5, 6, 0]); + append_path(job, PathType.Travel, [5, 6, 0, 5, 6, 2]); + + const layers = job.layers(); + + expect(layers).not.toBeNull(); + expect(layers).toBeInstanceOf(Array); + expect(layers?.length).toEqual(2); + expect(layers?.[0].length).toEqual(1); + expect(layers?.[1].length).toEqual(3); + }); + + test('extrusions after travels are on the same layer', () => { + const job = new Job(); + + append_path(job, PathType.Extrusion, [0, 0, 0, 1, 2, 0]); + append_path(job, PathType.Travel, [5, 6, 0, 5, 6, 2]); + append_path(job, PathType.Travel, [5, 6, 2, 5, 6, 0]); + append_path(job, PathType.Travel, [5, 6, 0, 5, 6, 2]); + append_path(job, PathType.Extrusion, [5, 6, 2, 5, 6, 2]); + + const layers = job.layers(); + + expect(layers).not.toBeNull(); + expect(layers).toBeInstanceOf(Array); + expect(layers?.length).toEqual(2); + expect(layers?.[0].length).toEqual(1); + expect(layers?.[1].length).toEqual(4); + }); +}); + +describe('.extrusions', () => { + test('returns all extrusion paths', () => { + const job = new Job(); + + append_path(job, PathType.Extrusion, [0, 0, 0, 1, 2, 0]); + append_path(job, PathType.Travel, [5, 6, 0, 5, 6, 0]); + append_path(job, PathType.Extrusion, [1, 2, 0, 5, 6, 0]); + + const extrusions = job.extrusions(); + + expect(extrusions).not.toBeNull(); + expect(extrusions).toBeInstanceOf(Array); + expect(extrusions.length).toEqual(2); + extrusions.forEach((path) => { + expect(path.travelType).toEqual(PathType.Extrusion); + }); + }); +}); + +describe('.travels', () => { + test('returns all travel paths', () => { + const job = new Job(); + + append_path(job, PathType.Extrusion, [0, 0, 0, 1, 2, 0]); + append_path(job, PathType.Travel, [5, 6, 0, 5, 6, 0]); + append_path(job, PathType.Extrusion, [1, 2, 0, 5, 6, 0]); + append_path(job, PathType.Travel, [5, 6, 0, 5, 6, 0]); + + const travels = job.travels(); + + expect(travels).not.toBeNull(); + expect(travels).toBeInstanceOf(Array); + expect(travels.length).toEqual(2); + travels.forEach((path) => { + expect(path.travelType).toEqual(PathType.Travel); + }); + }); +}); + +function append_path(job, travelType, vertices) { + const path = new Path(travelType, 0.6, 0.2, job.state.tool); + path.vertices = vertices; + job.paths.push(path); +} diff --git a/src/job.ts b/src/job.ts index 31bed313..47f332ed 100644 --- a/src/job.ts +++ b/src/job.ts @@ -45,7 +45,10 @@ export class Job { if (path.travelType === PathType.Extrusion) { currentLayer.push(path); } else { - if (path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2])) { + if ( + path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2]) && + currentLayer.find((p) => p.travelType === PathType.Extrusion) + ) { layers.push(currentLayer); currentLayer = []; } @@ -53,6 +56,8 @@ export class Job { } }); + layers.push(currentLayer); + return layers; } From bcf7682bebaab1d150dbb14ffb15001f301647e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Fri, 11 Oct 2024 21:02:57 -0400 Subject: [PATCH 31/34] Keep G28 for a separate PR --- src/__tests__/interpreter.ts | 14 -------------- src/gcode-parser.ts | 3 --- src/interpreter.ts | 6 ------ 3 files changed, 23 deletions(-) diff --git a/src/__tests__/interpreter.ts b/src/__tests__/interpreter.ts index bd2da4a4..8f2ddd85 100644 --- a/src/__tests__/interpreter.ts +++ b/src/__tests__/interpreter.ts @@ -180,20 +180,6 @@ test('.G20 sets the units to inches', () => { expect(job.state.units).toEqual('in'); }); -test('.g28 moves the state to the origin', () => { - const command = new GCodeCommand('G28', 'g28', {}); - const interpreter = new Interpreter(); - const job = new Job(); - job.state.x = 3; - job.state.y = 4; - - interpreter.G28(command, job); - - expect(job.state.x).toEqual(0); - expect(job.state.y).toEqual(0); - expect(job.state.z).toEqual(0); -}); - test('.t0 sets the tool to 0', () => { const command = new GCodeCommand('T0', 't0', {}); const interpreter = new Interpreter(); diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index 0f3f72ff..c3fbccd2 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -62,7 +62,6 @@ export enum Code { G2 = 'G2', G3 = 'G3', G20 = 'G20', - G28 = 'G28', T0 = 'T0', T1 = 'T1', T2 = 'T2', @@ -99,8 +98,6 @@ export class GCodeCommand { return Code.G3; case 'g20': return Code.G20; - case 'g28': - return Code.G28; case 't0': return Code.T0; case 't1': diff --git a/src/interpreter.ts b/src/interpreter.ts index 6200c6ea..3e680516 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -134,12 +134,6 @@ export class Interpreter { job.state.units = 'in'; } - G28(command: GCodeCommand, job: Job): void { - job.state.x = 0; - job.state.y = 0; - job.state.z = 0; - } - T0(command: GCodeCommand, job: Job): void { job.state.tool = 0; } From ff2911f4ee1c56801030c0e423660e0be5d0a508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Sun, 13 Oct 2024 10:13:08 -0400 Subject: [PATCH 32/34] Improve tests --- src/__tests__/interpreter.ts | 10 ++++++++-- src/__tests__/job.ts | 9 ++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/__tests__/interpreter.ts b/src/__tests__/interpreter.ts index 8f2ddd85..da89e82b 100644 --- a/src/__tests__/interpreter.ts +++ b/src/__tests__/interpreter.ts @@ -2,6 +2,7 @@ import { test, expect } from 'vitest'; import { GCodeCommand } from '../gcode-parser'; import { Interpreter } from '../interpreter'; import { Job } from '../job'; +import { PathType } from '../path'; test('.execute returns a stateful job', () => { const command = new GCodeCommand('G0 X1 Y2 Z3', 'g0', { x: 1, y: 2, z: 3 }); @@ -78,6 +79,9 @@ test('.G0 starts a path if the job has none', () => { expect(job.paths[0].vertices[0]).toEqual(0); expect(job.paths[0].vertices[1]).toEqual(0); expect(job.paths[0].vertices[2]).toEqual(0); + expect(job.paths[0].vertices[3]).toEqual(1); + expect(job.paths[0].vertices[4]).toEqual(2); + expect(job.paths[0].vertices[5]).toEqual(0); }); test('.G0 starts a path if the job has none, starting at the job current state', () => { @@ -103,6 +107,8 @@ test('.G0 continues the path if the job has one', () => { const command2 = new GCodeCommand('G0 X3 Y4', 'g0', { x: 3, y: 4 }); const interpreter = new Interpreter(); const job = new Job(); + + job.state.z = 5; interpreter.execute([command1], job); interpreter.G0(command2, job); @@ -111,7 +117,7 @@ test('.G0 continues the path if the job has one', () => { expect(job.paths[0].vertices.length).toEqual(9); expect(job.paths[0].vertices[6]).toEqual(3); expect(job.paths[0].vertices[7]).toEqual(4); - expect(job.paths[0].vertices[8]).toEqual(0); + expect(job.paths[0].vertices[8]).toEqual(5); }); test(".G0 assigns the travel type if there's no extrusion", () => { @@ -122,7 +128,7 @@ test(".G0 assigns the travel type if there's no extrusion", () => { interpreter.G0(command, job); expect(job.paths.length).toEqual(1); - expect(job.paths[0].travelType).toEqual('Travel'); + expect(job.paths[0].travelType).toEqual(PathType.Travel); }); test(".G0 assigns the extrusion type if there's extrusion", () => { diff --git a/src/__tests__/job.ts b/src/__tests__/job.ts index 2b723724..8d1d0538 100644 --- a/src/__tests__/job.ts +++ b/src/__tests__/job.ts @@ -1,16 +1,11 @@ import { test, expect, describe } from 'vitest'; -import { Job } from '../job'; +import { Job, State } from '../job'; import { PathType, Path } from '../path'; test('it has an initial state', () => { const job = new Job(); - expect(job.state.x).toEqual(0); - expect(job.state.y).toEqual(0); - expect(job.state.z).toEqual(0); - expect(job.state.e).toEqual(0); - expect(job.state.tool).toEqual(0); - expect(job.state.units).toEqual('mm'); + expect(job.state).toEqual(State.initial); }); describe('.isPlanar', () => { From 4fc21bfd803b8c40a9ac0e9bc159fc83bc34c2e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Sun, 13 Oct 2024 10:42:45 -0400 Subject: [PATCH 33/34] Code improvements --- src/interpreter.ts | 6 +++--- src/webgl-preview.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/interpreter.ts b/src/interpreter.ts index 3e680516..ecabded4 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -24,9 +24,9 @@ export class Interpreter { lastPath = this.breakPath(job, pathType); } - state.x = x || state.x; - state.y = y || state.y; - state.z = z || state.z; + state.x = x ?? state.x; + state.y = y ?? state.y; + state.z = z ?? state.z; lastPath.addPoint(state.x, state.y, state.z); } diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index d07358ff..839e9e9e 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -332,15 +332,15 @@ export class WebGLPreview { this.group = this.createGroup('layer' + this.renderLayerIndex); const endIndex = Math.min(this.renderLayerIndex + layerCount, this.job.layers().length - 1); - const layersToRender = this.job + const pathsToRender = this.job .layers() .slice(this.renderLayerIndex, endIndex) .flatMap((l) => l); - this.renderGeometries(layersToRender.filter((path) => path.travelType === 'Extrusion')); + this.renderGeometries(pathsToRender.filter((path) => path.travelType === 'Extrusion')); this.renderLines( - layersToRender.filter((path) => path.travelType === 'Travel'), - layersToRender.filter((path) => path.travelType === 'Extrusion') + pathsToRender.filter((path) => path.travelType === 'Travel'), + pathsToRender.filter((path) => path.travelType === 'Extrusion') ); this.renderLayerIndex = endIndex; From 14b54ca311c8f46bcdffdd0fdbae314594401ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophie=20D=C3=A9ziel?= Date: Sun, 13 Oct 2024 11:00:15 -0400 Subject: [PATCH 34/34] Retractions are travel --- src/__tests__/interpreter.ts | 11 +++++++++++ src/interpreter.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/__tests__/interpreter.ts b/src/__tests__/interpreter.ts index da89e82b..49b58cf6 100644 --- a/src/__tests__/interpreter.ts +++ b/src/__tests__/interpreter.ts @@ -142,6 +142,17 @@ test(".G0 assigns the extrusion type if there's extrusion", () => { expect(job.paths[0].travelType).toEqual('Extrusion'); }); +test('.G0 assigns the travel type if the extrusion is a retraction', () => { + const command = new GCodeCommand('G0 E-2', 'g0', { e: -2 }); + const interpreter = new Interpreter(); + const job = new Job(); + + interpreter.G0(command, job); + + expect(job.paths.length).toEqual(1); + expect(job.paths[0].travelType).toEqual('Travel'); +}); + test('.G0 starts a new path if the travel type changes from Travel to Extrusion', () => { const command1 = new GCodeCommand('G0 X1 Y2', 'g0', { x: 1, y: 2 }); const command2 = new GCodeCommand('G1 X3 Y4 E5', 'g1', { x: 3, y: 4, e: 5 }); diff --git a/src/interpreter.ts b/src/interpreter.ts index ecabded4..65b09e15 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -18,7 +18,7 @@ export class Interpreter { const { state } = job; let lastPath = job.paths[job.paths.length - 1]; - const pathType = e ? PathType.Extrusion : PathType.Travel; + const pathType = e > 0 ? PathType.Extrusion : PathType.Travel; if (lastPath === undefined || lastPath.travelType !== pathType) { lastPath = this.breakPath(job, pathType);