diff --git a/demo/apply-cors.sh b/demo/apply-cors.sh new file mode 100755 index 00000000..12627263 --- /dev/null +++ b/demo/apply-cors.sh @@ -0,0 +1,2 @@ +#!/bin/sh +gcloud storage buckets update gs://gcode-preview.firebasestorage.app --cors-file=cors.json \ No newline at end of file diff --git a/demo/cors.json b/demo/cors.json new file mode 100644 index 00000000..695672ca --- /dev/null +++ b/demo/cors.json @@ -0,0 +1,8 @@ +[ + { + "origin": ["*"], + "method": ["GET"], + "responseHeader": ["Content-Type"], + "maxAgeSeconds": 3600 + } +] \ No newline at end of file diff --git a/package.json b/package.json index 5ac4547e..930292ff 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "lint": "prettier --check . && eslint -c .eslintrc.js .", "lint:fix": "eslint -c .eslintrc.js . --fix", "prettier:fix": "prettier --plugin-search-dir . --write .", - "test": "vitest", + "test": "vitest run", "test:watch": "vitest --watch", "preversion": "npm run typeCheck && npm run test && npm run lint", "version:patch": "npm version patch", diff --git a/src/build-volume.ts b/src/build-volume.ts index 88edfaa1..1b9eb5af 100644 --- a/src/build-volume.ts +++ b/src/build-volume.ts @@ -3,13 +3,28 @@ import { AxesHelper, Group, Vector3 } from 'three'; import { LineBox } from './helpers/line-box'; import { type Disposable } from './helpers/three-utils'; +/** + * Represents the build volume of a 3D printer. + */ export class BuildVolume { + /** Width of the build volume in mm */ x: number; + /** Depth of the build volume in mm */ y: number; + /** Height of the build volume in mm */ z: number; + /** Color used for the grid */ color: number; + /** List of disposable objects that need cleanup */ private disposables: Disposable[] = []; + /** + * Creates a new BuildVolume instance + * @param x - Width in mm + * @param y - Depth in mm + * @param z - Height in mm + * @param color - Color for visualization (default: 0x888888) + */ constructor(x: number, y: number, z: number, color: number = 0x888888) { this.x = x; this.y = y; @@ -17,6 +32,10 @@ export class BuildVolume { this.color = color; } + /** + * Creates and positions the XYZ axes helper for the build volume + * @returns Configured AxesHelper instance + */ createAxes(): AxesHelper { const axes = new AxesHelper(10); @@ -32,18 +51,30 @@ export class BuildVolume { return axes; } + /** + * Creates a grid visualization for the build volume's base + * @returns Configured Grid instance + */ createGrid(): Grid { const grid = new Grid(this.x, 10, this.y, 10, this.color); this.disposables.push(grid); return grid; } + /** + * Creates a wireframe box representing the build volume boundaries + * @returns Configured LineBox instance + */ createLineBox(): LineBox { const lineBox = new LineBox(this.x, this.z, this.y, this.color); this.disposables.push(lineBox); return lineBox; } + /** + * Creates a group containing all visualization elements (box, grid, axes) + * @returns Group containing all build volume visualizations + */ createGroup(): Group { const group = new Group(); group.add(this.createLineBox()); @@ -53,6 +84,9 @@ export class BuildVolume { return group; } + /** + * Cleans up all disposable resources created by this build volume + */ dispose(): void { this.disposables.forEach((disposable) => disposable.dispose()); } diff --git a/src/dev-gui.ts b/src/dev-gui.ts index 5ec5a899..1664e590 100644 --- a/src/dev-gui.ts +++ b/src/dev-gui.ts @@ -1,5 +1,15 @@ import { GUI } from 'lil-gui'; - +import { WebGLPreview } from './webgl-preview'; + +/** + * Configuration options for development mode GUI + * @property camera - Show camera controls (default: false) + * @property renderer - Show renderer stats (default: false) + * @property parser - Show parser/job stats (default: false) + * @property buildVolume - Show build volume controls (default: false) + * @property devHelpers - Show development helpers (default: false) + * @property statsContainer - Container element for stats display + */ export type DevModeOptions = { camera?: boolean | false; renderer?: boolean | false; @@ -9,13 +19,21 @@ export type DevModeOptions = { statsContainer?: HTMLElement | undefined; }; +/** + * Development GUI for debugging and monitoring the 3D preview + */ class DevGUI { private gui: GUI; - private watchedObject; + private watchedObject: WebGLPreview; private options?: DevModeOptions | undefined; private openFolders: string[] = []; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(watchedObject: any, options?: DevModeOptions | undefined) { + + /** + * Creates a new DevGUI instance + * @param watchedObject - The object to monitor and control + * @param options - Configuration options for the GUI + */ + constructor(watchedObject: WebGLPreview, options?: DevModeOptions | undefined) { this.watchedObject = watchedObject; this.options = options; @@ -25,10 +43,13 @@ class DevGUI { this.setup(); } + /** + * Sets up the development GUI with all configured panels + */ setup(): void { this.loadOpenFolders(); if (!this.options || this.options.renderer) { - this.setupRedererFolder(); + this.setupRendererFolder(); } if (!this.options || this.options.camera) { @@ -48,6 +69,9 @@ class DevGUI { } } + /** + * Resets the GUI by destroying and recreating it + */ reset(): void { this.gui.destroy(); this.gui = new GUI(); @@ -55,10 +79,16 @@ class DevGUI { this.setup(); } + /** + * Loads the state of open folders from localStorage + */ loadOpenFolders(): void { this.openFolders = JSON.parse(localStorage.getItem('dev-gui-open') || '{}').open || []; } + /** + * Saves the state of open folders to localStorage + */ saveOpenFolders(): void { this.openFolders = this.gui .foldersRecursive() @@ -72,7 +102,10 @@ class DevGUI { localStorage.setItem('dev-gui-open', JSON.stringify({ open: this.openFolders })); } - private setupRedererFolder(): void { + /** + * Sets up the renderer stats panel with memory and render call information + */ + private setupRendererFolder(): void { const render = this.gui.addFolder('Render Info'); if (!this.openFolders.includes('Render Info')) { render.close(); @@ -89,6 +122,9 @@ class DevGUI { render.add(this.watchedObject, '_lastRenderTime').listen(); } + /** + * Sets up the camera controls panel with position and rotation controls + */ private setupCameraFolder(): void { const camera = this.gui.addFolder('Camera'); if (!this.openFolders.includes('Camera')) { @@ -108,6 +144,9 @@ class DevGUI { cameraRotation.add(this.watchedObject.camera.rotation, 'z').listen(); } + /** + * Sets up the parser/job stats panel with path and line count information + */ private setupParserFolder(): void { const parser = this.gui.addFolder('Job'); if (!this.openFolders.includes('Job')) { @@ -123,6 +162,9 @@ class DevGUI { parser.add(this.watchedObject.parser.lines, 'length').name('lines.count').listen(); } + /** + * Sets up the build volume controls panel with dimension controls + */ private setupBuildVolumeFolder(): void { if (!this.watchedObject.buildVolume) { return; @@ -160,6 +202,9 @@ class DevGUI { }); } + /** + * Sets up the development helpers panel with wireframe and render controls + */ private setupDevHelpers(): void { const devHelpers = this.gui.addFolder('Dev Helpers'); if (!this.openFolders.includes('Dev Helpers')) { diff --git a/src/extrusion-geometry.ts b/src/extrusion-geometry.ts index 545fdea1..24c73b1b 100644 --- a/src/extrusion-geometry.ts +++ b/src/extrusion-geometry.ts @@ -1,16 +1,37 @@ import { BufferGeometry, Float32BufferAttribute, Vector2, Vector3 } from 'three'; +/** + * A geometry class for extruding 3D paths into volumetric shapes + */ class ExtrusionGeometry extends BufferGeometry { + /** + * Parameters used to create this geometry + */ parameters: { + /** Array of points defining the path */ points: Vector3[]; + /** Width of the extruded shape */ lineWidth: number; + /** Height of the extruded shape */ lineHeight: number; + /** Number of segments around the circumference */ radialSegments: number; + /** Whether the path is closed */ closed: boolean; }; + /** + * The geometry type + */ readonly type: string; + /** + * Creates a new ExtrusionGeometry + * @param points - Array of points defining the path (default: single point at origin) + * @param lineWidth - Width of the extruded shape (default: 0.6) + * @param lineHeight - Height of the extruded shape (default: 0.2) + * @param radialSegments - Number of segments around the circumference (default: 8) + */ constructor( points: Vector3[] = [new Vector3()], lineWidth: number = 0.6, @@ -55,6 +76,9 @@ class ExtrusionGeometry extends BufferGeometry { // functions + /** + * Generates all buffer data (vertices, normals, UVs, and indices) + */ function generateBufferData(): void { for (let i = 0; i < points.length; i++) { generateSegment(i); @@ -77,6 +101,10 @@ class ExtrusionGeometry extends BufferGeometry { generateIndices(); } + /** + * Generates vertices and normals for a segment of the path + * @param i - Index of the segment to generate + */ function generateSegment(i: number): void { // First get the tangent to the corner between the two segments. @@ -106,6 +134,9 @@ class ExtrusionGeometry extends BufferGeometry { } } + /** + * Generates face indices to connect the vertices + */ function generateIndices(): void { for (let j = 1; j < points.length; j++) { for (let i = 1; i <= radialSegments; i++) { @@ -122,6 +153,9 @@ class ExtrusionGeometry extends BufferGeometry { } } + /** + * Generates UV coordinates for texture mapping + */ function generateUVs(): void { for (let i = 0; i < points.length; i++) { for (let j = 0; j <= radialSegments; j++) { @@ -133,6 +167,11 @@ class ExtrusionGeometry extends BufferGeometry { } } + /** + * Computes the corner angles (position, normal, binormal) for a segment + * @param i - Index of the segment + * @returns Array containing position, normal and binormal vectors + */ function computeCornerAngles(i: number): Array { const P = points[i]; const tangent = new Vector3(); diff --git a/src/gcode-parser.ts b/src/gcode-parser.ts index 354cddb2..9a613216 100644 --- a/src/gcode-parser.ts +++ b/src/gcode-parser.ts @@ -1,80 +1,148 @@ -/* eslint-disable no-unused-vars */ import { Thumbnail } from './thumbnail'; -type singleLetter = - | 'a' - | 'b' - | 'c' - | 'd' - | 'e' - | 'f' - | 'g' - | 'h' - | 'i' - | 'j' - | 'k' - | 'l' - | 'm' - | 'n' - | 'o' - | 'p' - | 'q' - | 'r' - | 's' - | 't' - | 'u' - | 'v' - | 'w' - | 'x' - | 'y' - | 'z' - | 'A' - | 'B' - | 'C' - | 'D' - | 'E' - | 'F' - | 'G' - | 'H' - | 'I' - | 'J' - | 'K' - | 'L' - | 'M' - | 'N' - | 'O' - | 'P' - | 'Q' - | 'R' - | 'S' - | 'T' - | 'U' - | 'V' - | 'W' - | 'X' - | 'Y' - | 'Z'; -type CommandParams = { [key in singleLetter]?: number }; +/** + * Parameters for G-code commands used in 3D printing. + * + * @remarks + * This interface defines the common parameters used in G-code commands for 3D printing. + * While additional parameters may exist in G-code files, only the parameters listed here + * are actively used in this library. Other parameters are still parsed and preserved + * through the index signature. + * + * @example + * ```typescript + * const params: GCodeParameters = { + * y: 100, // Move to Y position 100 + * z: 0.2, // Set layer height to 0.2 + * f: 1200, // Set feed rate to 1200mm/min + * e: 123.45 // Extrude filament + * }; + * ``` + */ +export interface GCodeParameters { + /** + * X-axis position in millimeters. + * Used for positioning the print head along the X axis. + */ + x?: number; + /** + * Y-axis position in millimeters. + * Used for positioning the print head along the Y axis. + */ + y?: number; + + /** + * Z-axis position in millimeters. + * Typically used for layer height control and vertical positioning. + */ + z?: number; + + /** + * Extruder position/length in millimeters. + * Controls the amount of filament to extrude. + */ + e?: number; + + /** + * Feed rate (speed) in millimeters per minute. + * Determines how fast the print head moves. + */ + f?: number; + + /** + * X offset from current position to arc center in millimeters. + * Used in G2/G3 arc movement commands. + */ + i?: number; + + /** + * Y offset from current position to arc center in millimeters. + * Used in G2/G3 arc movement commands. + */ + j?: number; + + /** + * Radius of arc in millimeters. + * Alternative way to specify arc movement in G2/G3 commands. + */ + r?: number; + + /** + * Tool number for multi-tool setups. + * Used to select between different extruders or tools. + */ + t?: number; + + /** + * Index signature for additional G-code parameters. + * Allows parsing and storing of parameters not explicitly defined above. + */ + [key: string]: number | undefined; +} + +/** + * Represents a parsed G-code command + */ export class GCodeCommand { + /* eslint-disable no-unused-vars */ + /** + * Creates a new GCodeCommand instance + * @param src - The original G-code line + * @param gcode - The parsed G-code command (e.g., 'g0', 'g1') + * @param params - The parsed parameters + * @param comment - Optional comment from the G-code line + */ constructor( public src: string, public gcode: string, - public params: CommandParams, + public params: GCodeParameters, public comment?: string ) {} + /* eslint-enable no-unused-vars */ } -type Metadata = { thumbnails: Record }; +export type ParseResult = { metadata: Metadata; commands: GCodeCommand[] }; +export type Metadata = { thumbnails: Record }; +/** + * A G-code parser that processes G-code commands and extracts metadata. + * + * @remarks + * This parser handles both single-line and multi-line G-code input, extracting + * commands, parameters, and metadata such as thumbnails. It preserves comments + * and maintains the original source lines. + * + * @example + * ```typescript + * const parser = new Parser(); + * const result = parser.parseGCode('G1 X100 Y100 F1000 ; Move to position'); + * ``` + */ export class Parser { + /** Metadata extracted from G-code comments, including thumbnails */ metadata: Metadata = { thumbnails: {} }; + + /** Original G-code lines stored for reference */ lines: string[] = []; - parseGCode(input: string | string[]): { - metadata: Metadata; - commands: GCodeCommand[]; - } { + /** + * Parses G-code input into commands and metadata + * @param input - G-code to parse, either as a string or array of lines + * @returns Object containing parsed metadata and commands + * + * @remarks + * This method handles both single-line and multi-line G-code input, extracting + * commands, parameters, and metadata such as thumbnails. It preserves comments + * and maintains the original source lines. + * + * @example + * ```typescript + * const parser = new Parser(); + * const result = parser.parseGCode('G1 X100 Y100 F1000 ; Move to position'); + * ``` + */ + parseGCode(input: string | string[]): ParseResult { this.lines = Array.isArray(input) ? input : input.split('\n'); const commands = this.lines2commands(this.lines); @@ -87,10 +155,34 @@ export class Parser { return { metadata: this.metadata, commands: commands }; } - private lines2commands(lines: string[]) { + /** + * Converts an array of G-code lines into GCodeCommand objects + * @param lines - Array of G-code lines to convert + * @returns Array of parsed GCodeCommand objects + * @private + */ + private lines2commands(lines: string[]): GCodeCommand[] { return lines.map((l) => this.parseCommand(l)); } + /** + * Parses a single line of G-code into a command object. + * + * @param line - Single line of G-code to parse + * @param keepComments - Whether to preserve comments in the parsed command (default: true) + * @returns Parsed GCodeCommand object or null if line is empty/invalid + * + * @remarks + * This method handles the parsing of individual G-code lines, including: + * - Separating commands from comments + * - Extracting the G-code command (e.g., G0, G1) + * - Parsing parameters + * + * @example + * ```typescript + * const cmd = parser.parseCommand('G1 X100 Y100 F1000 ; Move to position'); + * ``` + */ parseCommand(line: string, keepComments = true): GCodeCommand | null { const input = line.trim(); const splitted = input.split(';'); @@ -107,24 +199,68 @@ export class Parser { return new GCodeCommand(line, gcode, params, comment); } - private isAlpha(char: string | singleLetter): char is singleLetter { + /** + * Checks if a character is an alphabetic letter (A-Z or a-z). + * + * @param char - Single character to check + * @returns True if character is a letter, false otherwise + * @private + */ + private isAlpha(char: string): boolean { const code = char.charCodeAt(0); return (code >= 97 && code <= 122) || (code >= 65 && code <= 90); } - private parseParams(params: string[]): CommandParams { - return params.reduce((acc: CommandParams, cur: string, idx: number, array) => { + /** + * Parses G-code parameters from an array of parameter strings. + * + * @param params - Array of parameter strings (alternating between parameter letters and values) + * @returns Object containing parsed parameters with their values + * @private + * + * @remarks + * Parameters in G-code are letter-value pairs (e.g., X100 Y200). + * This method processes these pairs and converts values to numbers. + * It alternates through the array since parameters come in pairs: + * - Even indices contain parameter letters (X, Y, Z, etc.) + * - Odd indices contain the corresponding values + */ + private parseParams(params: string[]): GCodeParameters { + return params.reduce((acc: GCodeParameters, cur: string, idx: number, array) => { // alternate bc we're processing in pairs if (idx % 2 == 0) return acc; - let key = array[idx - 1]; - key = key.toLowerCase(); - if (this.isAlpha(key)) acc[key] = parseFloat(cur); + const key = array[idx - 1].toLowerCase(); + if (this.isAlpha(key)) { + acc[key] = parseFloat(cur); + } return acc; }, {}); } + /** + * Extracts metadata from G-code commands, particularly focusing on thumbnails. + * + * @param metadata - Array of G-code commands containing metadata in comments + * @returns Object containing extracted metadata (currently only thumbnails) + * + * @remarks + * This method processes special comments in the G-code that contain metadata. + * Currently, it focuses on extracting thumbnail data that some slicers embed + * in the G-code file. The thumbnail data is typically found between + * 'thumbnail begin' and 'thumbnail end' markers in the comments. + * + * The method handles multi-line thumbnail data by accumulating characters + * until it encounters the end marker. Once complete, it validates the + * thumbnail data before storing it in the thumbnails record. + * + * @example + * ```typescript + * const commands = parser.parseGCode(gcode).commands; + * const metadata = parser.parseMetadata(commands.filter(cmd => cmd.comment)); + * ``` + */ parseMetadata(metadata: GCodeCommand[]): Metadata { const thumbnails: Record = {}; diff --git a/src/gcode-preview.ts b/src/gcode-preview.ts index 5500c3d6..ffeec274 100644 --- a/src/gcode-preview.ts +++ b/src/gcode-preview.ts @@ -1,7 +1,19 @@ import { type GCodePreviewOptions, WebGLPreview } from './webgl-preview'; import { type DevModeOptions } from './dev-gui'; +/** + * Initializes a new WebGLPreview instance with the given options + * @param opts - Configuration options for the G-code preview + * @returns A new WebGLPreview instance + */ const init = function (opts: GCodePreviewOptions) { return new WebGLPreview(opts); }; +/** + * Main exports for the G-code preview module + * + * @remarks + * This module provides the core functionality for rendering G-code previews in WebGL. + * It exports the WebGLPreview class, initialization function, and related types. + */ export { WebGLPreview, init, DevModeOptions, GCodePreviewOptions }; diff --git a/src/helpers/grid.ts b/src/helpers/grid.ts index 40f26d34..9d20f5c1 100644 --- a/src/helpers/grid.ts +++ b/src/helpers/grid.ts @@ -1,13 +1,19 @@ import { BufferGeometry, Color, Float32BufferAttribute, LineBasicMaterial, LineSegments } from 'three'; +/** + * A grid helper that creates a 2D grid in the XZ plane using Three.js LineSegments. + * The grid is centered at the origin and can be configured with different sizes and step intervals. + */ class Grid extends LineSegments { - constructor( - sizeX: number, // Size along the X axis - stepX: number, // Step distance along the X axis - sizeZ: number, // Size along the Z axis - stepZ: number, // Step distance along the Z axis - color: Color | string | number = 0x888888 // Single color for all grid lines - ) { + /** + * Creates a new Grid instance + * @param sizeX - Size of the grid along the X axis in world units + * @param stepX - Distance between grid lines along the X axis + * @param sizeZ - Size of the grid along the Z axis in world units + * @param stepZ - Distance between grid lines along the Z axis + * @param color - Color of the grid lines (can be Color, hex number, or CSS color string) + */ + constructor(sizeX: number, stepX: number, sizeZ: number, stepZ: number, color: Color | string | number = 0x888888) { // Convert color input to a Color object color = new Color(color); @@ -71,10 +77,15 @@ class Grid extends LineSegments { super(geometry, material); } - // Override the type property for clarity and identification + /** + * The type of this object, used for identification and debugging + */ override readonly type = 'GridHelper'; - // Add dispose method for resource management + /** + * Disposes of the grid's geometry and material resources + * Call this method when the grid is no longer needed to free up memory + */ dispose() { this.geometry.dispose(); if (Array.isArray(this.material)) { diff --git a/src/helpers/line-box.ts b/src/helpers/line-box.ts index 360e12dd..29e226f1 100644 --- a/src/helpers/line-box.ts +++ b/src/helpers/line-box.ts @@ -1,6 +1,17 @@ import { BufferGeometry, Float32BufferAttribute, Color, LineSegments, LineDashedMaterial } from 'three'; +/** + * A helper class that creates a 3D box outline with dashed lines using Three.js LineSegments. + * The box is centered at the origin and can be configured with different dimensions and colors. + */ class LineBox extends LineSegments { + /** + * Creates a new LineBox instance + * @param x - Width of the box along the X axis + * @param y - Height of the box along the Y axis + * @param z - Depth of the box along the Z axis + * @param color - Color of the box lines (can be Color, hex number, or CSS color string) + */ constructor(x: number, y: number, z: number, color: Color | number | string) { // Create geometry for the box const geometryBox = LineBox.createBoxGeometry(x, y, z); @@ -18,7 +29,13 @@ class LineBox extends LineSegments { this.position.setY(y / 2); } - // Static method to create the box geometry + /** + * Creates the geometry for the box outline + * @param xSize - Width of the box along the X axis + * @param ySize - Height of the box along the Y axis + * @param zSize - Depth of the box along the Z axis + * @returns BufferGeometry containing the box's line segments + */ static createBoxGeometry(xSize: number, ySize: number, zSize: number): BufferGeometry { const x = xSize / 2; const y = ySize / 2; @@ -83,7 +100,10 @@ class LineBox extends LineSegments { return geometry; } - // Dispose method to clean up resources + /** + * Disposes of the box's geometry and material resources + * Call this method when the box is no longer needed to free up memory + */ dispose() { this.geometry.dispose(); if (Array.isArray(this.material)) { diff --git a/src/helpers/three-utils.ts b/src/helpers/three-utils.ts index 225d8749..73db6811 100644 --- a/src/helpers/three-utils.ts +++ b/src/helpers/three-utils.ts @@ -1,3 +1,8 @@ +/** + * Represents an object that can be disposed of to free up resources + * @remarks + * Used for Three.js objects that need cleanup when no longer needed + */ export type Disposable = { dispose(): void; }; diff --git a/src/interpreter.ts b/src/interpreter.ts index 80943929..90f9361c 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -2,9 +2,24 @@ import { Path, PathType } from './path'; import { GCodeCommand } from './gcode-parser'; import { Job } from './job'; +/** + * Interprets and executes G-code commands, updating the job state accordingly + * + * @remarks + * This class handles the execution of G-code commands, translating them into + * movements and state changes in the print job. It supports common G-code commands + * including linear moves (G0/G1), arcs (G2/G3), unit changes (G20/G21), and tool selection. + */ export class Interpreter { // eslint-disable-next-line no-unused-vars [key: string]: (...args: unknown[]) => unknown; + + /** + * Executes an array of G-code commands, updating the provided job + * @param commands - Array of GCodeCommand objects to execute + * @param job - Job instance to update (default: new Job) + * @returns The updated job instance + */ execute(commands: GCodeCommand[], job = new Job()): Job { job.resumeLastPath(); commands.forEach((command) => { @@ -20,6 +35,15 @@ export class Interpreter { return job; } + /** + * Executes a linear move command (G0/G1) + * @param command - GCodeCommand containing move parameters + * @param job - Job instance to update + * @remarks + * Handles both rapid moves (G0) and linear moves (G1). Updates the job state + * and adds points to the current path based on the command parameters. + * 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; @@ -40,6 +64,16 @@ export class Interpreter { g1 = this.g0; + /** + * Executes an arc move command (G2/G3) + * @param command - GCodeCommand containing arc parameters + * @param job - Job instance to update + * @remarks + * Handles both clockwise (G2) and counter-clockwise (G3) arc moves. Supports + * both I/J center offset and R radius modes. Calculates intermediate points + * along the arc and updates the job state accordingly. + * G2 is for clockwise arcs, G3 is for counter-clockwise arcs. + */ g2(command: GCodeCommand, job: Job): void { const { x, y, z, e } = command.params; let { i, j, r } = command.params; @@ -137,45 +171,136 @@ export class Interpreter { g3 = this.g2; + /** + * Executes a G20 command to set units to inches + * @param command - GCodeCommand containing the command + * @param job - Job instance to update + */ g20(command: GCodeCommand, job: Job): void { job.state.units = 'in'; } + /** + * Executes a G21 command to set units to millimeters + * @param command - GCodeCommand containing the command + * @param job - Job instance to update + */ g21(command: GCodeCommand, job: Job): void { job.state.units = 'mm'; } + /** + * Executes a G28 homing command + * @param command - GCodeCommand containing the command + * @param job - Job instance to update + * @remarks + * Moves all axes to their home positions (0,0,0) and updates the job state. + */ g28(command: GCodeCommand, job: Job): void { job.state.x = 0; job.state.y = 0; job.state.z = 0; } + /** + * Selects tool 0 (T0) + * @param command - GCodeCommand containing the command + * @param job - Job instance to update + * @remarks + * Updates the job state to use tool 0. Tools are typically used for + * multi-extruder setups or different print heads. + */ t0(command: GCodeCommand, job: Job): void { job.state.tool = 0; } + /** + * Selects tool 1 (T1) + * @param command - GCodeCommand containing the command + * @param job - Job instance to update + * @remarks + * Updates the job state to use tool 1. Tools are typically used for + * multi-extruder setups or different print heads. + */ t1(command: GCodeCommand, job: Job): void { job.state.tool = 1; } + /** + * Selects tool 2 (T2) + * @param command - GCodeCommand containing the command + * @param job - Job instance to update + * @remarks + * Updates the job state to use tool 2. Tools are typically used for + * multi-extruder setups or different print heads. + */ t2(command: GCodeCommand, job: Job): void { job.state.tool = 2; } + /** + * Selects tool 3 (T3) + * @param command - GCodeCommand containing the command + * @param job - Job instance to update + * @remarks + * Updates the job state to use tool 3. Tools are typically used for + * multi-extruder setups or different print heads. + */ t3(command: GCodeCommand, job: Job): void { job.state.tool = 3; } + /** + * Selects tool 4 (T4) + * @param command - GCodeCommand containing the command + * @param job - Job instance to update + * @remarks + * Updates the job state to use tool 4. Tools are typically used for + * multi-extruder setups or different print heads. + */ t4(command: GCodeCommand, job: Job): void { job.state.tool = 4; } + /** + * Selects tool 5 (T5) + * @param command - GCodeCommand containing the command + * @param job - Job instance to update + * @remarks + * Updates the job state to use tool 5. Tools are typically used for + * multi-extruder setups or different print heads. + */ t5(command: GCodeCommand, job: Job): void { job.state.tool = 5; } + /** + * Selects tool 6 (T6) + * @param command - GCodeCommand containing the command + * @param job - Job instance to update + * @remarks + * Updates the job state to use tool 6. Tools are typically used for + * multi-extruder setups or different print heads. + */ t6(command: GCodeCommand, job: Job): void { job.state.tool = 6; } + /** + * Selects tool 7 (T7) + * @param command - GCodeCommand containing the command + * @param job - Job instance to update + * @remarks + * Updates the job state to use tool 7. Tools are typically used for + * multi-extruder setups or different print heads. + */ t7(command: GCodeCommand, job: Job): void { job.state.tool = 7; } + /** + * Creates a new path and sets it as the current in-progress path + * @param job - Job instance to update + * @param newType - Type of the new path + * @returns The newly created path + * @remarks + * This method is called when a path type change is detected (e.g. switching + * between travel and extrusion moves). It finalizes the current path and + * starts a new one of the specified type. + */ private breakPath(job: Job, newType: PathType): Path { job.finishPath(); const currentPath = new Path(newType, 0.6, 0.2, job.state.tool); diff --git a/src/job.ts b/src/job.ts index c198d807..200382bd 100644 --- a/src/job.ts +++ b/src/job.ts @@ -1,13 +1,28 @@ import { Path, PathType } from './path'; +/** + * Represents the current state of the print job + * @remarks + * Tracks the current position, extrusion state, active tool, and units + */ export class State { + /** Current X position */ x: number; + /** Current Y position */ y: number; + /** Current Z position */ z: number; + /** Current extrusion amount */ e: number; + /** Currently active tool */ tool: number; + /** Current units (millimeters or inches) */ units: 'mm' | 'in'; + /** + * Gets a new State instance with default initial values + * @returns New State instance with x=0, y=0, z=0, e=0, tool=0, units='mm' + */ static get initial(): State { const state = new State(); Object.assign(state, { x: 0, y: 0, z: 0, e: 0, tool: 0, units: 'mm' }); @@ -15,12 +30,31 @@ export class State { } } +/** + * Represents a single layer in the print job + * @remarks + * Contains information about the layer number, paths, height, and Z position + */ export class Layer { + /** Layer number (0-based index) */ public layer: number; + /** Array of paths in this layer */ public paths: Path[]; + /** Line number in the G-code file where this layer starts */ public lineNumber: number; + /** Height of this layer */ public height: number = 0; + /** Z position of this layer */ public z: number = 0; + + /** + * Creates a new Layer instance + * @param layer - Layer number + * @param paths - Array of paths in this layer + * @param lineNumber - Line number in G-code file + * @param height - Layer height (default: 0) + * @param z - Z position (default: 0) + */ constructor(layer: number, paths: Path[], lineNumber: number, height: number = 0, z: number = 0) { this.layer = layer; this.paths = paths; @@ -30,16 +64,36 @@ export class Layer { } } +/** + * Represents a complete print job containing paths, layers, and state + * @remarks + * Manages the collection of paths, organizes them into layers and tools, + * and tracks the current print state + */ export class Job { + /** All paths in the job */ paths: Path[] = []; + /** Current print state */ state: State; + /** Travel paths (non-extrusion moves) */ private travelPaths: Path[] = []; + /** Extrusion paths */ private extrusionPaths: Path[] = []; + /** Layers in the job */ private _layers: Layer[] = []; + /** Paths organized by tool */ private _toolPaths: Path[][] = []; + /** Indexers for organizing paths */ private indexers: Indexer[]; + /** Current in-progress path */ inprogressPath: Path | undefined; + /** + * Creates a new Job instance + * @param opts - Job options + * @param opts.state - Initial state (default: State.initial) + * @param opts.minLayerThreshold - Minimum layer height threshold (default: LayersIndexer.DEFAULT_TOLERANCE) + */ constructor(opts: { state?: State; minLayerThreshold?: number } = {}) { this.state = opts.state || State.initial; this.indexers = [ @@ -49,27 +103,53 @@ export class Job { ]; } + /** + * Gets all extrusion paths in the job + * @returns Array of extrusion paths + */ get extrusions(): Path[] { return this.extrusionPaths; } + /** + * Gets all travel paths in the job + * @returns Array of travel paths + */ get travels(): Path[] { return this.travelPaths; } + /** + * Gets paths organized by tool + * @returns 2D array of paths, where each sub-array contains paths for a specific tool + */ get toolPaths(): Path[][] { return this._toolPaths; } + /** + * Gets all layers in the job + * @returns Array of Layer objects + */ get layers(): Layer[] { return this._layers; } + /** + * Adds a path to the job and indexes it + * @param path - Path to add + */ addPath(path: Path): void { this.paths.push(path); this.indexPath(path); } + /** + * Finalizes the current in-progress path + * @remarks + * If the in-progress path has vertices, it will be added to the job + * and the in-progress path reference will be cleared + */ finishPath(): void { if (this.inprogressPath === undefined) { return; @@ -80,6 +160,11 @@ export class Job { } } + /** + * Resumes the last path from the job as the current in-progress path + * @remarks + * Removes the path from all indexes and sets it as the current in-progress path + */ resumeLastPath(): void { if (this.paths.length === 0) { return; @@ -96,10 +181,22 @@ export class Job { }); } + /** + * Checks if the job contains planar layers + * @returns True if the job contains at least one layer, false otherwise + */ isPlanar(): boolean { return this.layers.length > 0; } + /** + * Indexes a path using all available indexers + * @param path - Path to index + * @remarks + * If an indexer throws a NonApplicableIndexer error, it will be removed + * from the list of indexers. If the error is a NonPlanarPathError, + * the layers will be cleared. + */ private indexPath(path: Path): void { this.indexers.forEach((indexer) => { try { @@ -119,24 +216,58 @@ export class Job { } } +/** + * Base error class for indexer-related errors + */ class NonApplicableIndexer extends Error {} + +/** + * Base class for path indexers + * @remarks + * Indexers organize paths into different structures (layers, tools, etc.) + */ export class Indexer { + /** The indexes being managed by this indexer */ protected indexes: unknown; + + /** + * Creates a new Indexer instance + * @param indexes - The indexes to manage + */ constructor(indexes: unknown) { this.indexes = indexes; } + + /** + * Sorts a path into the appropriate index + * @param path - Path to sort + * @throws Error if not implemented in subclass + */ sortIn(path: Path): void { path; throw new Error('Method not implemented.'); } } +/** + * Indexer that organizes paths by travel type (extrusion vs travel moves) + */ class TravelTypeIndexer extends Indexer { + /** Indexes containing arrays of paths for each travel type */ protected declare indexes: Record; + + /** + * Creates a new TravelTypeIndexer + * @param indexes - Object containing arrays for travel and extrusion paths + */ constructor(indexes: Record) { super(indexes); } + /** + * Sorts a path into either extrusion or travel paths + * @param path - Path to sort + */ sortIn(path: Path): void { if (path.travelType === PathType.Extrusion) { this.indexes.extrusion.push(path); @@ -146,20 +277,43 @@ class TravelTypeIndexer extends Indexer { } } +/** + * Error thrown when attempting to index a non-planar path + */ class NonPlanarPathError extends NonApplicableIndexer { constructor() { super("Non-planar paths can't be indexed by layer"); } } + +/** + * Indexer that organizes paths into layers based on Z height + */ export class LayersIndexer extends Indexer { + /** Default tolerance for layer height differences */ static readonly DEFAULT_TOLERANCE = 0.05; + + /** Array of layers being managed */ protected declare indexes: Layer[]; + + /** Tolerance for layer height differences */ private tolerance: number; + + /** + * Creates a new LayersIndexer + * @param indexes - Array to store layers + * @param tolerance - Height tolerance for layer detection (default: DEFAULT_TOLERANCE) + */ constructor(indexes: Layer[], tolerance: number = LayersIndexer.DEFAULT_TOLERANCE) { super(indexes); this.tolerance = tolerance; } + /** + * Sorts a path into the appropriate layer + * @param path - Path to sort + * @throws NonPlanarPathError if path is non-planar + */ sortIn(path: Path): void { if ( path.travelType === PathType.Extrusion && @@ -183,10 +337,18 @@ export class LayersIndexer extends Indexer { this.lastLayer().paths.push(path); } + /** + * Gets the last layer in the indexes + * @returns The most recent layer + */ private lastLayer(): Layer { return this.indexes[this.indexes.length - 1]; } + /** + * Creates a new layer at the specified Z height + * @param z - Z height for the new layer + */ private createLayer(z: number): void { const layerNumber = this.indexes.length; const height = z - (this.lastLayer()?.z || 0); @@ -194,11 +356,25 @@ export class LayersIndexer extends Indexer { } } +/** + * Indexer that organizes paths by tool number + */ class ToolIndexer extends Indexer { + /** 2D array of paths indexed by tool number */ protected declare indexes: Path[][]; + + /** + * Creates a new ToolIndexer + * @param indexes - 2D array to store paths by tool + */ constructor(indexes: Path[][]) { super(indexes); } + + /** + * Sorts a path into the appropriate tool's path array + * @param path - Path to sort + */ sortIn(path: Path): void { if (path.travelType === PathType.Extrusion) { this.indexes; diff --git a/src/path.ts b/src/path.ts index fc9340b1..b807dee2 100644 --- a/src/path.ts +++ b/src/path.ts @@ -3,18 +3,45 @@ import { BufferGeometry, Vector3 } from 'three'; import { ExtrusionGeometry } from './extrusion-geometry'; import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js'; +/** + * Type of path movement + */ export enum PathType { + /** Travel move (non-extrusion) */ Travel = 'Travel', + /** Extrusion move (material deposition) */ Extrusion = 'Extrusion' } +/** + * Represents a path in 3D space with associated properties + * @remarks + * Used to store and manipulate G-code path data including vertices, + * extrusion parameters, and tool information + */ export class Path { + /** Type of path movement */ public travelType: PathType; + + /** Width of extruded material */ public extrusionWidth: number; + + /** Height of extruded line */ public lineHeight: number; + + /** Tool number used for this path */ public tool: number; + + /** Internal storage for path vertices */ private _vertices: number[]; + /** + * Creates a new Path instance + * @param travelType - Type of path movement + * @param extrusionWidth - Width of extruded material (default: 0.6) + * @param lineHeight - Height of extruded line (default: 0.2) + * @param tool - Tool number (default: 0) + */ constructor(travelType: PathType, extrusionWidth = 0.6, lineHeight = 0.2, tool = 0) { this.travelType = travelType; this._vertices = []; @@ -23,14 +50,31 @@ export class Path { this.tool = tool; } + /** + * Gets the path's vertices as a flat array of numbers + * @returns Array of vertex coordinates in [x,y,z] order + */ get vertices(): number[] { return this._vertices; } + /** + * Adds a new point to the path + * @param x - X coordinate + * @param y - Y coordinate + * @param z - Z coordinate + */ addPoint(x: number, y: number, z: number): void { this._vertices.push(x, y, z); } + /** + * Checks if a point continues the current line + * @param x - X coordinate to check + * @param y - Y coordinate to check + * @param z - Z coordinate to check + * @returns True if the point matches the last point in the path + */ checkLineContinuity(x: number, y: number, z: number): boolean { if (this._vertices.length < 3) { return false; @@ -43,6 +87,10 @@ export class Path { return x === lastX && y === lastY && z === lastZ; } + /** + * Converts the path's vertices to an array of Vector3 points + * @returns Array of Vector3 points + */ path(): Vector3[] { const path: Vector3[] = []; @@ -52,6 +100,13 @@ export class Path { return path; } + /** + * Creates a 3D geometry from the path + * @param opts - Geometry options + * @param opts.extrusionWidthOverride - Optional override for extrusion width + * @param opts.lineHeightOverride - Optional override for line height + * @returns BufferGeometry representing the path + */ geometry(opts: { extrusionWidthOverride?: number; lineHeightOverride?: number } = {}): BufferGeometry { if (this._vertices.length < 3) { return new BufferGeometry(); @@ -65,6 +120,10 @@ export class Path { ); } + /** + * Creates a line geometry from the path + * @returns LineSegmentsGeometry representing the path + */ line(): LineSegmentsGeometry { const lineVertices = []; for (let i = 0; i < this._vertices.length - 3; i += 3) { @@ -75,6 +134,10 @@ export class Path { return new LineSegmentsGeometry().setPositions(lineVertices); } + /** + * Checks if the path contains any vertical moves + * @returns True if any Z coordinates differ from the initial Z + */ hasVerticalMoves(): boolean { return this.vertices.some((_, i, arr) => i % 3 === 2 && arr[i] !== arr[2]); } diff --git a/src/thumbnail.ts b/src/thumbnail.ts index 046ff473..505cea69 100644 --- a/src/thumbnail.ts +++ b/src/thumbnail.ts @@ -1,15 +1,31 @@ const prefix = 'data:image/jpeg;base64,'; +/** + * Represents a thumbnail image extracted from G-code + */ export class Thumbnail { + /** Base64 encoded image characters */ public chars = ''; + /** + * Creates a new Thumbnail instance + * @param size - Dimensions in "WxH" format + * @param width - Image width in pixels + * @param height - Image height in pixels + * @param charLength - Expected length of base64 characters + */ constructor( - public size: string, - public width: number, - public height: number, - public charLength: number + public size: string, // eslint-disable-line no-unused-vars + public width: number, // eslint-disable-line no-unused-vars + public height: number, // eslint-disable-line no-unused-vars + public charLength: number // eslint-disable-line no-unused-vars ) {} + /** + * Parses thumbnail information string into a Thumbnail instance + * @param thumbInfo - Thumbnail info string in format "WxH charLength" + * @returns New Thumbnail instance + */ public static parse(thumbInfo: string): Thumbnail { const infoParts = thumbInfo.split(' '); const size = infoParts[0]; @@ -17,12 +33,20 @@ export class Thumbnail { return new Thumbnail(size, +sizeParts[0], +sizeParts[1], +infoParts[1]); } + /** + * Gets the complete base64 image source string + * @returns Data URL for the thumbnail image + */ get src(): string { return prefix + this.chars; } + /** + * Checks if the thumbnail data is valid + * @returns True if the base64 data matches expected length and format + * @see https://stackoverflow.com/questions/475074/regex-to-parse-or-validate-base64-data/475217#475217 + */ get isValid(): boolean { - // https://stackoverflow.com/questions/475074/regex-to-parse-or-validate-base64-data/475217#475217 const base64regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; return this.chars.length == this.charLength && base64regex.test(this.chars); } diff --git a/src/webgl-preview.ts b/src/webgl-preview.ts index dabfeff6..f0e15855 100644 --- a/src/webgl-preview.ts +++ b/src/webgl-preview.ts @@ -32,88 +32,177 @@ import { WebGLRenderer } from 'three'; +/** + * Options for configuring the G-code preview + */ export type GCodePreviewOptions = { + /** Build volume dimensions */ buildVolume?: BuildVolume; + /** Background color of the preview */ backgroundColor?: ColorRepresentation; + /** Canvas element to render into */ canvas?: HTMLCanvasElement; + /** Last layer to render (1-based index) */ endLayer?: number; + /** Color(s) for extruded paths */ extrusionColor?: ColorRepresentation | ColorRepresentation[]; + /** Initial camera position [x, y, z] */ initialCameraPosition?: number[]; + /** Color for the last segment of each path */ lastSegmentColor?: ColorRepresentation; + /** Width of rendered lines */ lineWidth?: number; + /** Height of extruded lines */ lineHeight?: number; + /** List of G-code commands considered non-travel moves */ nonTravelMoves?: string[]; + /** Minimum layer height threshold */ minLayerThreshold?: number; + /** Whether to render extrusion paths */ renderExtrusion?: boolean; + /** Whether to render travel moves */ renderTravel?: boolean; + /** First layer to render (1-based index) */ startLayer?: number; + /** Color for the top layer */ topLayerColor?: ColorRepresentation; + /** Color for travel moves */ travelColor?: ColorRepresentation; + /** Colors for different tools */ toolColors?: Record; + /** Disable color gradient between layers */ disableGradient?: boolean; + /** Width of extruded material */ extrusionWidth?: number; + /** Render paths as 3D tubes instead of lines */ renderTubes?: boolean; /** * @deprecated Please see the demo how to implement drag and drop. */ allowDragNDrop?: boolean; + /** Enable developer mode with additional controls */ devMode?: boolean | DevModeOptions; }; +/** + * WebGL-based G-code preview renderer + */ export class WebGLPreview { + /** Minimum layer height threshold */ minLayerThreshold: number; + /** Three.js scene */ scene: Scene; + /** Three.js perspective camera */ camera: PerspectiveCamera; - renderer: WebGLRenderer; + /** Three.js WebGL renderer */ + renderer: WebGLRenderer & { + info: { + render: { triangles: number; calls: number; lines: number; points: number }; + memory: { geometries: number; textures: number }; + }; + }; + /** Orbit controls for camera */ controls: OrbitControls; + /** Canvas element being rendered to */ canvas: HTMLCanvasElement; + /** Whether to render extrusion paths */ renderExtrusion = true; + /** Whether to render travel moves */ renderTravel = false; + /** Whether to render paths as 3D tubes */ renderTubes = false; + /** Width of extruded material */ extrusionWidth?: number; + /** Width of rendered lines */ lineWidth?: number; + /** Height of extruded lines */ lineHeight?: number; + /** First layer to render (1-based index) */ _startLayer?: number; + /** Last layer to render (1-based index) */ _endLayer?: number; + /** Whether single layer mode is enabled */ _singleLayerMode = false; - buildVolume?: BuildVolume; + /** Build volume dimensions */ + buildVolume?: BuildVolume & { + x: number; + y: number; + z: number; + }; + /** Initial camera position [x, y, z] */ initialCameraPosition = [-100, 400, 450]; + /** Whether to use inches instead of millimeters */ inches = false; + /** List of G-code commands considered non-travel moves */ nonTravelmoves: string[] = []; + /** Disable color gradient between layers */ disableGradient = false; - private job: Job; - interpreter = new Interpreter(); + /** Job containing parsed G-code data */ + job: Job & { + state: { x: number; y: number; z: number }; + paths: { length: number }; + }; + /** G-code interpreter */ + private interpreter = new Interpreter(); + /** G-code parser */ parser = new Parser(); // rendering + /** Group containing all rendered paths */ private group?: Group; + /** Disposable resources */ private disposables: Disposable[] = []; + /** Default extrusion color */ static readonly defaultExtrusionColor = new Color('hotpink'); + /** Current extrusion color(s) */ private _extrusionColor: Color | Color[] = WebGLPreview.defaultExtrusionColor; + /** Animation frame ID */ private animationFrameId?: number; + /** Current path index for animated rendering */ private renderPathIndex?: number; + /** Clipping plane for minimum layer */ private minPlane = new Plane(new Vector3(0, 1, 0), 0.6); + /** Clipping plane for maximum layer */ private maxPlane = new Plane(new Vector3(0, -1, 0), 0.1); + /** Active clipping planes */ private clippingPlanes: Plane[] = []; + /** Previous start layer before single layer mode */ private prevStartLayer = 0; // colors + /** Background color */ private _backgroundColor = new Color(0xe0e0e0); + /** Travel move color */ private _travelColor = new Color(0x990000); + /** Top layer color */ private _topLayerColor?: Color; + /** Last segment color */ private _lastSegmentColor?: Color; + /** Tool-specific colors */ private _toolColors: Record = {}; // dev mode + /** Developer mode configuration */ private devMode?: boolean | DevModeOptions = false; - private _lastRenderTime = 0; - private _wireframe = false; + /** Last render time in milliseconds */ + _lastRenderTime = 0; + /** Whether to render in wireframe mode */ + _wireframe = false; + /** Performance stats */ private stats?: Stats; + /** Container for stats display */ private statsContainer?: HTMLElement; + /** Developer GUI */ private devGui?: DevGUI; + /** Whether to preserve drawing buffer */ private preserveDrawingBuffer = false; + /** + * Creates a new WebGLPreview instance + * @param opts - Configuration options + * @throws Error if no canvas element is provided + */ constructor(opts: GCodePreviewOptions) { this.minLayerThreshold = opts.minLayerThreshold ?? this.minLayerThreshold; this.job = new Job({ minLayerThreshold: this.minLayerThreshold }); @@ -187,9 +276,18 @@ export class WebGLPreview { this.initStats(); } + /** + * Gets the current extrusion color(s) + * @returns Color or array of colors for extruded paths + */ get extrusionColor(): Color | Color[] { return this._extrusionColor; } + + /** + * Sets the extrusion color(s) + * @param value - Color value(s) as number, string, Color instance, or array of ColorRepresentation + */ set extrusionColor(value: number | string | Color | ColorRepresentation[]) { if (Array.isArray(value)) { this._extrusionColor = []; @@ -202,43 +300,91 @@ export class WebGLPreview { this._extrusionColor = new Color(value); } + /** + * Gets the current background color + * @returns Current background color + */ get backgroundColor(): Color { return this._backgroundColor; } + /** + * Sets the background color + * @param value - Color value as number, string, or Color instance + */ set backgroundColor(value: number | string | Color) { this._backgroundColor = new Color(value); this.scene.background = this._backgroundColor; } + /** + * Gets the current travel move color + * @returns Current travel move color + */ get travelColor(): Color { return this._travelColor; } + + /** + * Sets the travel move color + * @param value - Color value as number, string, or Color instance + */ set travelColor(value: number | string | Color) { this._travelColor = new Color(value); } + /** + * Gets the current top layer color + * @returns Color representation or undefined if not set + */ get topLayerColor(): ColorRepresentation | undefined { return this._topLayerColor; } + + /** + * Sets the top layer color + * @param value - Color value or undefined to clear + */ set topLayerColor(value: ColorRepresentation | undefined) { this._topLayerColor = value !== undefined ? new Color(value) : undefined; } + /** + * Gets the current last segment color + * @returns Color representation or undefined if not set + */ get lastSegmentColor(): ColorRepresentation | undefined { return this._lastSegmentColor; } + + /** + * Sets the last segment color + * @param value - Color value or undefined to clear + */ set lastSegmentColor(value: ColorRepresentation | undefined) { this._lastSegmentColor = value !== undefined ? new Color(value) : undefined; } + /** + * Gets the total number of layers in the job + * @returns Number of layers + */ get countLayers(): number { return this.job.layers.length; } + /** + * Gets the current start layer (1-based index) + * @returns Start layer number + */ get startLayer(): number { return this._startLayer; } + + /** + * Sets the start layer (1-based index) + * @param value - Layer number to start rendering from + */ set startLayer(value: number) { if (this.countLayers > 1 && value > 0) { this._startLayer = value; @@ -253,9 +399,18 @@ export class WebGLPreview { } } + /** + * Gets the current end layer (1-based index) + * @returns End layer number + */ get endLayer(): number { return this._endLayer; } + + /** + * Sets the end layer (1-based index) + * @param value - Layer number to end rendering at + */ set endLayer(value: number) { if (this.countLayers > 1 && value > 0) { this._endLayer = value; @@ -273,9 +428,18 @@ export class WebGLPreview { } } + /** + * Gets whether single layer mode is enabled + * @returns True if single layer mode is active + */ get singleLayerMode(): boolean { return this._singleLayerMode; } + + /** + * Sets single layer mode + * @param value - True to enable single layer mode + */ set singleLayerMode(value: boolean) { this._singleLayerMode = value; if (value) { @@ -286,7 +450,10 @@ export class WebGLPreview { } } - /** @internal */ + /** + * Animation loop that continuously renders the scene + * @internal + */ animate(): void { this.animationFrameId = requestAnimationFrame(() => this.animate()); this.controls.update(); @@ -294,13 +461,23 @@ export class WebGLPreview { this.stats?.update(); } + /** + * Processes G-code and updates the visualization + * @param gcode - G-code string or array of strings to process + */ processGCode(gcode: string | string[]): void { const { commands } = this.parser.parseGCode(gcode); this.interpreter.execute(commands, this.job); this.render(); } - private initScene() { + /** + * Initializes the Three.js scene by clearing existing elements and setting up lights + * @remarks + * Clears all existing scene objects and disposables, then adds build volume visualization + * and lighting if 3D tube rendering is enabled. + */ + private initScene(): void { while (this.scene.children.length > 0) { this.scene.remove(this.scene.children[0]); } @@ -325,6 +502,14 @@ export class WebGLPreview { } } + /** + * Creates a new Three.js group for organizing rendered paths + * @param name - Name for the group + * @returns Configured Three.js group + * @remarks + * Sets up the group's orientation and position based on build volume dimensions. + * If no build volume is defined, uses a default position. + */ private createGroup(name: string): Group { const group = new Group(); group.name = name; @@ -338,6 +523,9 @@ export class WebGLPreview { return group; } + /** + * Renders all visible paths in the scene + */ render(): void { const startRender = performance.now(); this.group = this.createGroup('allLayers'); @@ -350,8 +538,12 @@ export class WebGLPreview { this._lastRenderTime = performance.now() - startRender; } - // create a new render method to use an animation loop to render the layers incrementally - /** @experimental */ + /** + * Renders paths incrementally using an animation loop + * @experimental + * @param pathCount - Number of paths to render per frame + * @returns Promise that resolves when rendering is complete + */ async renderAnimated(pathCount = 1): Promise { this.initScene(); @@ -364,6 +556,11 @@ export class WebGLPreview { } } + /** + * Animation loop that renders paths incrementally + * @param pathCount - Number of paths to render per frame + * @returns Promise that resolves when all paths are rendered + */ private renderFrameLoop(pathCount: number): Promise { return new Promise((resolve) => { const loop = () => { @@ -378,6 +575,13 @@ export class WebGLPreview { }); } + /** + * Renders a frame with the specified number of paths + * @param pathCount - Number of paths to render in this frame + * @remarks + * Creates a new group for the frame and renders paths up to the specified count. + * Updates the renderPathIndex to track progress through the job's paths. + */ private renderFrame(pathCount: number): void { this.group = this.createGroup('parts' + this.renderPathIndex); const endPathNumber = Math.min(this.renderPathIndex + pathCount, this.job.paths.length - 1); @@ -394,6 +598,12 @@ export class WebGLPreview { } // reset processing state + /** + * Resets the preview state to default values + * @remarks + * Resets layer ranges, single layer mode, and developer GUI state. + * Called when clearing the preview or starting a new job. + */ private resetState(): void { this.startLayer = 1; this.endLayer = Infinity; @@ -418,12 +628,24 @@ export class WebGLPreview { this.cancelAnimation(); } + /** + * Cancels the current animation frame request + * @remarks + * Stops the animation loop and clears the animation frame ID. + * Called during cleanup to prevent memory leaks. + */ private cancelAnimation(): void { if (this.animationFrameId !== undefined) cancelAnimationFrame(this.animationFrameId); this.animationFrameId = undefined; } - private _enableDropHandler() { + /** + * Enables drag and drop handling for G-code files + * @remarks + * Adds event listeners for drag and drop operations on the canvas. + * @deprecated This feature is deprecated + */ + private _enableDropHandler(): void { console.warn('Drag and drop is deprecated as a library feature. See the demo how to implement your own.'); this.canvas.addEventListener('dragover', (evt) => { evt.stopPropagation(); @@ -453,6 +675,10 @@ export class WebGLPreview { }); } + /** + * Renders paths between the current render index and specified end index + * @param endPathNumber - End index of paths to render (default: Infinity) + */ private renderPaths(endPathNumber: number = Infinity): void { if (this.renderTravel) { this.renderPathsAsLines(this.job.travels.slice(this.renderPathIndex, endPathNumber), this._travelColor); @@ -470,6 +696,11 @@ export class WebGLPreview { } } + /** + * Renders paths as 2D lines + * @param paths - Array of paths to render + * @param color - Color to use for the lines + */ private renderPathsAsLines(paths: Path[], color: Color): void { const material = new LineMaterial({ color: Number(color.getHex()), @@ -500,6 +731,11 @@ export class WebGLPreview { this.group?.add(line); } + /** + * Renders paths as 3D tubes + * @param paths - Array of paths to render + * @param color - Color to use for the tubes + */ private renderPathsAsTubes(paths: Path[], color: Color): void { const colorNumber = Number(color.getHex()); const geometries: BufferGeometry[] = []; @@ -526,6 +762,12 @@ export class WebGLPreview { this.group?.add(batchedMesh); } + /** + * Creates a batched mesh from multiple geometries sharing the same material + * @param geometries - Array of geometries to batch + * @param material - Material to use for the batched mesh + * @returns Batched mesh instance + */ private createBatchMesh(geometries: BufferGeometry[], material: Material): BatchedMesh { const maxVertexCount = geometries.reduce((acc, geometry) => geometry.attributes.position.count * 3 + acc, 0); @@ -540,7 +782,12 @@ export class WebGLPreview { return batchedMesh; } - /** @experimental */ + /** + * Reads and processes G-code from a stream + * @experimental + * @param stream - Readable stream containing G-code data + * @returns Promise that resolves when stream processing is complete + */ async _readFromStream(stream: ReadableStream): Promise { const reader = stream.getReader(); let result; @@ -566,6 +813,9 @@ export class WebGLPreview { this.render(); } + /** + * Initializes the developer GUI if dev mode is enabled + */ private initGui() { if (typeof this.devMode === 'boolean' && this.devMode === true) { this.devGui = new DevGUI(this); @@ -574,6 +824,9 @@ export class WebGLPreview { } } + /** + * Initializes performance statistics display if enabled + */ private initStats() { if (this.stats) { if (typeof this.devMode === 'object') { diff --git a/typedoc.css b/typedoc.css index 82ed2f7d..1bdd5734 100644 --- a/typedoc.css +++ b/typedoc.css @@ -14,3 +14,18 @@ a.deprecated, a.deprecated span { color: grey; } + +.tsd-index-content { + background: #43474f; + padding: 10px; + display: none; +} + +.tsd-sources, +.tsd-sources a { + color: grey; +} + +body { + font-family: Hevetica, Arial, sans-serif; +} diff --git a/typedoc.json b/typedoc.json index abf42e9c..4e112cd4 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,10 +1,14 @@ { "$schema": "https://typedoc.org/schema.json", "entryPoints": [ - "src/gcode-preview.ts" + "src/*.ts", "src/helpers/*.ts" ], "out": "demo/docs", "includeVersion": true, "excludeInternal": true, + "excludeExternals": true, "customCss": "typedoc.css", + "navigationLinks": { + "Demo": "/" + } } \ No newline at end of file