Loading files
//! Re-bundle uploaded entries into a release zip and trigger a
//! browser download.
//!
//! The browser-side release is "what the user just uploaded, plus
//! whatever extras we've generated" — no kicad-cli, no fab-tool. For
//! the **PCBWay** variant we append `PCBWAY_FAB_SPECS.md` with the
//! board dimensions + component counts pulled from the centroid CSV.
//!
//! The actual download mechanism is a transient `<a download>` link
//! pointed at an Object URL of a Blob — same trick every wasm app uses
//! since the browser File System Access API isn't universally available.
use std::collections::BTreeMap;
use std::io::Write;
use wasm_bindgen::JsCast;
use zip::write::SimpleFileOptions;
use zip::ZipWriter;
/// Minimal stats shape the fab-specs writer needs. Mirrors the
/// in-app `ComponentStats` but kept here so this module stays
/// independent of `app.rs`. Caller fills in only what's available;
/// missing data just gets omitted from the output.
pub struct PcbwayStats {
pub total: usize,
pub top: usize,
pub bottom: usize,
/// `None` when no BOM was uploaded — SMT / THT rows are omitted.
pub smt: Option<usize>,
pub tht: Option<usize>,
pub unknown_mount: usize,
/// SMT pad counts from F.Paste / B.Paste flashes. Either field is
/// `None` when the corresponding paste layer wasn't in the upload.
pub smt_pads_top: Option<usize>,
pub smt_pads_bottom: Option<usize>,
}
/// PCBWay fab-specs markdown — same shape as
/// `vendor/pcbway::write_fab_specs_md` on the native side, but
/// generated from in-browser data (no .kicad_pcb available).
pub fn pcbway_fab_specs_md(
project_stem: &str,
rev_tag: &str,
board_width_mm: Option<f64>,
board_height_mm: Option<f64>,
stats: Option<&PcbwayStats>,
) -> String {
let mut out = String::new();
out.push_str(&format!(
"# {} — {} · PCBWay Fab Specs\n\n",
project_stem, rev_tag
));
out.push_str(
"Part and dimensional data assembled from the uploaded release \
zip. Paste these into PCBWay's assembly quote form.\n\n",
);
if let (Some(w), Some(h)) = (board_width_mm, board_height_mm) {
out.push_str("## Board dimensions\n\n");
out.push_str("| Dimension | mm |\n");
out.push_str("|-----------|---:|\n");
out.push_str(&format!("| Width | {:.2} |\n", w));
out.push_str(&format!("| Height | {:.2} |\n", h));
out.push_str(&format!("| Area | {:.2} mm² |\n\n", w * h));
}
if let Some(s) = stats {
out.push_str("## Component counts\n\n");
out.push_str("| Metric | Count |\n");
out.push_str("|------------------|------:|\n");
out.push_str(&format!("| Total components | {} |\n", s.total));
out.push_str(&format!("| Parts on top | {} |\n", s.top));
out.push_str(&format!("| Parts on bottom | {} |\n", s.bottom));
match (s.smt, s.tht) {
(Some(smt), Some(tht)) => {
out.push_str(&format!("| SMT parts | {} |\n", smt));
out.push_str(&format!("| Through-hole | {} |\n", tht));
if s.unknown_mount > 0 {
out.push_str(&format!(
"| Unclassified | {} |\n",
s.unknown_mount
));
}
}
_ => {
out.push_str("\n");
out.push_str(
"_SMT / through-hole split needs the BOM CSV \
bundled in the upload._\n",
);
}
}
out.push_str("\n");
// SMT pad sub-table — counted from F.Paste / B.Paste flashes,
// independent of the BOM. Same metric PCBWay quotes assembly
// against, so worth surfacing as its own table.
if s.smt_pads_top.is_some() || s.smt_pads_bottom.is_some() {
let top = s.smt_pads_top.unwrap_or(0);
let bot = s.smt_pads_bottom.unwrap_or(0);
out.push_str("## SMT pads\n\n");
out.push_str("| Metric | Count |\n");
out.push_str("|------------------|------:|\n");
out.push_str(&format!("| SMT pads on top | {} |\n", top));
out.push_str(&format!("| SMT pads bottom | {} |\n", bot));
out.push_str(&format!("| SMT pads total | {} |\n\n", top + bot));
}
}
out.push_str(
"_Generated by CopperForge (browser). \
See `RELEASE_NOTES.md` and the bundled BOM/centroid for the rest._\n",
);
out
}
/// Build a fresh zip from `entries` (everything the user uploaded)
/// plus any extra `(name, bytes)` pairs (the generated fab-specs MD).
/// Returns the zip bytes — caller hands them to `trigger_download`.
pub fn build_zip(
entries: &BTreeMap<String, Vec<u8>>,
extras: &[(String, Vec<u8>)],
) -> Result<Vec<u8>, String> {
let mut buf = Vec::new();
{
let cursor = std::io::Cursor::new(&mut buf);
let mut zw = ZipWriter::new(cursor);
let opts: SimpleFileOptions = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
for (name, bytes) in entries {
zw.start_file(name.as_str(), opts)
.map_err(|e| format!("zip start_file {}: {}", name, e))?;
zw.write_all(bytes)
.map_err(|e| format!("zip write {}: {}", name, e))?;
}
for (name, bytes) in extras {
zw.start_file(name.as_str(), opts)
.map_err(|e| format!("zip start_file {}: {}", name, e))?;
zw.write_all(bytes)
.map_err(|e| format!("zip write {}: {}", name, e))?;
}
zw.finish()
.map_err(|e| format!("zip finish: {}", e))?;
}
Ok(buf)
}
/// Trigger a browser download for `bytes` named `filename`. Creates a
/// short-lived `<a download href=blob:...>` element, programmatically
/// clicks it, and revokes the object URL. Works on every browser
/// engine without needing the File System Access API.
pub fn trigger_download(filename: &str, bytes: &[u8]) -> Result<(), String> {
let array = js_sys::Uint8Array::from(bytes);
let parts = js_sys::Array::new();
parts.push(&array);
let blob = web_sys::Blob::new_with_u8_array_sequence(&parts)
.map_err(|_| "Blob construction failed".to_string())?;
let url = web_sys::Url::create_object_url_with_blob(&blob)
.map_err(|_| "Object URL creation failed".to_string())?;
let document = web_sys::window()
.and_then(|w| w.document())
.ok_or("No document".to_string())?;
let anchor: web_sys::HtmlAnchorElement = document
.create_element("a")
.map_err(|_| "create_element failed".to_string())?
.dyn_into()
.map_err(|_| "anchor cast failed".to_string())?;
anchor.set_href(&url);
anchor.set_download(filename);
// Mount the element so click() works in Safari; remove right after.
let body = document.body().ok_or("No body".to_string())?;
body.append_child(&anchor)
.map_err(|_| "anchor append failed".to_string())?;
anchor.click();
body.remove_child(&anchor)
.map_err(|_| "anchor remove failed".to_string())?;
let _ = web_sys::Url::revoke_object_url(&url);
Ok(())
}
a72ad9cd1This file is pinned to the selected release. Download the original to open it in your design tool.