From 0c042db36143505b5fe51691d9818fc9a5b3868c Mon Sep 17 00:00:00 2001 From: Remco Veldkamp Date: Sun, 29 Dec 2024 23:55:03 +0100 Subject: [PATCH 1/9] Improve checking for zero-length path segments and don't allocate geometry --- src/path.ts | 19 +++++++++++++++++-- src/webgl-preview.ts | 3 +++ 2 files changed, 20 insertions(+), 2 deletions(-) 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..9f8b5349 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); }); From 6659efde3dfc698df55d32f4199c89b53f29384c Mon Sep 17 00:00:00 2001 From: Remco Veldkamp Date: Sun, 29 Dec 2024 23:58:24 +0100 Subject: [PATCH 2/9] Skip moves that have no x, y or z --- src/interpreter.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/interpreter.ts b/src/interpreter.ts index 90f9361c..90d768fc 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -45,9 +45,14 @@ 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) { + return; + } + + const { state } = job; let currentPath = job.inprogressPath; const pathType = e > 0 ? PathType.Extrusion : PathType.Travel; From c05f781cca2b27bd1d4065d6be8226fc2f6d8c97 Mon Sep 17 00:00:00 2001 From: Remco Veldkamp Date: Sun, 29 Dec 2024 23:59:22 +0100 Subject: [PATCH 3/9] Track non-moving move commands --- src/interpreter.ts | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/interpreter.ts b/src/interpreter.ts index 90d768fc..aedb8316 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; + /** * Interprets and executes G-code commands, updating the job state accordingly * @@ -14,6 +18,11 @@ export class Interpreter { // eslint-disable-next-line no-unused-vars [key: string]: (...args: unknown[]) => unknown; + private retractions = 0; + private wipes = 0; + private feedrateChanges = 0; + private points = 0; + /** * Executes an array of G-code commands, updating the provided job * @param commands - Array of GCodeCommand objects to execute @@ -24,14 +33,22 @@ export class Interpreter { 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(); + 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; } @@ -49,9 +66,24 @@ export class Interpreter { // 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; From 03b6b5f82c387d5f3e0ebbade04403fe817a2d9e Mon Sep 17 00:00:00 2001 From: Remco Veldkamp Date: Mon, 30 Dec 2024 00:00:28 +0100 Subject: [PATCH 4/9] Measure gcode execution time --- src/interpreter.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/interpreter.ts b/src/interpreter.ts index aedb8316..8014ea9e 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -30,6 +30,7 @@ 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) { @@ -43,6 +44,8 @@ export class Interpreter { }); 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'); From 73faea3794da0214c52d4688e9f671ee78d64fa4 Mon Sep 17 00:00:00 2001 From: Remco Veldkamp Date: Mon, 30 Dec 2024 00:01:57 +0100 Subject: [PATCH 5/9] Add note --- src/interpreter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/interpreter.ts b/src/interpreter.ts index 8014ea9e..8995a4d7 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -95,6 +95,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; From 2f75f858f305f29cdcb7e9f9a2588622bd2003b7 Mon Sep 17 00:00:00 2001 From: Remco Veldkamp Date: Mon, 30 Dec 2024 00:04:22 +0100 Subject: [PATCH 6/9] Update tests --- src/__tests__/path.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/__tests__/path.ts b/src/__tests__/path.ts index 0684e84e..4c4ec7b7 100644 --- a/src/__tests__/path.ts +++ b/src/__tests__/path.ts @@ -123,13 +123,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(); }); }); From a3814736204e111a25595a7935ef05e3e578ac04 Mon Sep 17 00:00:00 2001 From: Remco Veldkamp Date: Mon, 30 Dec 2024 00:06:35 +0100 Subject: [PATCH 7/9] Add a parser test for E values without a leading 0 --- src/__tests__/gcode-parser.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/__tests__/gcode-parser.ts b/src/__tests__/gcode-parser.ts index 1af316ab..de904cf6 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 From d6081c424b701fdfd3a5893fa8e908a8afcce16f Mon Sep 17 00:00:00 2001 From: Remco Veldkamp Date: Tue, 14 Jan 2025 23:04:24 +0100 Subject: [PATCH 8/9] Linting --- src/__tests__/gcode-parser.ts | 2 +- src/__tests__/path.ts | 1 - src/interpreter.ts | 9 ++++----- src/webgl-preview.ts | 4 ++-- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/__tests__/gcode-parser.ts b/src/__tests__/gcode-parser.ts index de904cf6..ae8eaffb 100644 --- a/src/__tests__/gcode-parser.ts +++ b/src/__tests__/gcode-parser.ts @@ -22,7 +22,7 @@ test('a single extrusion cmd should parse attributes', () => { }); // 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', () => { +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); diff --git a/src/__tests__/path.ts b/src/__tests__/path.ts index 4c4ec7b7..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); diff --git a/src/interpreter.ts b/src/interpreter.ts index 8995a4d7..43a4d733 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -5,6 +5,7 @@ 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 @@ -70,18 +71,16 @@ export class Interpreter { // discard zero length moves if (x === undefined && y === undefined && z === undefined) { // console.warn('Discarding zero length move'); - if (e > 0 ) { + if (e > 0) { this.retractions++; - } - - else if (e < 0) { + } else if (e < 0) { this.wipes++; } if (f !== undefined) { this.feedrateChanges++; } - + return; } diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 9f8b5349..f9245f23 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -867,9 +867,9 @@ export class WebGLPreview { extrusionWidthOverride: this.extrusionWidth, lineHeightOverride: this.lineHeight }); - + if (!geometry) return; - + this.disposables.push(geometry); geometries.push(geometry); }); From 04479a875c960443182792cb092f03c9b546569e Mon Sep 17 00:00:00 2001 From: Remco Veldkamp Date: Mon, 27 Jan 2025 22:22:40 +0100 Subject: [PATCH 9/9] Cleanup --- src/interpreter.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/interpreter.ts b/src/interpreter.ts index 43a4d733..52a0e42d 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -2,10 +2,9 @@ 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 }; +// type LookupTable = { [key: string]: Method | undefined }; /** * Interprets and executes G-code commands, updating the job state accordingly @@ -19,9 +18,21 @@ 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; /** @@ -35,7 +46,6 @@ export class Interpreter { job.resumeLastPath(); commands.forEach((command) => { if (command.gcode !== undefined) { - if (typeof this[command.gcode] !== 'function') { return; }