diff --git a/demo/js/app.js b/demo/js/app.js index 9daf46b9..93281454 100644 --- a/demo/js/app.js +++ b/demo/js/app.js @@ -53,6 +53,7 @@ export const app = (window.app = createApp({ const updateUI = async () => { const { parser, + countLayers, extrusionColor, topLayerColor, lastSegmentColor, @@ -64,17 +65,16 @@ export const app = (window.app = createApp({ renderExtrusion, lineWidth, renderTubes, - extrusionWidth, - job + extrusionWidth } = preview; const { thumbnails } = parser.metadata; thumbnail.value = thumbnails['220x124']?.src; - layerCount.value = job.layers?.length; + layerCount.value = countLayers; const colors = extrusionColor instanceof Array ? extrusionColor : [extrusionColor]; const currentSettings = { - maxLayer: job.layers?.length, - endLayer: job.layers?.length, + maxLayer: countLayers, + endLayer: countLayers, singleLayerMode, renderTravel, travelColor: '#' + travelColor.getHexString(), @@ -93,7 +93,7 @@ export const app = (window.app = createApp({ }; Object.assign(settings.value, currentSettings); - preview.endLayer = job.layers?.length; + preview.endLayer = countLayers; }; const loadGCodeFromServer = async (filename) => { @@ -126,7 +126,7 @@ export const app = (window.app = createApp({ preview.render(); return; } - await preview.renderAnimated(Math.ceil(preview.job.layers?.length / 60)); + await preview.renderAnimated(Math.ceil(preview.countLayers / 60)); } else { preview.render(); } diff --git a/src/__tests__/interpreter.ts b/src/__tests__/interpreter.ts index 91f913ba..16cc044c 100644 --- a/src/__tests__/interpreter.ts +++ b/src/__tests__/interpreter.ts @@ -1,200 +1,202 @@ -import { test, expect } from 'vitest'; +import { test, expect, describe } 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 }); - 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); -}); - -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(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', () => { - 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; - job.state.tool = 5; - - interpreter.execute([command], job); - - 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); +describe('.execute', () => { + test('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('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('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('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); + }); + + test('finishes the current path at the end of the job', () => { + const job = new Job(); + const command = new GCodeCommand('G0 X1 Y2 Z3', 'g0', { x: 1, y: 2, z: 3 }); + const interpreter = new Interpreter(); + interpreter.execute([command], job); + + expect(job.paths.length).toEqual(1); + expect(job.inprogressPath).toBeUndefined(); + }); + + test('resumes the current path when doing incremental execution', () => { + const job = new Job(); + 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(); + + interpreter.execute([command1], job); + interpreter.execute([command2], job); + + expect(job.paths.length).toEqual(1); + expect(job.paths[0].vertices.length).toEqual(9); + expect(job.paths[0].vertices[6]).toEqual(command2.params.x); + expect(job.paths[0].vertices[7]).toEqual(command2.params.y); + expect(job.paths[0].vertices[8]).toEqual(command2.params.z); + }); }); -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(); - - job.state.z = 5; - interpreter.execute([command1], job); - - interpreter.g0(command2, job); +describe('.g0', () => { + test('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; + job.state.tool = 5; + + interpreter.g0(command, job); + + 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('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(); + + job.state.z = 5; + interpreter.g0(command1, job); + + interpreter.g0(command2, job); + + expect(job.paths.length).toEqual(0); + expect(job.inprogressPath?.vertices.length).toEqual(9); + expect(job.inprogressPath?.vertices[6]).toEqual(command2.params.x); + expect(job.inprogressPath?.vertices[7]).toEqual(command2.params.y); + expect(job.inprogressPath?.vertices[8]).toEqual(job.state.z); + }); + + test("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(); + + interpreter.g0(command, job); + + expect(job.paths.length).toEqual(0); + expect(job.inprogressPath?.travelType).toEqual(PathType.Travel); + }); + + test("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(); + + interpreter.g0(command, job); + + expect(job.paths.length).toEqual(0); + expect(job.inprogressPath?.travelType).toEqual('Extrusion'); + }); + + test('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('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('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); + + interpreter.g0(command2, job); + + expect(job.paths.length).toEqual(1); + expect(job.inprogressPath?.travelType).toEqual('Extrusion'); + }); + + test('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); + + interpreter.g0(command2, job); + + expect(job.paths.length).toEqual(1); + expect(job.inprogressPath?.travelType).toEqual('Travel'); + }); - 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", () => { - const command = new GCodeCommand('G0 X1 Y2', 'g0', { x: 1, y: 2 }); - const interpreter = new Interpreter(); - const job = new Job(); - - interpreter.g0(command, job); - - expect(job.paths.length).toEqual(0); - expect(job.inprogressPath?.travelType).toEqual(PathType.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 = new Job(); - - interpreter.g0(command, job); - - expect(job.paths.length).toEqual(0); - expect(job.inprogressPath?.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(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', () => { - 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); - - interpreter.g0(command2, job); - - 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', () => { - 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); - - interpreter.g0(command2, job); - - expect(job.paths.length).toEqual(1); - expect(job.inprogressPath?.travelType).toEqual('Travel'); -}); - -test('.G1 is an alias to .G0', () => { - const interpreter = new Interpreter(); + test('.G1 is an alias to .G0', () => { + const interpreter = new Interpreter(); - expect(interpreter.g1).toEqual(interpreter.g0); + expect(interpreter.g1).toEqual(interpreter.g0); + }); }); test('.G20 sets the units to inches', () => { diff --git a/src/__tests__/job.ts b/src/__tests__/job.ts index df92d1ef..152aaf06 100644 --- a/src/__tests__/job.ts +++ b/src/__tests__/job.ts @@ -73,7 +73,7 @@ describe('.layers', () => { [5, 6, 1] ]); - expect(job.layers).toEqual(null); + expect(job.layers).toEqual([]); }); test('paths without z changes are on the same layer', () => { @@ -92,29 +92,29 @@ describe('.layers', () => { expect(layers).not.toBeNull(); expect(layers).toBeInstanceOf(Array); - expect(layers?.length).toEqual(1); - expect(layers?.[0].length).toEqual(2); + expect(layers.length).toEqual(1); + expect(layers[0].paths.length).toEqual(2); }); - test('travel paths moving z above the default tolerance create a new layer', () => { + test('extrusion paths moving z above the default tolerance 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, LayersIndexer.DEFAULT_TOLERANCE + 0.01] + append_path(job, PathType.Extrusion, [ + [5, 6, LayersIndexer.DEFAULT_TOLERANCE + 0.02], + [5, 6, LayersIndexer.DEFAULT_TOLERANCE + 0.02] ]); 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); + expect(layers.length).toEqual(2); + expect(layers[0].paths.length).toEqual(1); + expect(layers[1].paths.length).toEqual(1); }); test('travel paths moving z under the default tolerance are on the same layer', () => { @@ -133,8 +133,8 @@ describe('.layers', () => { expect(layers).not.toBeNull(); expect(layers).toBeInstanceOf(Array); - expect(layers?.length).toEqual(1); - expect(layers?.[0].length).toEqual(2); + expect(layers.length).toEqual(1); + expect(layers[0].paths.length).toEqual(2); }); test('Tolerance can be set', () => { @@ -153,8 +153,8 @@ describe('.layers', () => { expect(layers).not.toBeNull(); expect(layers).toBeInstanceOf(Array); - expect(layers?.length).toEqual(1); - expect(layers?.[0].length).toEqual(2); + expect(layers.length).toEqual(1); + expect(layers[0].paths.length).toEqual(2); }); test('multiple travels in a row are on the same layer', () => { @@ -181,9 +181,8 @@ describe('.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); + expect(layers.length).toEqual(1); + expect(layers[0].paths.length).toEqual(4); }); test('extrusions after travels are on the same layer', () => { @@ -214,9 +213,9 @@ describe('.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); + expect(layers.length).toEqual(2); + expect(layers[0].paths.length).toEqual(4); + expect(layers[1].paths.length).toEqual(1); }); }); @@ -280,8 +279,116 @@ describe('.travels', () => { }); }); -function append_path(job, travelType, points) { +describe('.addPath', () => { + test('adds the path to the job', () => { + const job = new Job(); + const path = new Path(PathType.Extrusion, 0.6, 0.2, 0); + + job.addPath(path); + + expect(job.paths).toEqual([path]); + }); + + test('indexes the path', () => { + const job = new Job(); + const path = new Path(PathType.Extrusion, 0.6, 0.2, 0); + + job.addPath(path); + + expect(job.extrusions).toEqual([path]); + }); +}); + +describe('.finishPath', () => { + test('does nothing if there is no in progress path', () => { + const job = new Job(); + + job.finishPath(); + + expect(job.paths).toEqual([]); + }); + + test('adds the in progress path to the job', () => { + const job = new Job(); + const path = new Path(PathType.Extrusion, 0.6, 0.2, 0); + + path.addPoint(0, 0, 0); + + job.inprogressPath = path; + job.finishPath(); + + expect(job.paths).toEqual([path]); + }); + + test('ignores empty paths', () => { + const job = new Job(); + const path = new Path(PathType.Extrusion, 0.6, 0.2, 0); + + job.inprogressPath = path; + job.finishPath(); + + expect(job.paths).toEqual([]); + }); + + test('clears the in progress path', () => { + const job = new Job(); + const path = new Path(PathType.Extrusion, 0.6, 0.2, 0); + + path.addPoint(0, 0, 0); + + job.inprogressPath = path; + job.finishPath(); + + expect(job.inprogressPath).toBeUndefined(); + }); +}); + +describe('.resumeLastPath', () => { + test('pops the last path and makes it in progress', () => { + const job = new Job(); + + job.resumeLastPath(); + + expect(job.paths).toEqual([]); + }); + + test('adds the in progress path to the job', () => { + const job = new Job(); + + const path = append_path(job, PathType.Extrusion, [[0, 0, 0]]); + + job.resumeLastPath(); + + expect(job.inprogressPath).toEqual(path); + expect(job.paths).toEqual([]); + }); + + test('clears the in progress path', () => { + const job = new Job(); + const path = new Path(PathType.Extrusion, 0.6, 0.2, 0); + + path.addPoint(0, 0, 0); + + job.inprogressPath = path; + job.resumeLastPath(); + + expect(job.inprogressPath).toBeUndefined(); + }); + + test('the path is removed from indexes to not appear twice', () => { + const job = new Job(); + + append_path(job, PathType.Extrusion, [[0, 0, 0]]); + job.resumeLastPath(); + + expect(job.extrusions).toEqual([]); + expect(job.layers[job.layers.length - 1].paths).toEqual([]); + }); +}); + +function append_path(job: Job, travelType, points: [number, number, number][]): Path { const path = new Path(travelType, 0.6, 0.2, job.state.tool); points.forEach((point: [number, number, number]) => path.addPoint(...point)); job.addPath(path); + return path; } diff --git a/src/interpreter.ts b/src/interpreter.ts index 44630b7c..80943929 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -6,6 +6,7 @@ export class Interpreter { // eslint-disable-next-line no-unused-vars [key: string]: (...args: unknown[]) => unknown; execute(commands: GCodeCommand[], job = new Job()): Job { + job.resumeLastPath(); commands.forEach((command) => { if (command.gcode !== undefined) { if (this[command.gcode] === undefined) { @@ -14,6 +15,7 @@ export class Interpreter { this[command.gcode](command, job); } }); + job.finishPath(); return job; } diff --git a/src/job.ts b/src/job.ts index 6ede3239..7c51cf08 100644 --- a/src/job.ts +++ b/src/job.ts @@ -15,22 +15,35 @@ export class State { } } +export class Layer { + public layer: number; + public paths: Path[]; + public lineNumber: number; + public height: number = 0; + public z: number = 0; + constructor(layer: number, paths: Path[], lineNumber: number, height: number = 0, z: number = 0) { + this.layer = layer; + this.paths = paths; + this.lineNumber = lineNumber; + this.height = height; + this.z = z; + } +} + export class Job { - paths: Path[]; + paths: Path[] = []; state: State; private travelPaths: Path[] = []; private extrusionPaths: Path[] = []; - private layersPaths: Path[][] | null; + private _layers: Layer[] = []; private indexers: Indexer[]; inprogressPath: Path | undefined; constructor(opts: { state?: State; minLayerThreshold?: number } = {}) { - this.paths = []; this.state = opts.state || State.initial; - this.layersPaths = [[]]; this.indexers = [ new TravelTypeIndexer({ travel: this.travelPaths, extrusion: this.extrusionPaths }), - new LayersIndexer(this.layersPaths, opts.minLayerThreshold) + new LayersIndexer(this._layers, opts.minLayerThreshold) ]; } @@ -42,8 +55,13 @@ export class Job { return this.travelPaths; } - get layers(): Path[][] | null { - return this.layersPaths; + get layers(): Layer[] { + return this._layers; + } + + addPath(path: Path): void { + this.paths.push(path); + this.indexPath(path); } finishPath(): void { @@ -52,16 +70,25 @@ export class Job { } if (this.inprogressPath.vertices.length > 0) { this.addPath(this.inprogressPath); + this.inprogressPath = undefined; } } - addPath(path: Path): void { - this.paths.push(path); - this.indexPath(path); + resumeLastPath(): void { + this.inprogressPath = this.paths.pop(); + [this.extrusionPaths, this.travelPaths, this.layers[this.layers.length - 1]?.paths].forEach((indexer) => { + if (indexer === undefined || indexer.length === 0) { + return; + } + const travelIndex = indexer.indexOf(this.inprogressPath); + if (travelIndex > -1) { + indexer.splice(travelIndex, 1); + } + }); } isPlanar(): boolean { - return this.layersPaths !== null; + return this.layers.length > 0; } private indexPath(path: Path): void { @@ -71,7 +98,7 @@ export class Job { } catch (e) { if (e instanceof NonApplicableIndexer) { if (e instanceof NonPlanarPathError) { - this.layersPaths = null; + this._layers = []; } const i = this.indexers.indexOf(indexer); this.indexers.splice(i, 1); @@ -85,8 +112,8 @@ export class Job { class NonApplicableIndexer extends Error {} export class Indexer { - protected indexes: Record | Path[][]; - constructor(indexes: Record | Path[][]) { + protected indexes: Record | Layer[]; + constructor(indexes: Record | Layer[]) { this.indexes = indexes; } sortIn(path: Path): void { @@ -117,52 +144,40 @@ class NonPlanarPathError extends NonApplicableIndexer { } export class LayersIndexer extends Indexer { static readonly DEFAULT_TOLERANCE = 0.05; - protected declare indexes: Path[][]; + protected declare indexes: Layer[]; private tolerance: number; - constructor(indexes: Path[][], tolerance: number = LayersIndexer.DEFAULT_TOLERANCE) { + constructor(indexes: Layer[], tolerance: number = LayersIndexer.DEFAULT_TOLERANCE) { super(indexes); this.tolerance = tolerance; } sortIn(path: Path): void { - if (path.travelType === PathType.Extrusion && path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2])) { + if ( + path.travelType === PathType.Extrusion && + path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] - arr[2] >= this.tolerance) + ) { throw new NonPlanarPathError(); } - if (path.travelType === PathType.Extrusion) { - this.lastLayer().push(path); - } else { - const verticalTravels = path.vertices - .map((_, i, arr) => { - if (i % 3 === 2 && arr[i] - arr[2] > this.tolerance) { - return arr[i] - arr[2]; - } - }) - .filter((z) => z !== undefined); - const hasVerticalTravel = verticalTravels.length > 0; - const hasExtrusions = this.lastLayer().find((p) => p.travelType === PathType.Extrusion); + if (this.indexes[this.indexes.length - 1] === undefined) { + this.createLayer(path.vertices[2]); + } - if (hasVerticalTravel && hasExtrusions) { - this.createLayer(); + if (path.travelType === PathType.Extrusion) { + if (path.vertices[2] - (this.lastLayer().z || 0) > this.tolerance) { + this.createLayer(path.vertices[2]); } - this.lastLayer().push(path); } + this.lastLayer().paths.push(path); } - private lastLayer(): Path[] { - if (this.indexes === undefined) { - this.indexes = [[]]; - } - - if (this.indexes[this.indexes.length - 1] === undefined) { - this.createLayer(); - return this.lastLayer(); - } + private lastLayer(): Layer { return this.indexes[this.indexes.length - 1]; } - private createLayer(): void { - const newLayer: Path[] = []; - this.indexes.push(newLayer); + private createLayer(z: number): void { + const layerNumber = this.indexes.length; + const height = z - this.lastLayer()?.z; + this.indexes.push(new Layer(this.indexes.length, [], layerNumber, height, z)); } } diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index e5328d10..5a79f519 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -91,7 +91,7 @@ export class WebGLPreview { nonTravelmoves: string[] = []; disableGradient = false; - job: Job; + private job: Job; interpreter = new Interpreter(); parser = new Parser(); @@ -254,6 +254,10 @@ export class WebGLPreview { this._lastSegmentColor = value !== undefined ? new Color(value) : undefined; } + get countLayers(): number { + return this.job.layers.length; + } + /** @internal */ animate(): void { this.animationFrameId = requestAnimationFrame(() => this.animate()); @@ -353,7 +357,7 @@ export class WebGLPreview { this.group = this.createGroup('layer' + this.renderLayerIndex); const endIndex = Math.min(this.renderLayerIndex + layerCount, this.job.layers?.length - 1); - const pathsToRender = this.job.layers?.slice(this.renderLayerIndex, endIndex)?.flatMap((l) => l); + const pathsToRender = this.job.layers.slice(this.renderLayerIndex, endIndex)?.flatMap((l) => l.paths); this.renderGeometries(pathsToRender.filter((path) => path.travelType === 'Extrusion')); this.renderLines(