diff --git a/src/__tests__/interpreter.ts b/src/__tests__/interpreter.ts index d53b6a2d..98fb1910 100644 --- a/src/__tests__/interpreter.ts +++ b/src/__tests__/interpreter.ts @@ -74,14 +74,15 @@ test('.G0 starts a path if the job has none', () => { 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); - expect(job.paths[0].vertices[3]).toEqual(1); - expect(job.paths[0].vertices[4]).toEqual(2); - expect(job.paths[0].vertices[5]).toEqual(0); + expect(job.paths.length).toEqual(0); + expect(job.inprogressPath).not.toBeNull(); + expect(job.inprogressPath?.vertices.length).toEqual(6); + expect(job.inprogressPath?.vertices[0]).toEqual(0); + expect(job.inprogressPath?.vertices[1]).toEqual(0); + expect(job.inprogressPath?.vertices[2]).toEqual(0); + expect(job.inprogressPath?.vertices[3]).toEqual(1); + expect(job.inprogressPath?.vertices[4]).toEqual(2); + expect(job.inprogressPath?.vertices[5]).toEqual(0); }); test('.G0 starts a path if the job has none, starting at the job current state', () => { @@ -94,12 +95,12 @@ test('.G0 starts a path if the job has none, starting at the job current state', 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); - expect(job.paths[0].tool).toEqual(5); + expect(job.paths.length).toEqual(0); + expect(job.inprogressPath?.vertices.length).toEqual(6); + expect(job.inprogressPath?.vertices[0]).toEqual(3); + expect(job.inprogressPath?.vertices[1]).toEqual(4); + expect(job.inprogressPath?.vertices[2]).toEqual(0); + expect(job.inprogressPath?.tool).toEqual(5); }); test('.G0 continues the path if the job has one', () => { @@ -113,11 +114,11 @@ test('.G0 continues the path if the job has one', () => { interpreter.G0(command2, job); - 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(5); + expect(job.paths.length).toEqual(0); + expect(job.inprogressPath?.vertices.length).toEqual(9); + expect(job.inprogressPath?.vertices[6]).toEqual(3); + expect(job.inprogressPath?.vertices[7]).toEqual(4); + expect(job.inprogressPath?.vertices[8]).toEqual(5); }); test(".G0 assigns the travel type if there's no extrusion", () => { @@ -127,8 +128,8 @@ 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(PathType.Travel); + expect(job.paths.length).toEqual(0); + expect(job.inprogressPath?.travelType).toEqual(PathType.Travel); }); test(".G0 assigns the extrusion type if there's extrusion", () => { @@ -138,8 +139,8 @@ test(".G0 assigns the extrusion type if there's extrusion", () => { interpreter.G0(command, job); - expect(job.paths.length).toEqual(1); - expect(job.paths[0].travelType).toEqual('Extrusion'); + expect(job.paths.length).toEqual(0); + expect(job.inprogressPath?.travelType).toEqual('Extrusion'); }); test('.G0 assigns the travel type if the extrusion is a retraction', () => { @@ -149,8 +150,19 @@ test('.G0 assigns the travel type if the extrusion is a retraction', () => { interpreter.G0(command, job); - expect(job.paths.length).toEqual(1); - expect(job.paths[0].travelType).toEqual('Travel'); + expect(job.paths.length).toEqual(0); + expect(job.inprogressPath?.travelType).toEqual('Travel'); +}); + +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(0); + expect(job.inprogressPath?.travelType).toEqual('Travel'); }); test('.G0 starts a new path if the travel type changes from Travel to Extrusion', () => { @@ -162,9 +174,8 @@ test('.G0 starts a new path if the travel type changes from Travel to Extrusion' interpreter.G0(command2, job); - expect(job.paths.length).toEqual(2); - expect(job.paths[0].travelType).toEqual('Travel'); - expect(job.paths[1].travelType).toEqual('Extrusion'); + expect(job.paths.length).toEqual(1); + expect(job.inprogressPath?.travelType).toEqual('Extrusion'); }); test('.G0 starts a new path if the travel type changes from Extrusion to Travel', () => { @@ -176,9 +187,8 @@ test('.G0 starts a new path if the travel type changes from Extrusion to Travel' interpreter.G0(command2, job); - expect(job.paths.length).toEqual(2); - expect(job.paths[0].travelType).toEqual('Extrusion'); - expect(job.paths[1].travelType).toEqual('Travel'); + expect(job.paths.length).toEqual(1); + expect(job.inprogressPath?.travelType).toEqual('Travel'); }); test('.G1 is an alias to .G0', () => { diff --git a/src/__tests__/job.ts b/src/__tests__/job.ts index 2d342a99..7968fa60 100644 --- a/src/__tests__/job.ts +++ b/src/__tests__/job.ts @@ -12,8 +12,14 @@ 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]); + 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); }); @@ -21,8 +27,14 @@ describe('.isPlanar', () => { 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]); + 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); }); @@ -30,9 +42,19 @@ describe('.isPlanar', () => { 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]); + 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); }); @@ -42,8 +64,14 @@ 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]); + 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); }); @@ -51,8 +79,14 @@ describe('.layers', () => { 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]); + 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; @@ -65,8 +99,14 @@ describe('.layers', () => { 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]); + 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; @@ -80,10 +120,22 @@ describe('.layers', () => { 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]); + 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; @@ -97,11 +149,26 @@ describe('.layers', () => { 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]); + 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; @@ -117,9 +184,18 @@ 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]); + 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; @@ -136,10 +212,22 @@ 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]); + 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; @@ -152,8 +240,8 @@ describe('.travels', () => { }); }); -function append_path(job, travelType, vertices) { +function append_path(job, travelType, points) { const path = new Path(travelType, 0.6, 0.2, job.state.tool); - path.vertices = vertices; + points.forEach((point: [number, number, number]) => path.addPoint(...point)); job.addPath(path); } diff --git a/src/__tests__/path.ts b/src/__tests__/path.ts index af040e0b..c845d475 100644 --- a/src/__tests__/path.ts +++ b/src/__tests__/path.ts @@ -67,7 +67,8 @@ test('.path returns an array of Vector3', () => { 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]; + path.addPoint(0, 0, 0); + path.addPoint(1, 2, 3); const result = path.geometry() as ExtrusionGeometry; @@ -80,7 +81,8 @@ test('.geometry returns an ExtrusionGeometry from the path', () => { 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]; + path.addPoint(0, 0, 0); + path.addPoint(1, 2, 3); const result = path.geometry() as ExtrusionGeometry; @@ -90,7 +92,8 @@ test('.geometry returns an ExtrusionGeometry with the path extrusion width', () 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]; + path.addPoint(0, 0, 0); + path.addPoint(1, 2, 3); const result = path.geometry() as ExtrusionGeometry; @@ -109,7 +112,8 @@ test('.geometry returns an empty BufferGeometry if there are less than 3 vertice 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]; + path.addPoint(0, 0, 0); + path.addPoint(1, 2, 3); const result = path.line(); diff --git a/src/interpreter.ts b/src/interpreter.ts index 122279ee..40523d04 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -17,18 +17,18 @@ export class Interpreter { const { x, y, z, e } = command.params; const { state } = job; - let lastPath = job.paths[job.paths.length - 1]; + let currentPath = job.inprogressPath; const pathType = e > 0 ? PathType.Extrusion : PathType.Travel; - if (lastPath === undefined || lastPath.travelType !== pathType) { - lastPath = this.breakPath(job, pathType); + if (currentPath === undefined || currentPath.travelType !== pathType) { + currentPath = this.breakPath(job, pathType); } state.x = x ?? state.x; state.y = y ?? state.y; state.z = z ?? state.z; - lastPath.addPoint(state.x, state.y, state.z); + currentPath.addPoint(state.x, state.y, state.z); } G1 = this.G0; @@ -39,11 +39,11 @@ export class Interpreter { const { state } = job; const cw = command.code === Code.G2; - let lastPath = job.paths[job.paths.length - 1]; + let currentPath = job.inprogressPath; const pathType = e ? PathType.Extrusion : PathType.Travel; - if (lastPath === undefined || lastPath.travelType !== pathType) { - lastPath = this.breakPath(job, pathType); + if (currentPath === undefined || currentPath.travelType !== pathType) { + currentPath = this.breakPath(job, pathType); } if (r) { @@ -118,14 +118,14 @@ export class Interpreter { px = centerX + arcRadius * Math.cos(currentAngle); py = centerY + arcRadius * Math.sin(currentAngle); pz += zStep; - lastPath.addPoint(px, py, pz); + currentPath.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); + currentPath.addPoint(state.x, state.y, state.z); } G3 = this.G2; @@ -170,9 +170,10 @@ export class Interpreter { } private breakPath(job: Job, newType: PathType): Path { - const lastPath = new Path(newType, 0.6, 0.2, job.state.tool); - job.addPath(lastPath); - lastPath.addPoint(job.state.x, job.state.y, job.state.z); - return lastPath; + job.finishPath(); + const currentPath = new Path(newType, 0.6, 0.2, job.state.tool); + currentPath.addPoint(job.state.x, job.state.y, job.state.z); + job.inprogressPath = currentPath; + return currentPath; } } diff --git a/src/job.ts b/src/job.ts index 3a2370d5..de372a45 100644 --- a/src/job.ts +++ b/src/job.ts @@ -20,15 +20,18 @@ export class Job { state: State; private travelPaths: Path[] = []; private extrusionPaths: Path[] = []; - private layersPaths: Path[][] | null = [[]]; - private indexers: Indexer[] = [ - new TravelTypeIndexer({ travel: this.travelPaths, extrusion: this.extrusionPaths }), - new LayersIndexer(this.layersPaths) - ]; + private layersPaths: Path[][] | null; + private indexers: Indexer[]; + inprogressPath: Path | undefined; constructor(state?: State) { this.paths = []; this.state = state || State.initial; + this.layersPaths = [[]]; + this.indexers = [ + new TravelTypeIndexer({ travel: this.travelPaths, extrusion: this.extrusionPaths }), + new LayersIndexer(this.layersPaths) + ]; } get extrusions(): Path[] { @@ -43,18 +46,22 @@ export class Job { return this.layersPaths; } + finishPath(): void { + if (this.inprogressPath === undefined) { + return; + } + if (this.inprogressPath.vertices.length > 0) { + this.addPath(this.inprogressPath); + } + } + addPath(path: Path): void { this.paths.push(path); this.indexPath(path); } isPlanar(): boolean { - return ( - this.paths.find( - (path) => - path.travelType === PathType.Extrusion && path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2]) - ) === undefined - ); + return this.paths.find((path) => path.travelType === PathType.Extrusion && path.hasVerticalMoves()) === undefined; } private indexPath(path: Path): void { @@ -62,12 +69,14 @@ export class Job { try { indexer.sortIn(path); } catch (e) { - if (e.instanceOf(NonApplicableIndexer)) { - if (e.instanceOf(NonPlanarPathError)) { + if (e instanceof NonApplicableIndexer) { + if (e instanceof NonPlanarPathError) { this.layersPaths = null; } const i = this.indexers.indexOf(indexer); this.indexers.splice(i, 1); + } else { + throw e; } } }); @@ -87,7 +96,7 @@ class Indexer { } class TravelTypeIndexer extends Indexer { - protected indexes: Record; + protected declare indexes: Record; constructor(indexes: Record) { super(indexes); } @@ -107,7 +116,7 @@ class NonPlanarPathError extends NonApplicableIndexer { } } class LayersIndexer extends Indexer { - protected indexes: Path[][]; + protected declare indexes: Path[][]; constructor(indexes: Path[][]) { super(indexes); } @@ -118,24 +127,32 @@ class LayersIndexer extends Indexer { } if (path.travelType === PathType.Extrusion) { - this.currentLayer().push(path); + this.lastLayer().push(path); } else { if ( path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2]) && - this.currentLayer().find((p) => p.travelType === PathType.Extrusion) + this.lastLayer().find((p) => p.travelType === PathType.Extrusion) ) { - this.indexes.push([]); + this.createLayer(); } - this.currentLayer().push(path); + this.lastLayer().push(path); } } - private currentLayer(): Path[] { - const layer = this.indexes[this.indexes.length - 1]; - if (layer === undefined) { - this.indexes.push([]); - return this.currentLayer(); + private lastLayer(): Path[] { + if (this.indexes === undefined) { + this.indexes = [[]]; } - return layer; + + if (this.indexes[this.indexes.length - 1] === undefined) { + this.createLayer(); + return this.lastLayer(); + } + return this.indexes[this.indexes.length - 1]; + } + + private createLayer(): void { + const newLayer: Path[] = []; + this.indexes.push(newLayer); } } diff --git a/src/path.ts b/src/path.ts index 8a89bdf3..4f6fa2ee 100644 --- a/src/path.ts +++ b/src/path.ts @@ -9,31 +9,35 @@ export enum PathType { export class Path { public travelType: PathType; - public vertices: number[]; - extrusionWidth: number; - lineHeight: number; - tool: number; + public extrusionWidth: number; + public lineHeight: number; + public tool: number; + private _vertices: number[]; constructor(travelType: PathType, extrusionWidth = 0.6, lineHeight = 0.2, tool = 0) { this.travelType = travelType; - this.vertices = []; + this._vertices = []; this.extrusionWidth = extrusionWidth; this.lineHeight = lineHeight; this.tool = tool; } + get vertices(): number[] { + return this._vertices; + } + addPoint(x: number, y: number, z: number): void { - this.vertices.push(x, y, z); + this._vertices.push(x, y, z); } checkLineContinuity(x: number, y: number, z: number): boolean { - if (this.vertices.length < 3) { + 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]; + 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; } @@ -41,14 +45,14 @@ export class Path { 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])); + 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.vertices.length < 3) { + if (this._vertices.length < 3) { return new BufferGeometry(); } @@ -58,4 +62,8 @@ export class Path { line(): BufferGeometry { return new BufferGeometry().setFromPoints(this.path()); } + + hasVerticalMoves(): boolean { + return this.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2]); + } }