app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
← Commit history
Merge PR #220: Optimized GLB: crop the silk capture to the board and key out the backdrop (#113)
1 file changed
+58−1
rust/bridge/src/services.rs+58−1@@ -49,10 +49,46 @@ fn multipart(files: &[(&str, String, Vec<u8>, &str)]) -> (Vec<u8>, String) { (out, boundary) } +/// The add-in's silk capture is a square 2048 px render with margin around the board, and the board body stays+/// visible as a backdrop (Fusion draws nothing on a canvas without it). The optimizer maps the WHOLE image onto+/// the board, so an unprepared capture bakes in offset, shrunken labels on an opaque board-coloured sheet+/// (fusion-bridge#113). Crop to the board (the opaque part of the transparent-background capture) and keep only+/// the near-white silk strokes, transparent everywhere else. Returns None when the image cannot be read.+pub(crate) fn clean_silk_png(bytes: &[u8]) -> Option<Vec<u8>> {+ let mut dec = png::Decoder::new(bytes);+ dec.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);+ let mut rd = dec.read_info().ok()?;+ let mut buf = vec![0u8; rd.output_buffer_size()];+ let info = rd.next_frame(&mut buf).ok()?;+ let (w, h) = (info.width as usize, info.height as usize);+ let ch = match info.color_type { png::ColorType::Rgba => 4, png::ColorType::Rgb => 3, png::ColorType::GrayscaleAlpha => 2, png::ColorType::Grayscale => 1, _ => return None };+ let px = |x: usize, y: usize| -> (f32, f32, f32, f32) {+ let i = (y * w + x) * ch;+ match ch { 4 => (buf[i] as f32, buf[i + 1] as f32, buf[i + 2] as f32, buf[i + 3] as f32), 3 => (buf[i] as f32, buf[i + 1] as f32, buf[i + 2] as f32, 255.0),+ 2 => (buf[i] as f32, buf[i] as f32, buf[i] as f32, buf[i + 1] as f32), _ => (buf[i] as f32, buf[i] as f32, buf[i] as f32, 255.0) }+ };+ // the board: bounding box of the opaque pixels (an opaque capture has no margin to crop, so it stays whole)+ let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0usize, 0usize);+ for y in 0..h { for x in 0..w { if px(x, y).3 > 8.0 { x0 = x0.min(x); y0 = y0.min(y); x1 = x1.max(x); y1 = y1.max(y); } } }+ if x0 > x1 || y0 > y1 { return None; }+ let (cw, chh) = (x1 - x0 + 1, y1 - y0 + 1);+ let mut out = Vec::with_capacity(cw * chh * 4);+ for y in y0..=y1 { for x in x0..=x1 {+ let (r, g, b, a) = px(x, y);+ let lum = 0.299 * r + 0.587 * g + 0.114 * b; // silk is near white; board fill, pads and holes are dark+ let alpha = ((lum - 150.0) / 70.0).clamp(0.0, 1.0) * (a / 255.0);+ out.extend_from_slice(&[245, 245, 245, (alpha * 255.0).round() as u8]);+ } }+ let mut png_out = Vec::new();+ { let mut enc = png::Encoder::new(&mut png_out, cw as u32, chh as u32); enc.set_color(png::ColorType::Rgba); enc.set_depth(png::BitDepth::Eight);+ let mut wr = enc.write_header().ok()?; wr.write_image_data(&out).ok()?; }+ Some(png_out)+}+ fn glb_submit(step_path: &str, silk_top: Option<&str>, silk_bottom: Option<&str>, pin: &str, job_name: &str) -> Value { let Ok(step) = std::fs::read(step_path) else { return json!({"ok": false, "error": format!("could not read STEP: {step_path}")}) }; let mut files: Vec<(&str, String, Vec<u8>, &str)> = vec![("step", std::path::Path::new(step_path).file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default(), step, "application/step")];- for (field, p) in [("silk_top", silk_top), ("silk_bottom", silk_bottom)] { if let Some(p) = p { if let Ok(b) = std::fs::read(p) { files.push((field, std::path::Path::new(p).file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default(), b, "image/png")); } } }+ for (field, p) in [("silk_top", silk_top), ("silk_bottom", silk_bottom)] { if let Some(p) = p { if let Ok(raw) = std::fs::read(p) { let b = clean_silk_png(&raw).unwrap_or(raw); files.push((field, std::path::Path::new(p).file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default(), b, "image/png")); } } } let (body, boundary) = multipart(&files); let url = format!("{}/convert?molecule=true&pin={}", step2glb_url(), fusion_core::aps::urlencode(pin)); let resp = ureq::post(&url).timeout(Duration::from_secs(90)).set("Content-Type", &format!("multipart/form-data; boundary={boundary}")).set("X-Client", "fusion-bridge/adom").set("X-Job-Name", job_name).set("User-Agent", &service_ua()).send_bytes(&body);@@ -290,3 +326,24 @@ pub fn set_design_rules(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> V } res }++#[cfg(test)]+mod silk_tests {+ use super::clean_silk_png;+ fn enc(w: u32, h: u32, px: &[u8]) -> Vec<u8> { let mut v = Vec::new(); { let mut e = png::Encoder::new(&mut v, w, h); e.set_color(png::ColorType::Rgba); e.set_depth(png::BitDepth::Eight); e.write_header().unwrap().write_image_data(px).unwrap(); } v }+ fn dec(b: &[u8]) -> (u32, u32, Vec<u8>) { let mut r = png::Decoder::new(b).read_info().unwrap(); let mut buf = vec![0; r.output_buffer_size()]; let i = r.next_frame(&mut buf).unwrap(); (i.width, i.height, buf) }+ #[test]+ fn crops_to_board_and_keeps_only_silk() {+ // 10x10 transparent canvas, a 6x4 opaque brown board at (2,3), one white silk pixel at (4,5)+ let mut px = vec![0u8; 10 * 10 * 4];+ for y in 3..7 { for x in 2..8 { let i = (y * 10 + x) * 4; px[i..i + 4].copy_from_slice(&[110, 100, 65, 255]); } }+ let i = (5 * 10 + 4) * 4; px[i..i + 4].copy_from_slice(&[250, 250, 250, 255]);+ let (w, h, out) = dec(&clean_silk_png(&enc(10, 10, &px)).unwrap());+ assert_eq!((w, h), (6, 4));+ let a = |x: usize, y: usize| out[(y * 6 + x) * 4 + 3];+ assert_eq!(a(2, 2), 255, "the silk pixel stays opaque");+ assert_eq!(a(0, 0), 0, "the board fill turns transparent");+ }+ #[test]+ fn empty_capture_is_left_alone() { assert!(clean_silk_png(&enc(4, 4, &[0u8; 64])).is_none()); }+}