diff --git a/src/__tests__/job.ts b/src/__tests__/job.ts index 7968fa60..df92d1ef 100644 --- a/src/__tests__/job.ts +++ b/src/__tests__/job.ts @@ -1,5 +1,5 @@ import { test, expect, describe } from 'vitest'; -import { Job, State } from '../job'; +import { Job, State, LayersIndexer } from '../job'; import { PathType, Path } from '../path'; test('it has an initial state', () => { @@ -96,7 +96,7 @@ describe('.layers', () => { expect(layers?.[0].length).toEqual(2); }); - test('travel paths moving z create a new layer', () => { + test('travel paths moving z above the default tolerance create a new layer', () => { const job = new Job(); append_path(job, PathType.Extrusion, [ @@ -105,7 +105,7 @@ describe('.layers', () => { ]); append_path(job, PathType.Travel, [ [5, 6, 0], - [5, 6, 1] + [5, 6, LayersIndexer.DEFAULT_TOLERANCE + 0.01] ]); const layers = job.layers; @@ -117,6 +117,46 @@ describe('.layers', () => { expect(layers?.[1].length).toEqual(1); }); + test('travel paths moving z under the default tolerance 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, LayersIndexer.DEFAULT_TOLERANCE - 0.01] + ]); + + const layers = job.layers; + + expect(layers).not.toBeNull(); + expect(layers).toBeInstanceOf(Array); + expect(layers?.length).toEqual(1); + expect(layers?.[0].length).toEqual(2); + }); + + test('Tolerance can be set', () => { + const job = new Job({ minLayerThreshold: 0.1 }); + + append_path(job, PathType.Extrusion, [ + [0, 0, 0], + [1, 2, 0] + ]); + append_path(job, PathType.Travel, [ + [5, 6, 0], + [5, 6, 0.09] + ]); + + const layers = job.layers; + + expect(layers).not.toBeNull(); + expect(layers).toBeInstanceOf(Array); + expect(layers?.length).toEqual(1); + expect(layers?.[0].length).toEqual(2); + }); + test('multiple travels in a row are on the same layer', () => { const job = new Job(); diff --git a/src/job.ts b/src/job.ts index de372a45..6ede3239 100644 --- a/src/job.ts +++ b/src/job.ts @@ -24,13 +24,13 @@ export class Job { private indexers: Indexer[]; inprogressPath: Path | undefined; - constructor(state?: State) { + constructor(opts: { state?: State; minLayerThreshold?: number } = {}) { this.paths = []; - this.state = state || State.initial; + this.state = opts.state || State.initial; this.layersPaths = [[]]; this.indexers = [ new TravelTypeIndexer({ travel: this.travelPaths, extrusion: this.extrusionPaths }), - new LayersIndexer(this.layersPaths) + new LayersIndexer(this.layersPaths, opts.minLayerThreshold) ]; } @@ -61,7 +61,7 @@ export class Job { } isPlanar(): boolean { - return this.paths.find((path) => path.travelType === PathType.Extrusion && path.hasVerticalMoves()) === undefined; + return this.layersPaths !== null; } private indexPath(path: Path): void { @@ -84,7 +84,7 @@ export class Job { } class NonApplicableIndexer extends Error {} -class Indexer { +export class Indexer { protected indexes: Record | Path[][]; constructor(indexes: Record | Path[][]) { this.indexes = indexes; @@ -95,7 +95,7 @@ class Indexer { } } -class TravelTypeIndexer extends Indexer { +export class TravelTypeIndexer extends Indexer { protected declare indexes: Record; constructor(indexes: Record) { super(indexes); @@ -115,10 +115,13 @@ class NonPlanarPathError extends NonApplicableIndexer { super("Non-planar paths can't be indexed by layer"); } } -class LayersIndexer extends Indexer { +export class LayersIndexer extends Indexer { + static readonly DEFAULT_TOLERANCE = 0.05; protected declare indexes: Path[][]; - constructor(indexes: Path[][]) { + private tolerance: number; + constructor(indexes: Path[][], tolerance: number = LayersIndexer.DEFAULT_TOLERANCE) { super(indexes); + this.tolerance = tolerance; } sortIn(path: Path): void { @@ -129,10 +132,17 @@ class LayersIndexer extends Indexer { if (path.travelType === PathType.Extrusion) { this.lastLayer().push(path); } else { - if ( - path.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2]) && - this.lastLayer().find((p) => p.travelType === PathType.Extrusion) - ) { + 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 (hasVerticalTravel && hasExtrusions) { this.createLayer(); } this.lastLayer().push(path); diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index 6ee98ca0..bf7797d7 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -39,6 +39,7 @@ export type GCodePreviewOptions = { lineWidth?: number; lineHeight?: number; nonTravelMoves?: string[]; + minLayerThreshold?: number; renderExtrusion?: boolean; renderTravel?: boolean; startLayer?: number; @@ -61,6 +62,7 @@ export type GCodePreviewOptions = { }; export class WebGLPreview { + minLayerThreshold: number; /** * @deprecated Please use the `canvas` param instead. */ @@ -89,8 +91,8 @@ export class WebGLPreview { nonTravelmoves: string[] = []; disableGradient = false; + job: Job; interpreter = new Interpreter(); - job = new Job(); parser = new Parser(); // rendering @@ -118,6 +120,8 @@ export class WebGLPreview { private devGui?: DevGUI; constructor(opts: GCodePreviewOptions) { + this.minLayerThreshold = opts.minLayerThreshold ?? this.minLayerThreshold; + this.job = new Job({ minLayerThreshold: this.minLayerThreshold }); this.scene = new Scene(); this.scene.background = this._backgroundColor; if (opts.backgroundColor !== undefined) { @@ -366,7 +370,7 @@ export class WebGLPreview { clear(): void { this.resetState(); this.parser = new Parser(); - this.job = new Job(); + this.job = new Job({ minLayerThreshold: this.minLayerThreshold }); } // reset processing state