| |
|
|
| 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 }; |
| |
| |
| 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; |
| |
| |
| if (trimmed.includes('M280')) { |
| const match = trimmed.match(/S(\d+)/); |
| if (match) { |
| const angle = parseInt(match[1]); |
| const wasDown = penDown; |
| penDown = angle < 50; |
| |
| if (wasDown && !penDown && currentPath.length > 1) { |
| this.paths.push([...currentPath]); |
| currentPath = []; |
| } |
| } |
| } |
| |
| |
| 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); |
| |
| |
| 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); |
| |
| |
| this.ctx.strokeStyle = '#333'; |
| this.ctx.lineWidth = 1 / scale; |
| this.ctx.strokeRect( |
| this.bounds.left, |
| this.bounds.bottom, |
| boundsW, |
| boundsH |
| ); |
| |
| |
| 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 xmlns="http://www.w3.org/2000/svg" viewBox="${this.bounds.left} ${-this.bounds.top} ${boundsW} ${boundsH}">`; |
| svg += `<rect x="${this.bounds.left}" y="${-this.bounds.top}" width="${boundsW}" height="${boundsH}" fill="none" stroke="#333"/>`; |
| |
| 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 += `<path d="${d}" fill="none" stroke="#4ade80" stroke-width="1"/>`; |
| } |
| |
| svg += '</svg>'; |
| return svg; |
| } |
| } |
|
|
| |
| 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 = ''; |
| |
| |
| |
| 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); |
| }; |
| }); |
|
|
| |
| 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'; |
| }; |
|
|
|
|