// dcode - Gcode visualization and generation class GcodeViewer { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.paths = []; this.zoom = 1; this.panX = 0; this.panY = 0; this.isDragging = false; this.lastMouse = { x: 0, y: 0 }; // Work area bounds (from machine config) this.bounds = { left: -420.5, right: 420.5, top: 594.5, bottom: -594.5 }; this.resize(); this.setupEvents(); this.draw(); } resize() { const rect = this.canvas.getBoundingClientRect(); this.canvas.width = rect.width * window.devicePixelRatio; this.canvas.height = rect.height * window.devicePixelRatio; this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio); } setupEvents() { this.canvas.addEventListener('wheel', (e) => { e.preventDefault(); const delta = e.deltaY > 0 ? 0.9 : 1.1; this.zoom *= delta; this.zoom = Math.max(0.1, Math.min(10, this.zoom)); this.draw(); }); this.canvas.addEventListener('mousedown', (e) => { this.isDragging = true; this.lastMouse = { x: e.clientX, y: e.clientY }; }); this.canvas.addEventListener('mousemove', (e) => { if (this.isDragging) { this.panX += e.clientX - this.lastMouse.x; this.panY += e.clientY - this.lastMouse.y; this.lastMouse = { x: e.clientX, y: e.clientY }; this.draw(); } }); this.canvas.addEventListener('mouseup', () => this.isDragging = false); this.canvas.addEventListener('mouseleave', () => this.isDragging = false); window.addEventListener('resize', () => { this.resize(); this.draw(); }); } parseGcode(gcode) { this.paths = []; let currentPath = []; let x = 0, y = 0; let penDown = false; const lines = gcode.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith(';')) continue; // Pen up/down if (trimmed.includes('M280')) { const match = trimmed.match(/S(\d+)/); if (match) { const angle = parseInt(match[1]); const wasDown = penDown; penDown = angle < 50; // Down if angle < 50 if (wasDown && !penDown && currentPath.length > 1) { this.paths.push([...currentPath]); currentPath = []; } } } // Movement const xMatch = trimmed.match(/X([-\d.]+)/i); const yMatch = trimmed.match(/Y([-\d.]+)/i); if (xMatch) x = parseFloat(xMatch[1]); if (yMatch) y = parseFloat(yMatch[1]); if ((xMatch || yMatch) && penDown) { currentPath.push({ x, y }); } } if (currentPath.length > 1) { this.paths.push(currentPath); } this.resetView(); this.draw(); } resetView() { this.zoom = 1; this.panX = 0; this.panY = 0; } draw() { const w = this.canvas.width / window.devicePixelRatio; const h = this.canvas.height / window.devicePixelRatio; this.ctx.fillStyle = '#111'; this.ctx.fillRect(0, 0, w, h); this.ctx.save(); this.ctx.translate(w / 2 + this.panX, h / 2 + this.panY); // Scale to fit work area const boundsW = this.bounds.right - this.bounds.left; const boundsH = this.bounds.top - this.bounds.bottom; const scale = Math.min(w / boundsW, h / boundsH) * 0.9 * this.zoom; this.ctx.scale(scale, -scale); // Flip Y // Draw work area this.ctx.strokeStyle = '#333'; this.ctx.lineWidth = 1 / scale; this.ctx.strokeRect( this.bounds.left, this.bounds.bottom, boundsW, boundsH ); // Draw paths this.ctx.strokeStyle = '#4ade80'; this.ctx.lineWidth = 1 / scale; this.ctx.lineCap = 'round'; this.ctx.lineJoin = 'round'; for (const path of this.paths) { if (path.length < 2) continue; this.ctx.beginPath(); this.ctx.moveTo(path[0].x, path[0].y); for (let i = 1; i < path.length; i++) { this.ctx.lineTo(path[i].x, path[i].y); } this.ctx.stroke(); } this.ctx.restore(); } toSVG() { const boundsW = this.bounds.right - this.bounds.left; const boundsH = this.bounds.top - this.bounds.bottom; let svg = ``; svg += ``; for (const path of this.paths) { if (path.length < 2) continue; const d = path.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${-p.y}`).join(' '); svg += ``; } svg += ''; return svg; } } // Main app let viewer; let currentGcode = ''; document.addEventListener('DOMContentLoaded', () => { const canvas = document.getElementById('workplane'); viewer = new GcodeViewer(canvas); document.getElementById('zoom-in').onclick = () => { viewer.zoom *= 1.2; viewer.draw(); }; document.getElementById('zoom-out').onclick = () => { viewer.zoom *= 0.8; viewer.draw(); }; document.getElementById('reset-view').onclick = () => { viewer.resetView(); viewer.draw(); }; document.getElementById('generate').onclick = async () => { const prompt = document.getElementById('prompt').value; if (!prompt) return; document.getElementById('status').textContent = 'Generating...'; document.getElementById('validation').textContent = ''; // TODO: Call actual inference API // For now, show placeholder document.getElementById('status').textContent = 'API endpoint not configured. Deploy with Gradio backend.'; }; document.getElementById('download-gcode').onclick = () => { if (!currentGcode) return; const blob = new Blob([currentGcode], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'output.gcode'; a.click(); URL.revokeObjectURL(url); }; document.getElementById('download-svg').onclick = () => { if (!viewer.paths.length) return; const svg = viewer.toSVG(); const blob = new Blob([svg], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'output.svg'; a.click(); URL.revokeObjectURL(url); }; }); // Expose for testing window.loadGcode = (gcode) => { currentGcode = gcode; viewer.parseGcode(gcode); document.getElementById('download-gcode').disabled = false; document.getElementById('download-svg').disabled = false; document.getElementById('status').textContent = `Loaded ${viewer.paths.length} paths`; document.getElementById('validation').textContent = 'Machine compatible'; document.getElementById('validation').className = 'valid'; };