Skip to main content

05 - G-code Simulator

Visualize CNC toolpath simulation (30 min)


G-code Viewer Component

Create src/app/(core)/cad-cam-demo/components/GCodeViewer.tsx:

"use client";

import { useState, useEffect, useRef } from "react";
import { DesignParams, GCodeLine } from "../utils/types";
import { generateTankGCode } from "../utils/design-calculator";

interface Props {
params: DesignParams | null;
}

export function GCodeViewer({ params }: Props) {
const [gcode, setGcode] = useState<string>("");
const [currentLine, setCurrentLine] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const [toolPosition, setToolPosition] = useState({ x: 0, y: 0, z: 5 });
const [toolPath, setToolPath] = useState<{ x: number; y: number }[]>([]);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);

// Generate G-code when params change
useEffect(() => {
if (params) {
const code = generateTankGCode(params);
setGcode(code);
setCurrentLine(0);
setToolPath([]);
setToolPosition({ x: 0, y: 0, z: 5 });
}
}, [params]);

// Parse G-code line
const parseGCodeLine = (line: string): Partial<GCodeLine> => {
const result: Partial<GCodeLine> = { command: line.split(" ")[0] };

const xMatch = line.match(/X([-\d.]+)/);
const yMatch = line.match(/Y([-\d.]+)/);
const zMatch = line.match(/Z([-\d.]+)/);
const fMatch = line.match(/F([-\d.]+)/);

if (xMatch) result.x = parseFloat(xMatch[1]);
if (yMatch) result.y = parseFloat(yMatch[1]);
if (zMatch) result.z = parseFloat(zMatch[1]);
if (fMatch) result.f = parseFloat(fMatch[1]);

return result;
};

// Animation loop
useEffect(() => {
if (isPlaying && gcode) {
const lines = gcode.split("\n").filter((l) => l.trim() && !l.startsWith(";"));

intervalRef.current = setInterval(() => {
setCurrentLine((prev) => {
if (prev >= lines.length - 1) {
setIsPlaying(false);
return prev;
}

const line = lines[prev + 1];
const parsed = parseGCodeLine(line);

setToolPosition((pos) => ({
x: parsed.x ?? pos.x,
y: parsed.y ?? pos.y,
z: parsed.z ?? pos.z,
}));

// Add to path if cutting (Z < 0)
if ((parsed.z ?? toolPosition.z) < 0 && (parsed.x !== undefined || parsed.y !== undefined)) {
setToolPath((path) => [...path, { x: parsed.x ?? toolPosition.x, y: parsed.y ?? toolPosition.y }]);
}

return prev + 1;
});
}, 200);
} else {
if (intervalRef.current) clearInterval(intervalRef.current);
}

return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [isPlaying, gcode]);

// Draw toolpath on canvas
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;

const ctx = canvas.getContext("2d");
if (!ctx) return;

// Clear
ctx.fillStyle = "#0f172a";
ctx.fillRect(0, 0, canvas.width, canvas.height);

// Draw grid
ctx.strokeStyle = "#334155";
ctx.lineWidth = 0.5;
for (let i = 0; i <= canvas.width; i += 20) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, canvas.height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(canvas.width, i);
ctx.stroke();
}

// Scale and offset
const scale = 0.4;
const offsetX = canvas.width / 2;
const offsetY = canvas.height / 2;

// Draw toolpath
if (toolPath.length > 1) {
ctx.strokeStyle = "#22c55e";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(toolPath[0].x * scale + offsetX, -toolPath[0].y * scale + offsetY);
for (const point of toolPath.slice(1)) {
ctx.lineTo(point.x * scale + offsetX, -point.y * scale + offsetY);
}
ctx.stroke();
}

// Draw tool position
const toolX = toolPosition.x * scale + offsetX;
const toolY = -toolPosition.y * scale + offsetY;

// Tool shadow (when cutting)
if (toolPosition.z < 0) {
ctx.fillStyle = "#ef444440";
ctx.beginPath();
ctx.arc(toolX, toolY, 8, 0, Math.PI * 2);
ctx.fill();
}

// Tool
ctx.fillStyle = toolPosition.z < 0 ? "#ef4444" : "#3b82f6";
ctx.beginPath();
ctx.arc(toolX, toolY, 5, 0, Math.PI * 2);
ctx.fill();

// Center crosshair
ctx.strokeStyle = "#64748b";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(offsetX - 10, offsetY);
ctx.lineTo(offsetX + 10, offsetY);
ctx.moveTo(offsetX, offsetY - 10);
ctx.lineTo(offsetX, offsetY + 10);
ctx.stroke();
}, [toolPosition, toolPath]);

if (!params) {
return (
<div className="bg-white rounded-xl shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">G-code Simulator</h3>
<div className="text-center text-gray-400 py-8">Waiting for design parameters...</div>
</div>
);
}

const lines = gcode.split("\n");

return (
<div className="bg-white rounded-xl shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">CNC Toolpath Simulation</h3>

<div className="grid grid-cols-2 gap-4">
{/* Canvas */}
<div>
<canvas ref={canvasRef} width={300} height={300} className="border rounded-lg w-full" />

{/* Tool position */}
<div className="mt-2 grid grid-cols-3 gap-2 text-xs">
<div className="bg-gray-100 rounded p-2 text-center">
<div className="text-gray-500">X</div>
<div className="font-mono">{toolPosition.x.toFixed(1)}</div>
</div>
<div className="bg-gray-100 rounded p-2 text-center">
<div className="text-gray-500">Y</div>
<div className="font-mono">{toolPosition.y.toFixed(1)}</div>
</div>
<div className={`rounded p-2 text-center ${toolPosition.z < 0 ? "bg-red-100" : "bg-gray-100"}`}>
<div className="text-gray-500">Z</div>
<div className="font-mono">{toolPosition.z.toFixed(1)}</div>
</div>
</div>

{/* Controls */}
<div className="flex gap-2 mt-3">
<button
onClick={() => setIsPlaying(!isPlaying)}
className={`flex-1 py-2 rounded-lg text-white text-sm ${isPlaying ? "bg-red-500 hover:bg-red-600" : "bg-green-500 hover:bg-green-600"}`}
>
{isPlaying ? "⏸ Pause" : "▶ Play"}
</button>
<button
onClick={() => {
setCurrentLine(0);
setToolPath([]);
setToolPosition({ x: 0, y: 0, z: 5 });
}}
className="px-4 py-2 bg-gray-500 text-white rounded-lg text-sm hover:bg-gray-600"
>
↺ Reset
</button>
</div>
</div>

{/* G-code listing */}
<div className="bg-slate-900 rounded-lg p-3 h-[350px] overflow-y-auto font-mono text-xs">
{lines.map((line, i) => (
<div
key={i}
className={`py-0.5 px-2 ${i === currentLine ? "bg-blue-500/30 text-white" : i < currentLine ? "text-green-400" : "text-gray-500"}`}
>
<span className="text-gray-600 mr-2">{String(i + 1).padStart(3, "0")}</span>
{line}
</div>
))}
</div>
</div>

{/* Progress */}
<div className="mt-4">
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="h-full bg-green-500 transition-all" style={{ width: `${(currentLine / lines.length) * 100}%` }} />
</div>
<div className="text-xs text-gray-500 text-center mt-1">
Line {currentLine + 1} / {lines.length}
</div>
</div>
</div>
);
}

Enhanced G-code Generator

Update src/app/(core)/cad-cam-demo/utils/design-calculator.ts - add more detailed G-code:

export function generateDetailedGCode(params: DesignParams): string {
const radius = params.tankDiameter * 500; // mm (scaled for visualization)
const lines: string[] = [
"; ====================================",
"; TANK FLANGE MACHINING PROGRAM",
"; Generated by Edubotx CAD/CAM",
"; ====================================",
`; Tank: Ø${params.tankDiameter}m × ${params.tankHeight}m`,
`; Volume: ${params.tankVolume}`,
"",
"; Setup",
"G21 ; Metric (mm)",
"G90 ; Absolute positioning",
"G17 ; XY plane selection",
"G40 ; Cancel cutter compensation",
"",
"; Tool: 10mm End Mill",
"T1 M6",
"S3000 M3 ; Spindle on, 3000 RPM",
"",
"; Rapid to safe height",
"G0 Z10",
"",
"; ---- OUTER PROFILE ----",
`G0 X0 Y${radius.toFixed(1)}`,
"G1 Z-3 F500 ; Plunge",
`G2 X0 Y${radius.toFixed(1)} I0 J${(-radius).toFixed(1)} F1000 ; Full circle`,
"G0 Z10",
"",
];

// Add bolt holes
const boltRadius = radius * 0.85;
const boltCount = 8;
lines.push("; ---- BOLT HOLES ----");

for (let i = 0; i < boltCount; i++) {
const angle = (i * 360) / boltCount;
const rad = (angle * Math.PI) / 180;
const x = boltRadius * Math.cos(rad);
const y = boltRadius * Math.sin(rad);

lines.push(`; Hole ${i + 1}`);
lines.push(`G0 X${x.toFixed(1)} Y${y.toFixed(1)}`);
lines.push("G1 Z-5 F300");
lines.push("G0 Z10");
}

lines.push("");
lines.push("; ---- END PROGRAM ----");
lines.push("M5 ; Spindle off");
lines.push("G0 Z50 ; Retract");
lines.push("G0 X0 Y0 ; Return home");
lines.push("M30 ; Program end");

return lines.join("\n");
}

Next: 06-DEMO-PAGE.md

Put it all together in a complete demo page →