feat: FluidNC upload, bed/paper layout, pen-plotter gcode hardening
- Send-to-printer button POSTs gcode to FluidNC's /upload from Rust (avoids CORS) - Bed dimensions + paper-on-bed offset; Viewport draws nested bed/paper outlines - Pen lift height as a slider (default 2mm); replaces freeform pen_up string - Generated gcode now auto-homes ($H on its own line — FluidNC \$ parser doesn't strip ; comments) and adds safety pen-ups at start/end - Fix: export_all_gcode was scaling stroke coords by original image dims instead of DPI-scaled per-pass dims, blowing up output coords - Defaults: A4 paper, feed_travel 5000, Letter/Legal/A5 added to picker Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ serde_json = "1"
|
||||
base64 = "0.22"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["multipart", "rustls-tls", "blocking"] }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 2
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
# Frontend changes: edit code, click ↺ in the app (no restart needed).
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Install frontend deps if missing
|
||||
if [ ! -d src-frontend/node_modules ]; then
|
||||
echo "Installing frontend dependencies..."
|
||||
npm --prefix src-frontend install
|
||||
fi
|
||||
|
||||
# Start Vite if it isn't already running
|
||||
if ! lsof -ti:1420 > /dev/null 2>&1; then
|
||||
echo "Starting Vite dev server..."
|
||||
|
||||
@@ -216,6 +216,18 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadToPrinter() {
|
||||
const url = (gcodeConfig.printer_url || '').trim()
|
||||
if (!url) { setGlobalStatus('Set Printer URL in the G-code panel first'); return }
|
||||
try {
|
||||
setGlobalStatus(`Uploading to ${url}…`)
|
||||
const names = await tauri.uploadToPrinter(gcodeConfig, url)
|
||||
setGlobalStatus(`Uploaded ${names.length} file(s) to ${url} — open the WebUI to run`)
|
||||
} catch (e) {
|
||||
setGlobalStatus(`Upload error: ${e.message ?? e}`)
|
||||
}
|
||||
}
|
||||
|
||||
function setGcode(patch) { setGcodeConfig(c => ({ ...c, ...patch })) }
|
||||
|
||||
// ── Project save ───────────────────────────────────────────────────────────
|
||||
@@ -405,9 +417,22 @@ export default function App() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Paper on bed */}
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-neutral-500 text-xs font-semibold uppercase tracking-wider mb-1.5">
|
||||
Paper on bed ({gcodeConfig.bed_w_mm}×{gcodeConfig.bed_h_mm}mm)
|
||||
</p>
|
||||
<Slider label="Paper X" value={gcodeConfig.paper_offset_x_mm ?? 0}
|
||||
min={0} max={Math.max(0, (gcodeConfig.bed_w_mm ?? 220) - gcodeConfig.paper_w_mm)} step={1} unit="mm"
|
||||
onChange={v => setGcode({ paper_offset_x_mm: v })} />
|
||||
<Slider label="Paper Y" value={gcodeConfig.paper_offset_y_mm ?? 0}
|
||||
min={0} max={Math.max(0, (gcodeConfig.bed_h_mm ?? 320) - gcodeConfig.paper_h_mm)} step={1} unit="mm"
|
||||
onChange={v => setGcode({ paper_offset_y_mm: v })} />
|
||||
</div>
|
||||
|
||||
{/* Placement */}
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-neutral-500 text-xs font-semibold uppercase tracking-wider mb-1.5">Placement</p>
|
||||
<p className="text-neutral-500 text-xs font-semibold uppercase tracking-wider mb-1.5">Image on paper</p>
|
||||
<Slider label="Width mm" value={gcodeConfig.img_w_mm} min={10} max={2000} step={1}
|
||||
onChange={v => setGcode({ img_w_mm: v })} />
|
||||
<Slider label="Offset X" value={gcodeConfig.offset_x_mm} min={-500} max={500} step={1} unit="mm"
|
||||
@@ -426,14 +451,28 @@ export default function App() {
|
||||
onChange={v => setGcode({ feed_draw: v })} unit=" mm/m" />
|
||||
<Slider label="Travel speed" value={gcodeConfig.feed_travel} min={100} max={10000} step={100}
|
||||
onChange={v => setGcode({ feed_travel: v })} unit=" mm/m" />
|
||||
<Slider label="Pen lift height" value={gcodeConfig.pen_up_z_mm ?? 2} min={0.5} max={5} step={0.1} unit="mm"
|
||||
onChange={v => setGcode({ pen_up_z_mm: v })} />
|
||||
</div>
|
||||
|
||||
{/* Export */}
|
||||
<div>
|
||||
<p className="text-neutral-500 text-xs font-semibold uppercase tracking-wider mb-1.5">Export</p>
|
||||
{/* Export & upload */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-neutral-500 text-xs font-semibold uppercase tracking-wider">Output</p>
|
||||
<button onClick={exportAll} disabled={!passes.some(p => p.strokeCount > 0)}
|
||||
className="w-full px-3 py-1.5 rounded bg-indigo-700 hover:bg-indigo-600 text-xs text-white disabled:opacity-40 transition-colors">
|
||||
Export G-code
|
||||
Export G-code to folder
|
||||
</button>
|
||||
<div className="space-y-1">
|
||||
<label className="text-neutral-400 text-xs">Printer URL</label>
|
||||
<input type="text" value={gcodeConfig.printer_url ?? ''}
|
||||
onChange={e => setGcode({ printer_url: e.target.value })}
|
||||
placeholder="http://fluidnc.local"
|
||||
className="w-full px-2 py-1 rounded bg-neutral-800 border border-neutral-700 text-xs text-neutral-200" />
|
||||
</div>
|
||||
<button onClick={uploadToPrinter}
|
||||
disabled={!passes.some(p => p.strokeCount > 0) || !gcodeConfig.printer_url}
|
||||
className="w-full px-3 py-1.5 rounded bg-emerald-700 hover:bg-emerald-600 text-xs text-white disabled:opacity-40 transition-colors">
|
||||
Send to printer (upload only)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -101,11 +101,29 @@ export default function Viewport({ imageB64, strokes, imgSize, viewMode, gcodeCo
|
||||
const imgWmm = gcodeConfig.img_w_mm
|
||||
if (!imgWmm || imgWmm <= 0) return
|
||||
const spm = (iw * scale) / imgWmm
|
||||
const paperOffX = gcodeConfig.paper_offset_x_mm ?? 0
|
||||
const paperOffY = gcodeConfig.paper_offset_y_mm ?? 0
|
||||
const px = ox - gcodeConfig.offset_x_mm * spm
|
||||
const py = oy - gcodeConfig.offset_y_mm * spm
|
||||
const pw = gcodeConfig.paper_w_mm * spm
|
||||
const ph = gcodeConfig.paper_h_mm * spm
|
||||
ctx.save()
|
||||
// Bed outline (outermost, cyan)
|
||||
if (gcodeConfig.bed_w_mm && gcodeConfig.bed_h_mm) {
|
||||
const bx = px - paperOffX * spm
|
||||
const by = py - paperOffY * spm
|
||||
const bw = gcodeConfig.bed_w_mm * spm
|
||||
const bh = gcodeConfig.bed_h_mm * spm
|
||||
ctx.strokeStyle = 'rgba(80, 200, 220, 0.8)'
|
||||
ctx.lineWidth = 2
|
||||
ctx.setLineDash([10, 6])
|
||||
ctx.strokeRect(bx, by, bw, bh)
|
||||
ctx.setLineDash([])
|
||||
ctx.fillStyle = 'rgba(80, 200, 220, 0.7)'
|
||||
ctx.font = `${Math.max(10, spm * 5)}px sans-serif`
|
||||
ctx.fillText(`Bed ${gcodeConfig.bed_w_mm}×${gcodeConfig.bed_h_mm}mm`, bx + 4, by - 4)
|
||||
}
|
||||
// Paper outline (yellow dashed)
|
||||
ctx.strokeStyle = 'rgba(255, 220, 50, 0.8)'
|
||||
ctx.lineWidth = 2
|
||||
ctx.setLineDash([6, 4])
|
||||
|
||||
@@ -38,6 +38,14 @@ export async function exportAllGcode(gcodeConfig, outDir) {
|
||||
return tracedInvoke('export_all_gcode', { gcodeConfig, outDir })
|
||||
}
|
||||
|
||||
export async function getAllGcode(gcodeConfig) {
|
||||
return tracedInvoke('get_all_gcode', { gcodeConfig })
|
||||
}
|
||||
|
||||
export async function uploadToPrinter(gcodeConfig, printerUrl) {
|
||||
return tracedInvoke('upload_to_printer', { gcodeConfig, printerUrl })
|
||||
}
|
||||
|
||||
export async function exportDebugState(passConfigs) {
|
||||
return tracedInvoke('export_debug_state', { passConfigs })
|
||||
}
|
||||
@@ -59,7 +67,7 @@ export async function pickSaveFile(defaultName) {
|
||||
}
|
||||
|
||||
export async function pickFolder() {
|
||||
return openDialog({ directory: true })
|
||||
return openDialog({ directory: true, title: 'Choose export folder for G-code files' })
|
||||
}
|
||||
|
||||
// ── Project file I/O ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -24,7 +24,7 @@ const MINIMAL_GCODE = {
|
||||
paper_w_mm: 594, paper_h_mm: 841,
|
||||
img_w_mm: 540, offset_x_mm: 27, offset_y_mm: 27,
|
||||
feed_draw: 1000, feed_travel: 3000,
|
||||
pen_down: 'M3 S1000', pen_up: 'M5',
|
||||
pen_down: 'M3 S1000', pen_up_z_mm: 2,
|
||||
}
|
||||
|
||||
const FULL_STATE = {
|
||||
|
||||
@@ -136,14 +136,20 @@ export const PAPER_SIZES = [
|
||||
{ name: 'A2', w: 420, h: 594 },
|
||||
{ name: 'A3', w: 297, h: 420 },
|
||||
{ name: 'A4', w: 210, h: 297 },
|
||||
{ name: 'A5', w: 148, h: 210 },
|
||||
{ name: 'Letter', w: 216, h: 279 },
|
||||
{ name: 'Legal', w: 216, h: 356 },
|
||||
]
|
||||
|
||||
export function defaultGcodeConfig() {
|
||||
return {
|
||||
paper_w_mm: 594, paper_h_mm: 841,
|
||||
img_w_mm: 540,
|
||||
offset_x_mm: 27, offset_y_mm: 27,
|
||||
feed_draw: 1000, feed_travel: 3000,
|
||||
pen_down: 'M3 S1000', pen_up: 'M5',
|
||||
bed_w_mm: 220, bed_h_mm: 320,
|
||||
paper_offset_x_mm: 0, paper_offset_y_mm: 0,
|
||||
paper_w_mm: 210, paper_h_mm: 297,
|
||||
img_w_mm: 180,
|
||||
offset_x_mm: 15, offset_y_mm: 15,
|
||||
feed_draw: 1000, feed_travel: 5000,
|
||||
pen_down: 'G1 Z0.4 F1000', pen_up_z_mm: 2,
|
||||
printer_url: 'http://fluidnc.local',
|
||||
}
|
||||
}
|
||||
|
||||
62
src/gcode.rs
62
src/gcode.rs
@@ -9,10 +9,19 @@ pub const PAPER_SIZES: &[(&str, f32, f32)] = &[
|
||||
("A2", 420.0, 594.0),
|
||||
("A3", 297.0, 420.0),
|
||||
("A4", 210.0, 297.0),
|
||||
("A5", 148.0, 210.0),
|
||||
("Letter", 216.0, 279.0),
|
||||
("Legal", 216.0, 356.0),
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GcodeConfig {
|
||||
// Bed (machine work area)
|
||||
pub bed_w_mm: f32,
|
||||
pub bed_h_mm: f32,
|
||||
// Paper placement on bed
|
||||
pub paper_offset_x_mm: f32, // paper left edge from bed left (mm)
|
||||
pub paper_offset_y_mm: f32, // paper top edge from bed top (mm)
|
||||
// Paper
|
||||
pub paper_w_mm: f32,
|
||||
pub paper_h_mm: f32,
|
||||
@@ -24,21 +33,25 @@ pub struct GcodeConfig {
|
||||
pub feed_draw: u32,
|
||||
pub feed_travel: u32,
|
||||
pub pen_down: String,
|
||||
pub pen_up: String,
|
||||
pub pen_up_z_mm: f32, // Z height (mm) for pen-up rapid moves
|
||||
}
|
||||
|
||||
impl Default for GcodeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
paper_w_mm: 594.0, // A1 portrait
|
||||
paper_h_mm: 841.0,
|
||||
img_w_mm: 540.0, // fills most of the width; fit/center adjusts this
|
||||
offset_x_mm: 27.0,
|
||||
offset_y_mm: 27.0,
|
||||
bed_w_mm: 220.0,
|
||||
bed_h_mm: 320.0,
|
||||
paper_offset_x_mm: 0.0,
|
||||
paper_offset_y_mm: 0.0,
|
||||
paper_w_mm: 210.0, // A4 portrait — fits inside 220×320 bed
|
||||
paper_h_mm: 297.0,
|
||||
img_w_mm: 180.0, // leaves a margin
|
||||
offset_x_mm: 15.0,
|
||||
offset_y_mm: 15.0,
|
||||
feed_draw: 1000,
|
||||
feed_travel: 3000,
|
||||
pen_down: "M3 S1000".to_string(),
|
||||
pen_up: "M5".to_string(),
|
||||
feed_travel: 5000,
|
||||
pen_down: "G1 Z0.4 F1000".to_string(),
|
||||
pen_up_z_mm: 2.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,22 +88,30 @@ impl GcodeConfig {
|
||||
|
||||
/// Convert fill results to G-code.
|
||||
/// Pixel coordinates are scaled by `img_w_mm / img_w` (uniform, aspect-correct),
|
||||
/// then offset by `(offset_x_mm, offset_y_mm)`.
|
||||
/// then offset by `(paper_offset_x_mm + offset_x_mm, paper_offset_y_mm + offset_y_mm)`.
|
||||
pub fn to_gcode(results: &[FillResult], img_w: u32, _img_h: u32, cfg: &GcodeConfig) -> String {
|
||||
let scale = cfg.px_to_mm(img_w);
|
||||
let ox = cfg.offset_x_mm;
|
||||
let oy = cfg.offset_y_mm;
|
||||
let ox = cfg.paper_offset_x_mm + cfg.offset_x_mm;
|
||||
let oy = cfg.paper_offset_y_mm + cfg.offset_y_mm;
|
||||
|
||||
let mut out = String::with_capacity(4096);
|
||||
|
||||
out.push_str("; Generated by Trac3r\n");
|
||||
out.push_str(&format!("; Paper: {:.0}x{:.0} mm\n", cfg.paper_w_mm, cfg.paper_h_mm));
|
||||
out.push_str(&format!("; Bed: {:.0}x{:.0} mm\n", cfg.bed_w_mm, cfg.bed_h_mm));
|
||||
out.push_str(&format!("; Paper: {:.0}x{:.0} mm @ ({:.1}, {:.1})\n",
|
||||
cfg.paper_w_mm, cfg.paper_h_mm, cfg.paper_offset_x_mm, cfg.paper_offset_y_mm));
|
||||
out.push_str(&format!("; Image: {:.1}x{:.1} mm @ ({:.1}, {:.1})\n",
|
||||
cfg.img_w_mm, scale * _img_h as f32, ox, oy));
|
||||
out.push_str("G21 ; mm mode\n");
|
||||
out.push_str("G90 ; absolute positioning\n");
|
||||
// $H must be on its own line: FluidNC's $-command parser does not strip
|
||||
// ';' comments (only the gcode path does), so any inline comment becomes
|
||||
// part of the command name lookup and fails with error 3.
|
||||
out.push_str("; home X and Y\n");
|
||||
out.push_str("$H\n");
|
||||
out.push_str(&format!("G0 Z{:.3} ; pen up before any moves\n", cfg.pen_up_z_mm));
|
||||
out.push_str(&format!("G0 F{} ; travel feed\n", cfg.feed_travel));
|
||||
out.push_str("G0 X0 Y0 ; home\n\n");
|
||||
out.push_str("G0 X0 Y0 ; go to origin\n\n");
|
||||
|
||||
for result in results {
|
||||
for stroke in &result.strokes {
|
||||
@@ -106,13 +127,13 @@ pub fn to_gcode(results: &[FillResult], img_w: u32, _img_h: u32, cfg: &GcodeConf
|
||||
out.push_str(&format!("G1 X{:.3} Y{:.3}\n", px * scale + ox, py * scale + oy));
|
||||
}
|
||||
|
||||
out.push_str(&cfg.pen_up);
|
||||
out.push_str("\n\n");
|
||||
out.push_str(&format!("G0 Z{:.3}\n\n", cfg.pen_up_z_mm));
|
||||
}
|
||||
}
|
||||
|
||||
out.push_str(&format!("G0 Z{:.3} ; pen up before final return\n", cfg.pen_up_z_mm));
|
||||
out.push_str(&format!("G0 F{}\n", cfg.feed_travel));
|
||||
out.push_str("G0 X0 Y0 ; return home\n");
|
||||
out.push_str("G0 X0 Y0 ; return to origin\n");
|
||||
out.push_str("; End\n");
|
||||
out
|
||||
}
|
||||
@@ -152,7 +173,12 @@ mod tests {
|
||||
];
|
||||
let cfg = GcodeConfig::default();
|
||||
let code = to_gcode(&results, 100, 100, &cfg);
|
||||
assert_eq!(code.matches(&cfg.pen_down).count(), code.matches(&cfg.pen_up).count());
|
||||
// Two extra pen_ups: one at start (before any move) and one at end (before return).
|
||||
let pen_up_marker = format!("G0 Z{:.3}", cfg.pen_up_z_mm);
|
||||
assert_eq!(
|
||||
code.matches(&pen_up_marker).count(),
|
||||
code.matches(&cfg.pen_down).count() + 2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
127
src/lib.rs
127
src/lib.rs
@@ -320,6 +320,10 @@ pub struct ProcessResult {
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct GcodeConfigPayload {
|
||||
#[serde(default = "default_bed_w")] pub bed_w_mm: f32,
|
||||
#[serde(default = "default_bed_h")] pub bed_h_mm: f32,
|
||||
#[serde(default)] pub paper_offset_x_mm: f32,
|
||||
#[serde(default)] pub paper_offset_y_mm: f32,
|
||||
pub paper_w_mm: f32,
|
||||
pub paper_h_mm: f32,
|
||||
pub img_w_mm: f32,
|
||||
@@ -328,8 +332,11 @@ pub struct GcodeConfigPayload {
|
||||
pub feed_draw: u32,
|
||||
pub feed_travel: u32,
|
||||
pub pen_down: String,
|
||||
pub pen_up: String,
|
||||
#[serde(default = "default_pen_up_z")] pub pen_up_z_mm: f32,
|
||||
}
|
||||
fn default_bed_w() -> f32 { 220.0 }
|
||||
fn default_bed_h() -> f32 { 320.0 }
|
||||
fn default_pen_up_z() -> f32 { 2.0 }
|
||||
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -992,23 +999,19 @@ async fn process_pass(payload: ProcessPassPayload, state: State<'_, Mutex<AppSta
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn export_all_gcode(
|
||||
gcode_config: GcodeConfigPayload,
|
||||
out_dir: String,
|
||||
state: State<Mutex<AppState>>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let st = state.lock().unwrap();
|
||||
let (iw, ih) = st.image_rgb.as_ref().map(|r| r.dimensions()).unwrap_or((512, 512));
|
||||
let cfg = to_gcode_config(&gcode_config);
|
||||
let mut saved = Vec::new();
|
||||
|
||||
/// Build (filename, gcode_string) for every pen with strokes, in order.
|
||||
/// Strokes are produced in the DPI-scaled pixel coordinate space, so the
|
||||
/// gcode scale must use the per-pass scaled dimensions, not the original
|
||||
/// image dimensions.
|
||||
fn build_all_gcode(st: &AppState, cfg: &gcode::GcodeConfig) -> Vec<(String, String)> {
|
||||
let mut out = Vec::new();
|
||||
for ps in st.passes.iter() {
|
||||
let (iw, ih) = (ps.img_w.max(1), ps.img_h.max(1));
|
||||
let mut pens = ps.pen_results.clone();
|
||||
pens.sort_by_key(|pr| pr.order);
|
||||
for pr in &pens {
|
||||
if pr.fill.strokes.is_empty() { continue; }
|
||||
let code = gcode::to_gcode(&[pr.fill.clone()], iw, ih, &cfg);
|
||||
let code = gcode::to_gcode(&[pr.fill.clone()], iw, ih, cfg);
|
||||
let slug = if pr.label.is_empty() {
|
||||
format!("{:02x}{:02x}{:02x}", pr.color[0], pr.color[1], pr.color[2])
|
||||
} else {
|
||||
@@ -1017,17 +1020,103 @@ fn export_all_gcode(
|
||||
.collect()
|
||||
};
|
||||
let fname = format!("{:02}_{slug}.gcode", pr.order + 1);
|
||||
let path = std::path::Path::new(&out_dir).join(&fname);
|
||||
std::fs::write(&path, &code).map_err(|e| e.to_string())?;
|
||||
out.push((fname, code));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn export_all_gcode(
|
||||
gcode_config: GcodeConfigPayload,
|
||||
out_dir: String,
|
||||
state: State<Mutex<AppState>>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let st = state.lock().unwrap();
|
||||
let cfg = to_gcode_config(&gcode_config);
|
||||
let files = build_all_gcode(&st, &cfg);
|
||||
if files.is_empty() { return Err("No pens with fill data to export".into()); }
|
||||
let mut saved = Vec::with_capacity(files.len());
|
||||
for (fname, code) in &files {
|
||||
let path = std::path::Path::new(&out_dir).join(fname);
|
||||
std::fs::write(&path, code).map_err(|e| e.to_string())?;
|
||||
saved.push(path.to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
Ok(saved)
|
||||
}
|
||||
|
||||
if saved.is_empty() { Err("No pens with fill data to export".into()) } else { Ok(saved) }
|
||||
#[derive(Serialize)]
|
||||
pub struct GcodeFile {
|
||||
pub name: String,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
/// Returns the same files `export_all_gcode` would write, but in-memory.
|
||||
/// Used by the "Send to printer" flow to upload directly without touching disk.
|
||||
#[tauri::command]
|
||||
fn get_all_gcode(
|
||||
gcode_config: GcodeConfigPayload,
|
||||
state: State<Mutex<AppState>>,
|
||||
) -> Result<Vec<GcodeFile>, String> {
|
||||
let st = state.lock().unwrap();
|
||||
let cfg = to_gcode_config(&gcode_config);
|
||||
let files = build_all_gcode(&st, &cfg);
|
||||
if files.is_empty() { return Err("No pens with fill data".into()); }
|
||||
Ok(files.into_iter().map(|(name, code)| GcodeFile { name, code }).collect())
|
||||
}
|
||||
|
||||
/// Upload all generated gcode files to a FluidNC controller via its WebUI's
|
||||
/// `/upload` endpoint. We do this from Rust (not the WebView's fetch) because
|
||||
/// FluidNC doesn't send CORS headers, which would block a browser POST.
|
||||
#[tauri::command]
|
||||
fn upload_to_printer(
|
||||
gcode_config: GcodeConfigPayload,
|
||||
printer_url: String,
|
||||
state: State<Mutex<AppState>>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let files = {
|
||||
let st = state.lock().unwrap();
|
||||
let cfg = to_gcode_config(&gcode_config);
|
||||
build_all_gcode(&st, &cfg)
|
||||
};
|
||||
if files.is_empty() { return Err("No pens with fill data".into()); }
|
||||
|
||||
let base = printer_url.trim().trim_end_matches('/').to_string();
|
||||
if base.is_empty() { return Err("Printer URL is empty".into()); }
|
||||
let endpoint = format!("{}/upload", base);
|
||||
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut uploaded = Vec::with_capacity(files.len());
|
||||
for (fname, code) in files {
|
||||
let part = reqwest::blocking::multipart::Part::bytes(code.into_bytes())
|
||||
.file_name(fname.clone())
|
||||
.mime_str("text/plain")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let form = reqwest::blocking::multipart::Form::new()
|
||||
.text("path", "/")
|
||||
.part("myfile", part);
|
||||
let resp = client.post(&endpoint)
|
||||
.multipart(form)
|
||||
.send()
|
||||
.map_err(|e| format!("{}: {}", fname, e))?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("{}: HTTP {}", fname, resp.status()));
|
||||
}
|
||||
uploaded.push(fname);
|
||||
}
|
||||
Ok(uploaded)
|
||||
}
|
||||
|
||||
fn to_gcode_config(p: &GcodeConfigPayload) -> gcode::GcodeConfig {
|
||||
gcode::GcodeConfig {
|
||||
bed_w_mm: p.bed_w_mm,
|
||||
bed_h_mm: p.bed_h_mm,
|
||||
paper_offset_x_mm: p.paper_offset_x_mm,
|
||||
paper_offset_y_mm: p.paper_offset_y_mm,
|
||||
paper_w_mm: p.paper_w_mm,
|
||||
paper_h_mm: p.paper_h_mm,
|
||||
img_w_mm: p.img_w_mm,
|
||||
@@ -1036,7 +1125,7 @@ fn to_gcode_config(p: &GcodeConfigPayload) -> gcode::GcodeConfig {
|
||||
feed_draw: p.feed_draw,
|
||||
feed_travel: p.feed_travel,
|
||||
pen_down: p.pen_down.clone(),
|
||||
pen_up: p.pen_up.clone(),
|
||||
pen_up_z_mm: p.pen_up_z_mm,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1925,6 +2014,8 @@ pub fn run() {
|
||||
get_gcode_viz,
|
||||
get_pass_viz,
|
||||
export_all_gcode,
|
||||
get_all_gcode,
|
||||
upload_to_printer,
|
||||
export_debug_state,
|
||||
write_project_file,
|
||||
read_project_file,
|
||||
|
||||
Reference in New Issue
Block a user