diff --git a/src/__tests__/gcode-parser.ts b/src/__tests__/gcode-parser.ts index 1af316ab..ae8eaffb 100644 --- a/src/__tests__/gcode-parser.ts +++ b/src/__tests__/gcode-parser.ts @@ -21,6 +21,17 @@ test('a single extrusion cmd should parse attributes', () => { expect(cmd.params.e).toEqual(1.9); }); +// G1 X61.769 Y90.734 E-.27245 +test("E value that doesn' have a leading 0 should be parsed as if there was a 0", () => { + const parser = new Parser(); + const gcode = `G1 X61.769 Y90.734 E-.27245`; + const parsed = parser.parseGCode(gcode); + const cmd = parsed.commands[0]; + expect(cmd.params.x).toEqual(61.769); + expect(cmd.params.y).toEqual(90.734); + expect(cmd.params.e).toEqual(-0.27245); +}); + test('multiple cmd results in an array of commands', () => { const parser = new Parser(); const gcode = `G1 X5 Y6 Z3 E1.9 diff --git a/src/__tests__/path.ts b/src/__tests__/path.ts index 0684e84e..570c6def 100644 --- a/src/__tests__/path.ts +++ b/src/__tests__/path.ts @@ -1,7 +1,6 @@ import { test, expect, describe } 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); @@ -123,13 +122,22 @@ describe('.geometry', () => { expect(result.parameters.lineHeight).toEqual(7); }); - test('returns an empty BufferGeometry if there are less than 3 vertices', () => { + test('returns null if there are 0 vertices', () => { const path = new Path(PathType.Travel, undefined, undefined, undefined); const result = path.geometry(); - expect(result).not.toBeNull(); - expect(result).toBeInstanceOf(BufferGeometry); + expect(result).toBeNull(); + }); + + test('returns null if there are less than 6 vertices', () => { + const path = new Path(PathType.Travel, undefined, undefined, undefined); + + path.addPoint(0, 0, 0); + + const result = path.geometry(); + + expect(result).toBeNull(); }); }); diff --git a/src/interpreter.ts b/src/interpreter.ts index 90f9361c..52a0e42d 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -2,6 +2,10 @@ import { Path, PathType } from './path'; import { GCodeCommand } from './gcode-parser'; import { Job } from './job'; +// eslint-disable-next-line no-unused-vars +type Method = (...args: unknown[]) => unknown; +// type LookupTable = { [key: string]: Method | undefined }; + /** * Interprets and executes G-code commands, updating the job state accordingly * @@ -14,6 +18,23 @@ export class Interpreter { // eslint-disable-next-line no-unused-vars [key: string]: (...args: unknown[]) => unknown; + // TODO: maybe these props should move to the Job class + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + private retractions = 0; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + private wipes = 0; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + private feedrateChanges = 0; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + private points = 0; + /** * Executes an array of G-code commands, updating the provided job * @param commands - Array of GCodeCommand objects to execute @@ -21,17 +42,27 @@ export class Interpreter { * @returns The updated job instance */ execute(commands: GCodeCommand[], job = new Job()): Job { + performance.mark('start execution'); job.resumeLastPath(); commands.forEach((command) => { if (command.gcode !== undefined) { - if (this[command.gcode] === undefined) { + if (typeof this[command.gcode] !== 'function') { return; } - this[command.gcode](command, job); + const method = this[command.gcode] as Method; + method.bind(this)(command, job); } }); job.finishPath(); + performance.mark('end execution'); + const measure = performance.measure('execution', 'start execution', 'end execution'); + console.debug('Done processing gcode', measure.duration.toFixed(0) + 'ms'); + console.debug(this.retractions, 'retractions'); + console.debug(this.wipes, 'wipes'); + console.debug(this.feedrateChanges, 'feedrateChanges'); + console.debug(this.points, 'points'); + return job; } @@ -45,9 +76,27 @@ export class Interpreter { * G0 is for rapid moves (non-extrusion), G1 is for linear moves (with optional extrusion). */ g0(command: GCodeCommand, job: Job): void { - const { x, y, z, e } = command.params; - const { state } = job; + const { x, y, z, e, f } = command.params; + // discard zero length moves + if (x === undefined && y === undefined && z === undefined) { + // console.warn('Discarding zero length move'); + if (e > 0) { + this.retractions++; + } else if (e < 0) { + this.wipes++; + } + + if (f !== undefined) { + this.feedrateChanges++; + } + + return; + } + + this.points++; + + const { state } = job; let currentPath = job.inprogressPath; const pathType = e > 0 ? PathType.Extrusion : PathType.Travel; @@ -55,6 +104,8 @@ export class Interpreter { currentPath = this.breakPath(job, pathType); } + // e is omitted bc currently we're assuming relative extrusion distances + // see also https://github.com/xyz-tools/gcode-preview/issues/179 state.x = x ?? state.x; state.y = y ?? state.y; state.z = z ?? state.z; diff --git a/src/path.ts b/src/path.ts index b807dee2..9cd34c3d 100644 --- a/src/path.ts +++ b/src/path.ts @@ -108,8 +108,23 @@ export class Path { * @returns BufferGeometry representing the path */ geometry(opts: { extrusionWidthOverride?: number; lineHeightOverride?: number } = {}): BufferGeometry { - if (this._vertices.length < 3) { - return new BufferGeometry(); + if (this._vertices.length < 6) { + // a path needs at least 2 points to be valid + console.warn('Path has less than 6 points, returning empty geometry'); + return null; + } + + // check for zero length paths + // do this check for each segment + for (let i = 0; i < this._vertices.length - 3; i += 3) { + const dx = this._vertices[i] - this._vertices[i + 3]; + const dy = this._vertices[i + 1] - this._vertices[i + 4]; + const dz = this._vertices[i + 2] - this._vertices[i + 5]; + const distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (distance < 0.0001) { + console.warn('Path has zero length, skipping'); + return null; + } } return new ExtrusionGeometry( diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 5ef03ff5..f9245f23 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -867,6 +867,9 @@ export class WebGLPreview { extrusionWidthOverride: this.extrusionWidth, lineHeightOverride: this.lineHeight }); + + if (!geometry) return; + this.disposables.push(geometry); geometries.push(geometry); });