diff --git a/example/index.html b/example/index.html index 65bb47e..2ab1db9 100644 --- a/example/index.html +++ b/example/index.html @@ -6,12 +6,56 @@ - +
+ + \ No newline at end of file diff --git a/src/LinePoint.ts b/src/LinePoint.ts index e8ce89c..64c64a2 100644 --- a/src/LinePoint.ts +++ b/src/LinePoint.ts @@ -4,10 +4,12 @@ export class LinePoint { public readonly point: Vector3 public readonly radius: number public readonly color: Color + public readonly alpha: number - constructor(point: Vector3, radius: number, color: Color = new Color("#29BEB0")) { + constructor(point: Vector3, radius: number, color: Color = new Color("#29BEB0"), alpha: number = 1.0) { this.point = point this.radius = radius this.color = color + this.alpha = alpha } } \ No newline at end of file diff --git a/src/LineTubeGeometry.ts b/src/LineTubeGeometry.ts index 57d60d8..1512a58 100644 --- a/src/LineTubeGeometry.ts +++ b/src/LineTubeGeometry.ts @@ -14,6 +14,7 @@ interface PointData { vertices: number[] normals: number[] colors: number[] + alpha: number } /** @@ -38,8 +39,9 @@ export class LineTubeGeometry extends BufferGeometry { private vertices: number[] = [] private normals: number[] = [] private colors: number[] = [] - private uvs: number[] = []; - private indices: number[] = []; + private alphas: number[] = [] + private uvs: number[] = [] + private indices: number[] = [] private segmentsRadialNumbers: number[] = [] @@ -56,6 +58,7 @@ export class LineTubeGeometry extends BufferGeometry { this.normals = [] this.vertices = [] this.colors = [] + this.alphas = [] this.uvs = [] this.indices = [] this.segmentsRadialNumbers = [] @@ -83,10 +86,11 @@ export class LineTubeGeometry extends BufferGeometry { this.generateSegment(1); } + console.log(this.alphas.filter(a => a === 0).length, this.alphas.filter(a => a === 1).length) this.setAttribute('position', new Float32BufferAttribute(this.vertices, 3)); this.setAttribute('normal', new Float32BufferAttribute(this.normals, 3)); this.setAttribute('color', new Float32BufferAttribute(this.colors, 3)); - + this.setAttribute('alpha', new Float32BufferAttribute(this.alphas, 1)); this.generateUVs(); this.setAttribute('uv', new Float32BufferAttribute(this.uvs, 2)); @@ -100,6 +104,7 @@ export class LineTubeGeometry extends BufferGeometry { // these are now in the attribute buffers - can be deleted this.normals = [] this.colors = [] + this.alphas = [] this.uvs = [] // The vertices are needed to slice. For now they need to be kept. @@ -162,7 +167,7 @@ export class LineTubeGeometry extends BufferGeometry { const lastRadius = this.pointsBuffer[i-1]?.radius || 0 - function createPointData(pointNr: number, radialNr: number, normal: Vector3, point: Vector3, radius: number, color: Color): PointData { + function createPointData(pointNr: number, radialNr: number, normal: Vector3, point: Vector3, radius: number, color: Color, alpha: number): PointData { return { pointNr, radialNr, @@ -172,7 +177,8 @@ export class LineTubeGeometry extends BufferGeometry { point.y + radius * normal.y, point.z + radius * normal.z ], - colors: color.toArray() + colors: color.toArray(), + alpha, } } @@ -200,27 +206,28 @@ export class LineTubeGeometry extends BufferGeometry { // When the previous point doesn't exist, create one with the radius 0 (lastRadius is set to 0 in this case), // to create a closed starting point. if (prevPoint === undefined) { - segmentsPoints[0].push(createPointData(i, j, normal, point.point, lastRadius, point.color)) + segmentsPoints[0].push(createPointData(i, j, normal, point.point, lastRadius, point.color, point.alpha)) } // Then insert the current point with the current radius - segmentsPoints[1].push(createPointData(i, j, normal, point.point, point.radius, point.color)) + segmentsPoints[1].push(createPointData(i, j, normal, point.point, point.radius, point.color, point.alpha)) // And also the next point with the current radius to finish the current line. - segmentsPoints[2].push(createPointData(i, j, normal, nextPoint.point, point.radius, point.color)) + segmentsPoints[2].push(createPointData(i, j, normal, nextPoint.point, point.radius, point.color, point.alpha)) // if the next point is the last one, also finish the line by inserting one with zero radius. if (nextNextPoint === undefined) { - segmentsPoints[3].push(createPointData(i+1, j, normal, nextPoint.point, 0, point.color)) + segmentsPoints[3].push(createPointData(i+1, j, normal, nextPoint.point, 0, point.color, point.alpha)) } } // Save everything into the buffers. segmentsPoints.forEach((p) => { p.forEach((pp) => { - pp.normals && this.normals.push(...pp.normals); - pp.vertices && this.vertices.push(...pp.vertices); - pp.colors && this.colors.push(...pp.colors); + this.normals.push(...pp.normals); + this.vertices.push(...pp.vertices); + this.colors.push(...pp.colors); + this.alphas.push(pp.alpha); }); this.segmentsRadialNumbers.push(...p.map((cur) => cur.radialNr)) }) diff --git a/src/SegmentColorizer.ts b/src/SegmentColorizer.ts index f222331..c43c6f7 100644 --- a/src/SegmentColorizer.ts +++ b/src/SegmentColorizer.ts @@ -8,17 +8,52 @@ export interface SegmentMetadata { temp: number speed: number - // TODO: Linetype based on comments in gcode + gCodeLine: number } +const DEFAULT_COLOR = new Color("#29BEB0") + export interface SegmentColorizer { getColor(meta: SegmentMetadata): Color } + +export interface LineColorizerOptions { + defaultColor: Color +} + +export type LineColorConfig = {toLine: number, color: Color}[] + +export class LineColorizer { + // This assumes that getColor is called ordered by gCodeLine. + private currentConfigIndex: number = 0 + + constructor( + private readonly lineColorConfig: LineColorConfig, + private readonly options?: LineColorizerOptions + ) {} + + getColor(meta: SegmentMetadata): Color { + // Safeguard check if the config is too short. + if (this.lineColorConfig[this.currentConfigIndex] === undefined) { + return this.options?.defaultColor || DEFAULT_COLOR + } + + if (this.lineColorConfig[this.currentConfigIndex].toLine < meta.gCodeLine) { + this.currentConfigIndex++ + } + + if (meta.gCodeLine < 50) + console.log(meta.gCodeLine) + + return this.lineColorConfig[this.currentConfigIndex].color || this.options?.defaultColor || DEFAULT_COLOR + } +} + export class SimpleColorizer implements SegmentColorizer { private readonly color - constructor(color = new Color("#29BEB0")) { + constructor(color = DEFAULT_COLOR) { this.color = color } diff --git a/src/gcode.ts b/src/gcode.ts index 5674983..8ceba34 100644 --- a/src/gcode.ts +++ b/src/gcode.ts @@ -9,11 +9,43 @@ import { AmbientLight, SpotLight, MeshPhongMaterial, + RawShaderMaterial, + ShaderMaterial, + TangentSpaceNormalMap, + Vector2, + MultiplyOperation, + UniformsUtils, + ShaderLib, + Blending, + NormalBlending, } from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' +import { lineMaterialFragmentShader } from './meshphong_frag.glsl' +import { lineMaterialVertexShader } from './meshphong_vert.glsl' import { GCodeParser } from './parser' import { SegmentColorizer } from './SegmentColorizer' +class LineMaterial extends ShaderMaterial { + constructor() { + super({ + name: 'line-material', + vertexShader: lineMaterialVertexShader, + fragmentShader: lineMaterialFragmentShader, + lights: true, + vertexColors: true, + blending: NormalBlending, + }) + + this.setValues({ + uniforms: UniformsUtils.merge([ + ShaderLib.phong.uniforms, + { diffuse: { value: new Color('#ffffff') } }, + { time: { value: 0.0 } } + ]) + }) + } +} + /** * GCode renderer which parses a GCode file and displays it using * three.js. Use .element() to retrieve the DOM canvas element. @@ -25,7 +57,7 @@ export class GCodeRenderer { private camera: PerspectiveCamera - private lineMaterial = new MeshPhongMaterial({ vertexColors: true } ) + private lineMaterial = new LineMaterial() private readonly parser: GCodeParser @@ -153,7 +185,7 @@ export class GCodeRenderer { */ constructor(gCode: string, width: number, height: number, background: Color) { this.scene = new Scene() - this.renderer = new WebGLRenderer() + this.renderer = new WebGLRenderer({alpha: true}) this.renderer.setSize(width, height) this.renderer.setClearColor(background, 1) this.camera = this.newCamera(width, height) @@ -228,6 +260,7 @@ export class GCodeRenderer { return } + this.renderer.clear() this.renderer.render(this.scene, this.camera) } diff --git a/src/meshphong_frag.glsl.ts b/src/meshphong_frag.glsl.ts new file mode 100644 index 0000000..31fbd85 --- /dev/null +++ b/src/meshphong_frag.glsl.ts @@ -0,0 +1,81 @@ +export const lineMaterialFragmentShader = /* glsl */` +#define LINE + +uniform vec3 diffuse; +uniform vec3 emissive; +uniform vec3 specular; +uniform float shininess; +uniform float opacity; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +varying float alpha; + +void main() { + // if (alpha == 0.0) + // { + // discard; + // return; + // } + + #include + + vec4 diffuseColor = vec4( diffuse, opacity ); + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + + #include + #include + #include + #include + #include + #include + #include + #include + #include + + // accumulation + #include + #include + #include + #include + + // modulation + #include + + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; + + #include + + gl_FragColor = vec4( outgoingLight, diffuseColor.a ); + + #include + #include + #include + #include + #include +} +`; diff --git a/src/meshphong_vert.glsl.ts b/src/meshphong_vert.glsl.ts new file mode 100644 index 0000000..84f71dc --- /dev/null +++ b/src/meshphong_vert.glsl.ts @@ -0,0 +1,60 @@ +export const lineMaterialVertexShader = /* glsl */` +#define PHONG + +varying vec3 vViewPosition; + +#ifndef FLAT_SHADED + + varying vec3 vNormal; + +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +varying float alpha; + +void main() { + #include + #include + #include + + #include + #include + #include + #include + #include + +#ifndef FLAT_SHADED // Normal computed with derivatives when FLAT_SHADED + + vNormal = normalize( transformedNormal ); + +#endif + + #include + #include + #include + #include + #include + #include + #include + + vViewPosition = - mvPosition.xyz; + + #include + #include + #include + #include + +} +`; diff --git a/src/parser.ts b/src/parser.ts index bb87326..5cc2392 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,16 +1,13 @@ import { - Vector3 + Vector3, + LineCurve3, } from 'three' import { LineTubeGeometry } from "./LineTubeGeometry"; import { LinePoint } from "./LinePoint"; import { SegmentColorizer, SimpleColorizer } from './SegmentColorizer'; -function getLength(lastPoint: Vector3, newPoint: Vector3) { - const distant = (lastPoint.x - newPoint.x) ** 2 + (lastPoint.y - newPoint.y) ** 2 + (lastPoint.z - newPoint.z) ** 2; - return distant ** 0.5; -} /** - * GCode renderer which parses a GCode file and displays it using + * GCode renderer which parses a GCode file and displays it using * three.js. Use .element() to retrieve the DOM canvas element. */ export class GCodeParser { @@ -28,11 +25,11 @@ export class GCodeParser { private layerIndex: {start: number, end: number}[] = [] - // Public configurations: - + // Public configurations: + /** - * Width of travel-lines. Use 0 to hide them. - * + * Width of travel-lines. Use 0 to hide them. + * * @type number */ public travelWidth: number = 0.01 @@ -40,16 +37,16 @@ export class GCodeParser { /** * Set any colorizer implementation to change the segment color based on the segment * metadata. Some default implementations are provided. - * + * * @type SegmentColorizer */ public colorizer: SegmentColorizer = new SimpleColorizer() /** * The number of radial segments per line. - * Less (e.g. 3) provides faster rendering with less memory usage. + * Less (e.g. 3) provides faster rendering with less memory usage. * More (e.g. 8) provides a better look. - * + * * @default 8 * @type number */ @@ -60,7 +57,7 @@ export class GCodeParser { * memory consumption while rendering. * You can set the number of points per object. * In most cases you can leave this at the default. - * + * * @default 120000 * @type number */ @@ -68,13 +65,13 @@ export class GCodeParser { /** * Creates a new GCode renderer for the given gcode. - * It initializes the canvas to the given size and + * It initializes the canvas to the given size and * uses the passed color as background. - * - * @param {string} gCode - * @param {number} width - * @param {number} height - * @param {Color} background + * + * @param {string} gCode + * @param {number} width + * @param {number} height + * @param {Color} background */ constructor(gCode: string) { this.gCode = gCode @@ -87,9 +84,9 @@ export class GCodeParser { * This can be used to retrieve some min / max values which may * be needed as param for a colorizer. * @returns {{ - * minTemp: number | undefined, - * maxTemp: number, - * minSpeed: number | undefined, + * minTemp: number | undefined, + * maxTemp: number, + * minSpeed: number | undefined, * maxSpeed: number * }} */ @@ -104,7 +101,7 @@ export class GCodeParser { /** * Recalculate the bounding box with the new point. - * @param {Vector3} newPoint + * @param {Vector3} newPoint */ private calcMinMax(newPoint: Vector3) { if (this.min === undefined) { @@ -224,7 +221,7 @@ export class GCodeParser { let lines: (string | undefined)[] = this.gCode.split("\n") this.gCode = "" // clear memory - + let currentObject = 0 let lastAddedLinePoint: LinePoint | undefined = undefined let pointCount = 0 @@ -249,7 +246,7 @@ export class GCodeParser { // Create the geometry. //this.combinedLines[oNr] = new LineTubeGeometry(this.radialSegments) - lines.forEach((line, i)=> { + lines.forEach((line, lineNumber)=> { if (line === undefined) { return } @@ -268,7 +265,8 @@ export class GCodeParser { const newPoint = new Vector3(x, y, z) - const length = getLength(lastPoint, newPoint) + const curve = new LineCurve3(lastPoint, newPoint) + const length = curve.getLength() if (length !== 0) { let radius = (e - lastE) / length * 10 @@ -281,19 +279,24 @@ export class GCodeParser { } // Get the color for this line. - const color = this.colorizer.getColor({ + const meta = { radius, segmentStart: lastPoint, segmentEnd: newPoint, speed: f, - temp: hotendTemp - }); - + temp: hotendTemp, + gCodeLine: lineNumber, + } + const color = this.colorizer.getColor(meta); + + // just for testing... + const alpha = color.r === 0 && color.g === 0 && color.b === 0 ? 0 : 1 + // Insert the last point with the current radius. - // As the GCode contains the extrusion for the 'current' line, + // As the GCode contains the extrusion for the 'current' line, // but the LinePoint contains the radius for the 'next' line // we need to combine the last point with the current radius. - addLine(new LinePoint(lastPoint.clone(), radius, color)) + addLine(new LinePoint(lastPoint.clone(), radius, color, alpha)) // Try to figure out the layer start and end points. if (lastPoint.z !== newPoint.z) { @@ -332,7 +335,6 @@ export class GCodeParser { } else if (cmd[0] === "G92") { // set state lastLastPoint.copy(lastPoint) - // TODO fix: 'parseValue' value may be zero which is also not truthy lastPoint = new Vector3( this.parseValue(cmd.find((v) => v[0] === "X")) || lastPoint.x, this.parseValue(cmd.find((v) => v[0] === "Y")) || lastPoint.y, @@ -347,14 +349,14 @@ export class GCodeParser { hotendTemp = this.parseValue(cmd.find((v) => v[0] === "S")) || 0 } - lines[i] = undefined + lines[lineNumber] = undefined }) // Finish last object if (this.combinedLines[currentObject]) { this.combinedLines[currentObject].finish() } - + // Sort the layers by starting line number. this.layerIndex = Array.from(layerPointsCache.values()).sort((v1, v2) => v1.start - v2.start) @@ -365,9 +367,9 @@ export class GCodeParser { /** * Slices the rendered model based on the passed start and end point numbers. * (0, pointsCount()) renders everything - * + * * Note: Currently negative values are not allowed. - * + * * @param {number} start the starting segment * @param {number} end the ending segment (excluding) */ @@ -396,8 +398,8 @@ export class GCodeParser { if (objectStart > 0) { from++ } - } - + } + if (i == objectEnd) { to = end - i * this.pointsPerObject // Only if it is not the last object, add the last point to the calculation. @@ -405,7 +407,7 @@ export class GCodeParser { to++ } } - + if (i < objectStart || i > objectEnd) { from = 0 to = 0 @@ -418,9 +420,9 @@ export class GCodeParser { /** * Slices the rendered model based on the passed start and end line numbers. * (0, layerCount()) renders everything - * + * * Note: Currently negative values are not allowed. - * + * * @param {number} start the starting layer * @param {number} end the ending layer (excluding) */ @@ -438,7 +440,7 @@ export class GCodeParser { /** * Get the amount of points in the model. - * + * * @returns {number} */ public pointsCount(): number { @@ -457,7 +459,7 @@ export class GCodeParser { * Get the amount of layers in the model. * This is an approximation which may be incorrect if the * nozzle moves downwards mid print. - * + * * @returns {number} */ public layerCount(): number { @@ -473,4 +475,4 @@ export class GCodeParser { public getGeometries() { return this.combinedLines } -} +} \ No newline at end of file