neuralcad / docs /wiki /gcode-preview.md
CallMeDaniel's picture
docs: add CNC/CAM knowledge wiki and slicer/preview design spec
265686c

G-code 3D Preview

Overview

G-code preview renders CNC toolpaths as 3D lines/tubes in a WebGL viewport. The toolpath visualization shows rapid moves, cutting moves, and arcs with distinct colors, allowing the operator to verify the machining strategy before running on a real machine.

Libraries Comparison

gcode-preview (npm)

Full-featured Three.js-based G-code renderer:

  • npm install gcode-preview
  • MIT license, active development (v2.18+)
  • Supports G0/G1/G2/G3, tool changes (T0-T7)
  • Tube rendering via BatchedMesh (Three.js r159+)
  • Layer controls (startLayer/endLayer), animation
  • Progressive loading for large files
const preview = new GCodePreview.WebGLPreview({
  canvas: document.querySelector('canvas'),
  extrusionColor: ['#0088FF', '#00CC66', '#FF4444'],
  travelColor: '#FF000040',
  renderTubes: true,
  buildVolume: { x: 500, y: 500, z: 200 },
});
preview.processGCode(gcodeString);

Limitation: Designed for 3D printing. No spindle/coolant M-code handling, no per-speed coloring built in.

Three.js GCodeLoader (built-in)

Minimal loader included with Three.js:

  • Located at three/examples/jsm/loaders/GCodeLoader.js
  • Supports G0/G1 only (no G2/G3 arcs)
  • Returns THREE.Group with green (cutting) and red (rapid) LineSegments
  • Colors hardcoded, no tool changes, no animation
  • Simple to integrate into existing Three.js scene

gcode-toolpath (npm, by cncjs)

Parser library, not a renderer:

  • Parses full G-code grammar including G2/G3 arcs
  • Tracks modal state (motion, WCS, plane, units, feed, spindle, tool)
  • Callbacks: addLine(modal, v1, v2), addArcCurve(modal, v1, v2, v0)
  • Pair with custom Three.js rendering for CNC-specific visualization

Custom Implementation (NeuralCAD Approach)

For CNC-specific needs, a custom parser + Three.js renderer provides the most control:

G-code Parser

function parseGCode(gcodeString) {
  const segments = [];
  let pos = { x: 0, y: 0, z: 0 };
  let mode = 'G0';
  let feed = 0;
  let tool = 0;

  for (const line of gcodeString.split('\n')) {
    const cleaned = line.split(';')[0].split('(')[0].trim();
    if (!cleaned) continue;

    // Extract commands
    const g = cleaned.match(/G(\d+)/)?.[1];
    const x = cleaned.match(/X([-\d.]+)/)?.[1];
    const y = cleaned.match(/Y([-\d.]+)/)?.[1];
    const z = cleaned.match(/Z([-\d.]+)/)?.[1];
    const f = cleaned.match(/F([-\d.]+)/)?.[1];
    const t = cleaned.match(/T(\d+)/)?.[1];
    const i = cleaned.match(/I([-\d.]+)/)?.[1];
    const j = cleaned.match(/J([-\d.]+)/)?.[1];

    if (g) mode = 'G' + g;
    if (f) feed = parseFloat(f);
    if (t) tool = parseInt(t);

    const newPos = {
      x: x ? parseFloat(x) : pos.x,
      y: y ? parseFloat(y) : pos.y,
      z: z ? parseFloat(z) : pos.z,
    };

    if (mode === 'G0' || mode === 'G1') {
      if (x || y || z) {
        segments.push({
          type: mode, start: {...pos}, end: {...newPos},
          feed, tool
        });
      }
    } else if (mode === 'G2' || mode === 'G3') {
      segments.push({
        type: mode, start: {...pos}, end: {...newPos},
        center: { x: pos.x + (i ? parseFloat(i) : 0),
                  y: pos.y + (j ? parseFloat(j) : 0) },
        feed, tool
      });
    }
    pos = newPos;
  }
  return segments;
}

Three.js Renderer

function renderToolpath(segments, scene) {
  const rapids = [], cuts = [];

  for (const seg of segments) {
    const arr = seg.type === 'G0' ? rapids : cuts;
    if (seg.type === 'G2' || seg.type === 'G3') {
      // Tessellate arc to line segments
      const points = tessellateArc(seg);
      for (let i = 0; i < points.length - 1; i++) {
        cuts.push(points[i].x, points[i].y, points[i].z,
                  points[i+1].x, points[i+1].y, points[i+1].z);
      }
    } else {
      arr.push(seg.start.x, seg.start.y, seg.start.z,
               seg.end.x, seg.end.y, seg.end.z);
    }
  }

  // Rapid moves -- red, semi-transparent
  const rapidGeo = new THREE.BufferGeometry();
  rapidGeo.setAttribute('position',
    new THREE.Float32BufferAttribute(rapids, 3));
  scene.add(new THREE.LineSegments(rapidGeo,
    new THREE.LineBasicMaterial({ color: 0xFF0000, opacity: 0.3,
                                  transparent: true })));

  // Cutting moves -- blue
  const cutGeo = new THREE.BufferGeometry();
  cutGeo.setAttribute('position',
    new THREE.Float32BufferAttribute(cuts, 3));
  scene.add(new THREE.LineSegments(cutGeo,
    new THREE.LineBasicMaterial({ color: 0x0088FF })));
}

View Toggle

const stlGroup = new THREE.Group();
const gcodeGroup = new THREE.Group();
scene.add(stlGroup);
scene.add(gcodeGroup);

function showPartView()     { stlGroup.visible = true;  gcodeGroup.visible = false; }
function showToolpathView() { stlGroup.visible = false; gcodeGroup.visible = true;  }
function showOverlay() {
  stlGroup.visible = true;
  gcodeGroup.visible = true;
  stlGroup.traverse(child => {
    if (child.material) {
      child.material.transparent = true;
      child.material.opacity = 0.3;
    }
  });
}

Material Removal Simulation

Heightmap/Dexel (Best for 3-axis)

Workpiece represented as a 2D grid of height values. Tool carving lowers heights along path. O(n^2) memory, GPU-friendly, cannot represent undercuts.

Voxel Grid (Multi-axis)

3D occupancy grid. Tool clears voxels along path. O(n^3) memory, handles undercuts. Marching cubes for mesh extraction.

CSG (three-bvh-csg)

Boolean subtraction of tool shape from stock mesh. 100x faster than BSP-based alternatives. Expensive per operation -- best for final result view, not animation.

For NeuralCAD's 3-axis scope, the heightmap approach is optimal if material removal simulation is needed in the future.