← Commit history

2.12.1: Z-up correction for STEP-sourced 3D models; generic delivery folders (PR #46 review)

John Lauer ·0b39ee29fd ·1mo ago ·parent f84ffb6
7 files changed +60−20
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-lbr"-version = "2.12.0"+version = "2.12.1" dependencies = [  "altium-codec",  "anyhow",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.12.0"+version = "2.12.1" edition = "2021" description = "adom-lbr generator / EDA translator: the canonical adom-lbr JSON is the hub, with native converters to Altium (.SchLib/.PcbLib/.IntLib), EAGLE/Fusion (.lbr), and KiCad. Altium output is written natively — no Altium needed at conversion time." 
VERSION+1−1
@@ -1 +1 @@-2.12.0+2.12.1
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "2.12.0",+  "version": "2.12.1",   "type": "app",   "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.",   "dependencies": {},
page.json+9−12
@@ -1,8 +1,8 @@ {   "slug": "adom-lbr",-  "version": "2.12.0",+  "version": "2.12.1",   "type": "app",-  "description": "adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.",+  "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.",   "dependencies": {},   "tags": [     "eda",@@ -52,24 +52,21 @@   ],   "files": [     "bin/**",-    "assets/**",     "SKILL.md",-    "skills/**",     "README.md",     "install.sh",     "uninstall.sh",-    "screenshots/**",-    "dockbar.json"+    "screenshots/**"   ],-  "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools — KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion — through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.",+  "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools \u2014 KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion \u2014 through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.",   "sample_prompts": [     {       "prompt": "Convert this KiCad symbol and footprint to an Altium SchLib and PcbLib",-      "label": "KiCad → Altium"+      "label": "KiCad \u2192 Altium"     },     {       "prompt": "Import this Altium footprint into the adom-lbr JSON",-      "label": "Altium → JSON"+      "label": "Altium \u2192 JSON"     },     {       "prompt": "Bundle this part into a single Altium IntLib",@@ -77,15 +74,15 @@     },     {       "prompt": "Convert this Altium SchLib and PcbLib to a KiCad symbol and footprint",-      "label": "Altium → KiCad"+      "label": "Altium \u2192 KiCad"     },     {       "prompt": "Export this adom-lbr JSON to a KiCad .kicad_sym and .kicad_mod",-      "label": "JSON → KiCad"+      "label": "JSON \u2192 KiCad"     },     {       "prompt": "Open the adom-lbr Manager to browse wiki components and view the layer stackup",       "label": "Browse parts"     }   ]-}\ No newline at end of file+}
src/main.rs+44−1
@@ -2360,6 +2360,14 @@ fn build_3d_viewer(dir: &std::path::Path, mpn: &str) -> Result<String, String> {     }      let viewer_js = ensure_model_viewer()?;+    // STEP-sourced GLBs are usually Z-up while glTF is Y-up, so a chip stands+    // on its edge (barrett's PR #46 catch). Chips are flat: whichever bbox+    // axis is thinnest is the height. Thinnest on Z => Z-up model => pitch it+    // -90deg; thinnest on Y => already Y-up, leave it alone.+    let orientation = match glb_flat_axis(&bytes) {+        Some('z') => " orientation=\"0deg -90deg 0deg\"",+        _ => "",+    };     let poster = ["-3d-iso-named.png", "-3d-iso.png", "-3d-iso-lg.png"]         .iter().map(|s| dir.join(format!("{mpn}{s}"))).find(|p| p.exists())         .and_then(|p| fs::read(&p).ok())@@ -2367,11 +2375,46 @@ fn build_3d_viewer(dir: &std::path::Path, mpn: &str) -> Result<String, String> {         .unwrap_or_default();     Ok(format!(         "<script type=\"module\">{viewer_js}</script>\n\-         <model-viewer src=\"data:model/gltf-binary;base64,{model}\"{poster} camera-controls auto-rotate shadow-intensity=\"1\" style=\"position:absolute;inset:0;width:100%;height:100%;background:#161c27\"></model-viewer>",+         <model-viewer src=\"data:model/gltf-binary;base64,{model}\"{poster}{orientation} camera-controls auto-rotate shadow-intensity=\"1\" style=\"position:absolute;inset:0;width:100%;height:100%;background:#161c27\"></model-viewer>",         model = b64(&bytes)     )) } +/// Which axis is the model THINNEST along, from the union of the JSON chunk's+/// VEC3 float accessor min/max bounds. For chip-shaped parts the thinnest axis+/// is the height, which tells us whether the model is Y-up or Z-up.+fn glb_flat_axis(bytes: &[u8]) -> Option<char> {+    if bytes.len() < 20 || &bytes[0..4] != b"glTF" {+        return None;+    }+    let json_len = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;+    let end = (20 + json_len).min(bytes.len());+    let v: serde_json::Value = serde_json::from_slice(&bytes[20..end]).ok()?;+    let (mut lo, mut hi) = ([f64::MAX; 3], [f64::MIN; 3]);+    let mut seen = false;+    for acc in v.get("accessors")?.as_array()? {+        // quantized meshes store positions as ints (KHR_mesh_quantization), so+        // accept ANY VEC3 accessor that carries min/max bounds+        if acc.get("type").and_then(|t| t.as_str()) != Some("VEC3") {+            continue;+        }+        let (Some(mn), Some(mx)) = (acc.get("min").and_then(|m| m.as_array()), acc.get("max").and_then(|m| m.as_array())) else { continue };+        if mn.len() != 3 || mx.len() != 3 { continue; }+        for i in 0..3 {+            lo[i] = lo[i].min(mn[i].as_f64().unwrap_or(0.0));+            hi[i] = hi[i].max(mx[i].as_f64().unwrap_or(0.0));+        }+        seen = true;+    }+    if !seen { return None; }+    let ext = [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]];+    let thin = if ext[0] <= ext[1] && ext[0] <= ext[2] { 'x' } else if ext[1] <= ext[2] { 'y' } else { 'z' };+    // only meaningful when the part is clearly flat (height under half the next axis)+    let mut sorted = ext; sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());+    if sorted[0] > sorted[1] * 0.6 { return None; }+    Some(thin)+}+ /// GLB magic + JSON chunk scan for the Draco extension requirement. fn glb_is_draco(bytes: &[u8]) -> bool {     if bytes.len() < 20 || &bytes[0..4] != b"glTF" {
src/manage.html+3−3
@@ -613,7 +613,7 @@ document.getElementById('modal-cancel').onclick=hideModal; document.getElementById('modal-go').onclick=()=>{if(modalGo)modalGo();}; function selOpt(sel,attr,cb){document.querySelectorAll('#modal-opts .opt').forEach(o=>o.onclick=()=>{document.querySelectorAll('#modal-opts .opt').forEach(x=>x.classList.remove('sel'));o.classList.add('sel');o.querySelector('input').checked=true;cb(o.dataset[attr]);});} // recommended destination parents for delivered projects (editable in the box)-const DEST_FOLDERS=['/home/barrett-land/Documents/KiCAD/Projects','/home/barrett-land/Documents/Adom/Molecules/Other Molecules','/home/barrett-land/Documents/Adom/Electrical','/home/barrett-land/Downloads'];+const DEST_FOLDERS=['~/Documents/KiCad/Projects','~/Documents/Adom/Molecules','~/Documents/Adom/Electrical','~/Downloads']; function openAddLib(){   const slugs=[...selected]; if(!slugs.length)return;   const plan=buildPlan(slugs,false);@@ -636,7 +636,7 @@ function openAddProj(){   document.getElementById('modal-sub').textContent=`schematic + ${slugs.length} component(s) — symbols & footprints resolved so it opens clean`;   document.getElementById('modal-opts').innerHTML=`<div class="opt sel" data-mode="personal"><input type="radio" name="mode" checked> Integrate into my libraries</div><div class="opt" data-mode="project"><input type="radio" name="mode"> Project-only library</div>`;   // editable destination + recommended-folder presets-  document.getElementById('modal-dest').innerHTML=`<label>Deliver project to</label><div class="destrow"><select id="destsel">${DEST_FOLDERS.map(f=>`<option value="${f}">${f.replace('/home/barrett-land','~')}</option>`).join('')}</select><input id="destpath" spellcheck="false"></div>`;+  document.getElementById('modal-dest').innerHTML=`<label>Deliver project to</label><div class="destrow"><select id="destsel">${DEST_FOLDERS.map(f=>`<option value="${f}">${f}</option>`).join('')}</select><input id="destpath" spellcheck="false"></div>`;   const destpath=document.getElementById('destpath'), destsel=document.getElementById('destsel');   destpath.value=DEST_FOLDERS[0]+'/'+boardSlug;   destsel.onchange=()=>{destpath.value=destsel.value+'/'+boardSlug;};@@ -667,7 +667,7 @@ async function doApply(plan,eda,project,board,dest){       clearInterval(poll);       const r=p.result||{}, pf=(r.project&&r.project.files||[]);       go.textContent=`✓ Added ${r.count||0}`;-      const folder=(r.project&&r.project.folder||'').replace('/home/barrett-land','~');+      const folder=(r.project&&r.project.folder||'').replace(/\/home\/[^/]+/,'~');       bar.innerHTML=`<b>✓ Done.</b> Merged ${r.count||0} symbol(s) into your libraries.`+         (pf.length?`<br>Delivered ${pf.length} project file(s) → <span class="lib">${folder}</span>`:'')+         ((r.errors&&r.errors.length)?`<br><span style="color:#f87171">${r.errors.length} issue(s): ${r.errors.slice(0,2).join('; ')}</span>`:'');