← Commit history

Altium body height from the STEP z-extent (wiki #22, the culling field)

Colby Knox ·5a1603559b ·1mo ago ·parent af731d3
2 files changed +58−2
crates/altium-codec/src/lib.rs+14
@@ -47,6 +47,16 @@ mod tests {         }     } +    #[test]+    fn overall_height_comes_from_the_step_z_extent() {+        // 1.2 mm of z-extent must render as Altium writes it: 47.2441mil+        // (the exact value Altium itself derived for the CN809S kernel test).+        let step = b"ISO-10303-21;\nDATA;\n#1=CARTESIAN_POINT('',(0.,0.,0.));\n#2=CARTESIAN_POINT('',(1.,1.,1.2));\nENDSEC;\n";+        let h = pcblib::step_z_extent_mil(step).expect("extent");+        assert!((h - 47.2441).abs() < 0.001, "1.2mm must read as 47.2441 mil, got {h}");+        assert_eq!(pcblib::step_z_extent_mil(b"no points here"), None);+    }+     #[test]     fn embedded_step_models_land_in_the_store_and_the_body_record() {         // wiki adom-lbr#22: the Altium lane shipped an empty model store, so@@ -82,6 +92,10 @@ mod tests {         let fd = read(&mut comp, "/WITHMODEL/Data");         let ftxt = String::from_utf8_lossy(&fd).to_string();         assert!(ftxt.contains("MODEL.EMBED=TRUE") && ftxt.contains("V7_LAYER=MECHANICAL1"), "body record missing");+        // Altium CULLS a zero-height body (the live A/B on adom-lbr#22 proved+        // OVERALLHEIGHT=0mil was the one field between bare pads and a rendered+        // part), so a synthetic STEP with no parseable points takes the floor.+        assert!(ftxt.contains("OVERALLHEIGHT=10.0000mil"), "floor height must be written, never 0: {ftxt}");         let id = md.split("ID={").nth(1).unwrap().split('}').next().unwrap();         assert!(ftxt.contains(&format!("MODELID={{{id}}}")), "body must reference the store's MODELID");         // primitive count includes the body
crates/altium-codec/src/pcblib.rs+44−2
@@ -174,6 +174,9 @@ struct ModelRef<'a> {     em: &'a EmbeddedModel,     guid: String,     checksum: u32,+    /// Height actually written: the model's own field, else the STEP z-extent,+    /// else a 10 mil floor. Never zero, because Altium culls a zero-height body.+    height_mil: f64, }  pub fn encode_pcblib(fp: &Footprint) -> Result<Vec<u8>> {@@ -209,6 +212,11 @@ pub fn encode_pcblib_many_with_models(fps: &[Footprint], models: &[Option<Embedd                     h.sum()                 };                 let guid = model_guid(&em.name, &em.step);+                let height_mil = if em.overall_height_mil > 0.0 {+                    em.overall_height_mil+                } else {+                    step_z_extent_mil(&em.step).filter(|h| *h > 0.0).unwrap_or(10.0)+                };                 // The store prints the checksum SIGNED and the body record                 // prints it UNSIGNED; both renderings of one u32 (vendor                 // fixture: -1067402024 and 3227565272).@@ -219,7 +227,7 @@ pub fn encode_pcblib_many_with_models(fps: &[Footprint], models: &[Option<Embedd                 let mut enc = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());                 std::io::Write::write_all(&mut enc, &em.step)?;                 store.push((rec, enc.finish()?));-                refs.push(Some(ModelRef { em, guid, checksum }));+                refs.push(Some(ModelRef { em, guid, checksum, height_mil }));             }             None => refs.push(None),         }@@ -599,6 +607,40 @@ fn harvest_pad_data_block(tpl: &BTreeMap<String, Vec<u8>>) -> Result<Vec<u8>> { }  /// Create any missing parent storages, then write `path` as a stream.+/// Overall height of a STEP body in mils, from the z-extent of its+/// CARTESIAN_POINT entities (wiki part models are millimetre-unit STEP).+/// Altium CULLS a body whose OVERALLHEIGHT is 0, so this is what keeps an+/// embedded model visible (adom-lbr#22: the one field between bare pads and+/// a rendered part).+pub fn step_z_extent_mil(step: &[u8]) -> Option<f64> {+    let txt = String::from_utf8_lossy(step);+    let (mut zmin, mut zmax) = (f64::MAX, f64::MIN);+    let mut found = false;+    let mut rest: &str = &txt;+    while let Some(i) = rest.find("CARTESIAN_POINT") {+        rest = &rest[i + 15..];+        // shapes: CARTESIAN_POINT('name',(x,y,z)) with arbitrary spacing+        let Some(open) = rest.find('(') else { break };+        let Some(coords_open) = rest[open + 1..].find('(') else { break };+        let coords_start = open + 1 + coords_open + 1;+        let Some(coords_len) = rest[coords_start..].find(')') else { break };+        let coords = &rest[coords_start..coords_start + coords_len];+        let parts: Vec<&str> = coords.split(',').collect();+        if parts.len() == 3 {+            if let Ok(z) = parts[2].trim().parse::<f64>() {+                zmin = zmin.min(z);+                zmax = zmax.max(z);+                found = true;+            }+        }+        rest = &rest[coords_start + coords_len..];+    }+    if !found || zmax <= zmin {+        return None;+    }+    Some((zmax - zmin) / 0.0254)+}+ /// Deterministic model GUID (8-4-4-4-12 uppercase) hashed from the model name /// and payload, so identical inputs encode identically. fn model_guid(name: &str, step: &[u8]) -> String {@@ -639,7 +681,7 @@ fn body_record(fp: &Footprint, m: &ModelRef) -> Vec<u8> {     ];     let txt = format!(         "V7_LAYER=MECHANICAL1|NAME= |KIND=0|SUBPOLYINDEX=-1|UNIONINDEX=0|ARCRESOLUTION=0.5mil|ISSHAPEBASED=FALSE|CAVITYHEIGHT=0mil|STANDOFFHEIGHT=0mil|OVERALLHEIGHT={oh}mil|BODYPROJECTION=0|ARCRESOLUTION=0.5mil|BODYCOLOR3D=8421504|BODYOPACITY3D=1.000|IDENTIFIER=|TEXTURE=|TEXTURECENTERX=0mil|TEXTURECENTERY=0mil|TEXTURESIZEX=0mil|TEXTURESIZEY=0mil|TEXTUREROTATION= 0.00000000000000E+0000|MODELID={{{id}}}|MODEL.CHECKSUM={ck}|MODEL.EMBED=TRUE|MODEL.NAME={name}|MODEL.2D.X=0mil|MODEL.2D.Y=0mil|MODEL.2D.ROTATION=0.000|MODEL.3D.ROTX={rx:.3}|MODEL.3D.ROTY={ry:.3}|MODEL.3D.ROTZ={rz:.3}|MODEL.3D.DZ={dz}mil|MODEL.MODELTYPE=1|MODEL.MODELSOURCE=Undefined",-        oh = m.em.overall_height_mil as i64,+        oh = format!("{:.4}", m.height_mil),         id = m.guid,         ck = m.checksum,         name = m.em.name,