dcode / app.js
twarner's picture
init interface
f479e26
raw
history blame
8.18 kB
// 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 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;
}
}
// 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';
};