molecule
BQ25792 Charger
Public Made by Adomby adom
Buck-boost 1S-4S battery charger board (TI BQ25792) with 14 Adom machine pins, by Adonis
← Commit history
animated stackup viewer: assembled->explode->reassemble loop, floaty toolbar, fullscreen, 80% transparent ground, live shadows; per-layer runtime groups (3 GLB groups + JS quads/slabs), bundle loadModel via FilesInputStore
13 files changed
+189−120
grp-components.glb.pngadded⋯ 1 unchanged line ⋯
grp-cu1.glb.pngadded⋯ 1 unchanged line ⋯
grp-cu16.glb.pngadded⋯ 1 unchanged line ⋯
layer-cu-l15.pngadded⋯ 1 unchanged line ⋯
layer-cu-l2.pngadded⋯ 1 unchanged line ⋯
layer-mask-bot.pngadded⋯ 1 unchanged line ⋯
layer-mask-top.pngadded⋯ 1 unchanged line ⋯
layer-silk-bot.pngadded⋯ 1 unchanged line ⋯
layer-silk-top.pngadded⋯ 1 unchanged line ⋯
package.json+1−1@@ -2,7 +2,7 @@ "slug": "bq25792-charger", "title": "BQ25792 Charger", "type": "component",- "version": "0.1.23",+ "version": "0.1.24", "description": "BQ25792 Charger - buck-boost 1S-4S battery charger board by Adonis, with real 3D copper traces and measured stackup.", "tags": [ "hardware"
page.json+10−3@@ -4,9 +4,16 @@ "slug": "bq25792-charger", "title": "BQ25792 Charger", "brief": "Buck-boost 1S-4S battery charger board (TI BQ25792) with 14 Adom machine pins, by Adonis",- "version": "0.1.23",+ "version": "0.1.24", "tags": [- "hardware"+ "adom",+ "adom-modular",+ "component",+ "charger",+ "battery",+ "bq25792",+ "power",+ "usb-c" ], "license": "proprietary", "component": {@@ -49,7 +56,7 @@ "email": "[email protected]" }, "visibility": "public",- "description": "BQ25792 Charger - buck-boost 1S-4S battery charger board by Adonis, with real 3D copper traces and measured stackup.",+ "description": "BQ25792 Charger - a buck-boost 1S-4S battery charger board (Texas Instruments BQ25792) designed by Adonis in Fusion 360, with 14 Adom machine pins for the modular stack. Interactive colored 3D model, schematic and layout views.", "dependencies": {}, "files": [ "README.md",
readme.html+178−116@@ -19,26 +19,34 @@ th { background:#161b22; color:#e6edf3; font-weight:600; } td { color:#9da7b3; } - /* viewer block */- #viewer-card { border:1px solid #21262d; border-radius:10px; overflow:hidden; background:#0a0d12; }- #toolbar { display:flex; align-items:center; gap:8px; padding:10px 12px; background:#161b22; border-bottom:1px solid #21262d; }- #toolbar .label { font-size:13px; color:#8b949e; margin-right:4px; }- .tbtn { font-size:13px; padding:6px 14px; border-radius:6px; border:1px solid #30363d; background:#21262d; color:#c9d1d9; cursor:pointer; }- .tbtn:hover { background:#30363d; }- .tbtn.active { background:#0b3d33; border-color:#00b8b1; color:#5ff5e8; }- #vstatus { margin-left:auto; font-size:12px; color:#6e7681; }+ /* viewer block: floaty toolbar OVER the canvas */+ #viewer-card { position:relative; border:1px solid #21262d; border-radius:10px; overflow:hidden; background:#0a0d12; }+ #viewer-wrap { position:relative; width:100%; height:640px; }+ #viewer-card:fullscreen { border:none; border-radius:0; }+ #viewer-card:fullscreen #viewer-wrap { height:100vh; }+ #floaty { position:absolute; top:14px; left:14px; z-index:30; display:flex; gap:6px;+ background:rgba(13,17,23,0.55); backdrop-filter:blur(8px); border:1px solid rgba(48,54,61,0.8);+ border-radius:10px; padding:6px; }+ .fbtn { font-size:12.5px; padding:6px 12px; border-radius:7px; border:1px solid transparent;+ background:transparent; color:#c9d1d9; cursor:pointer; }+ .fbtn:hover { background:rgba(48,54,61,0.7); }+ .fbtn.active { background:#0b3d33; border-color:#00b8b1; color:#5ff5e8; }+ #vstatus { position:absolute; right:14px; bottom:10px; z-index:30; font-size:12px; color:#8b949e;+ background:rgba(13,17,23,0.5); padding:3px 8px; border-radius:6px; } </style> </head> <body> <div id="viewer-card">- <div id="toolbar">- <span class="label">3D view:</span>- <button class="tbtn active" id="btn-exploded" type="button">Exploded stackup</button>- <button class="tbtn" id="btn-assembled" type="button">Assembled</button>- <span id="vstatus">starting viewer...</span>+ <div id="viewer-wrap"></div>+ <div id="floaty">+ <button class="fbtn active" id="btn-anim" type="button" title="Animate explode/assemble">Animate</button>+ <button class="fbtn" id="btn-assembled" type="button">Assembled</button>+ <button class="fbtn" id="btn-exploded" type="button">Exploded</button>+ <button class="fbtn" id="btn-fs" type="button" title="Fullscreen">Fullscreen</button> </div>- <div id="viewer-wrap" style="position:relative;width:100%;height:560px"></div>+ <span id="vstatus">starting viewer...</span>+</div> </div> <p style="margin-top:10px"><strong>Exploded stackup</strong> separates every fabrication layer (6 mm per slot) so you can see the@@ -96,27 +104,25 @@ exported bodies + design rules.</p> <script type="module"> (function(){+ const B=()=>window.BABYLON; const status=document.getElementById("vstatus"); const wrap=document.getElementById("viewer-wrap");- const btnE=document.getElementById("btn-exploded");+ const card=document.getElementById("viewer-card");+ const btnAnim=document.getElementById("btn-anim"); const btnA=document.getElementById("btn-assembled");- const MODELS={- exploded:"/blob/component/bq25792-charger/bq25792-charger-exploded.glb.png",- assembled:"/blob/component/bq25792-charger/bq25792-charger-assembled.glb.png"- };- // The readme sandbox forbids ALL fetch (connect-src none) - but img-src allows https:,- // and /blob serves PNG inline with CORS. So the full-quality GLBs ride in the repo as- // LOSSLESS PNGs (bytes packed into RGB channels, 4-byte LE length header) and are read- // back pixel-perfect through a CORS-clean canvas. No fetch, no size cap, no wasm.+ const btnE=document.getElementById("btn-exploded");+ const btnFS=document.getElementById("btn-fs");+ const set=(t)=>{ if(status) status.textContent=t; };+ const R="/blob/component/bq25792-charger/";+ const mm=1e-3;+ function loadPngBytes(url){ return new Promise(function(resolve,reject){- var img=new Image();- img.crossOrigin="anonymous";+ var img=new Image(); img.crossOrigin="anonymous"; img.onload=function(){ try{ var cv=document.createElement("canvas"); cv.width=img.width; cv.height=img.height;- var ctx=cv.getContext("2d",{willReadFrequently:true});- ctx.drawImage(img,0,0);+ var ctx=cv.getContext("2d",{willReadFrequently:true}); ctx.drawImage(img,0,0); var d=ctx.getImageData(0,0,img.width,img.height).data; var total=img.width*img.height, rgb=new Uint8Array(total*3); for(var i=0,j=0;i<total;i++){ var k=i*4; rgb[j++]=d[k]; rgb[j++]=d[k+1]; rgb[j++]=d[k+2]; }@@ -124,103 +130,159 @@ exported bodies + design rules.</p> resolve(rgb.subarray(4,4+n)); }catch(e){ reject(e); } };- img.onerror=function(){ reject(new Error("image load failed: "+url)); };+ img.onerror=function(){ reject(new Error("image failed: "+url)); }; img.src=url; }); }- function setStatus(t){ if(status) status.textContent=t; }- // CSP shim: the readme sandbox blocks blob: for both fetch (connect-src none) and- // images (img-src data: https:). Babylon wraps glTF textures in Blob object-URLs, so- // stamp small Blobs with an equivalent data: URI and hand THAT back from- // createObjectURL - img-src data: is allowed. No effect outside this document.- (function(){- var orig=URL.createObjectURL.bind(URL);- URL.createObjectURL=function(b){ return (b&&b.__dataUri)||orig(b); };- var OrigBlob=window.Blob;- window.Blob=function(parts,opts){- var bl=new OrigBlob(parts,opts);- try{- if(parts&&parts.length===1&&(parts[0] instanceof Uint8Array||parts[0] instanceof ArrayBuffer)){- var u8=parts[0] instanceof Uint8Array?parts[0]:new Uint8Array(parts[0]);- if(u8.length<3000000){- var st=""; for(var i=0;i<u8.length;i+=0x8000){ st+=String.fromCharCode.apply(null,u8.subarray(i,i+0x8000)); }- bl.__dataUri="data:"+((opts&&opts.type)||"application/octet-stream")+";base64,"+btoa(st);- }- }- }catch(e){}- return bl;- };- window.Blob.prototype=OrigBlob.prototype;- })();+ const Viewer=window.Adom3DViewerBabylon9;- if(typeof Viewer==="undefined"){ setStatus("viewer bundle failed to load"); return; }+ if(!Viewer){ set("viewer bundle failed to load"); return; } let viewer;- try { viewer=Viewer.init(wrap,{zUp:true,showViewCube:true,showGround:true}); }- catch(e){ setStatus("viewer failed to start: "+(e&&e.message)); return; }- let busy=false;- let appended=[]; // meshes we added - the bundle's clearScene doesn't know about them- function show(which){- if(busy) return; busy=true;- setStatus("loading "+which+"...");- btnE.classList.toggle("active",which==="exploded");- btnA.classList.toggle("active",which==="assembled");- appended.forEach(function(m){ try{m.dispose(false,true);}catch(e){} });- appended=[];- var preCount=viewer.getScene().meshes.length;- Promise.resolve(viewer.clearScene&&viewer.clearScene()).then(function(){- // Extend the Adom viewer: append into ITS scene via Babylon from a File object.- // The readme CSP (connect-src none) blocks every fetch - even data: URIs - so the- // geometry rides a File (FileReader, no network) and the GLB's textures are- // data: URIs in its glTF JSON (img-src data: is allowed).- return loadPngBytes(MODELS[which]).then(function(bin){- var f=new File([bin],which+".glb",{type:"model/gltf-binary"});- return window.BABYLON.SceneLoader.AppendAsync("", f, viewer.getScene(), null, ".glb");- });- }).then(function(){- appended=viewer.getScene().meshes.slice(preCount);- // The bundle only starts its render loop inside its own loadModel(), which we- // bypass - start it ourselves (idempotent: stop first).- var eng=viewer.getEngine(), sc=viewer.getScene();- eng.stopRenderLoop();- eng.runRenderLoop(function(){ sc.render(); });- viewer.frameModel();- // frameModel only frames the bundle's own registry - frame OUR meshes on the- // LIVE activeCamera (viewer.getCamera() can be a different object), after a tick- // so nothing internally reframes over us.- setTimeout(function(){- try{- var sc2=viewer.getScene();- var ext=sc2.getWorldExtends(); // full scene - quantized models rescale via node matrices, model dominates- var c=new window.BABYLON.Vector3((ext.min.x+ext.max.x)/2,(ext.min.y+ext.max.y)/2,(ext.min.z+ext.max.z)/2);- var dx=ext.max.x-ext.min.x, dy=ext.max.y-ext.min.y, dz=ext.max.z-ext.min.z;- var diag=Math.sqrt(dx*dx+dy*dy+dz*dz);- var cam=sc2.activeCamera;- if(cam && isFinite(diag) && diag>0){- if(cam.setTarget) cam.setTarget(c);- if(typeof cam.radius==="number") cam.radius=diag*1.2;- cam.minZ=Math.max(diag*0.005,0.0001); cam.maxZ=diag*200;- if(typeof cam.wheelPrecision==="number") cam.wheelPrecision=3/diag;- }- }catch(e){}- },200);- // The bundle creates its lights/environment inside its own loadModel - light the- // scene ourselves (PBR is black without any light or environment).+ try{ viewer=Viewer.init(wrap,{zUp:true,showViewCube:true,showGround:true}); }+ catch(e){ set("viewer failed to start: "+(e&&e.message)); return; }++ // layers: [element, assembled z-center (m), exploded z-center (m)]+ const LAYERS=[]; // {meshes:[], delta}+ function reg(meshes, delta){ meshes.forEach(m=>{ m.__z0=m.position.z; }); LAYERS.push({meshes:meshes, delta:delta}); }++ async function build(){+ const sc=viewer.getScene();+ const eng=viewer.getEngine();++ set("loading board...");+ // 1) components via the bundle's OWN loadModel (FilesInputStore + file:)+ const compBytes=await loadPngBytes(R+"grp-components.glb.png");+ B().FilesInputStore.FilesToLoad["components.glb"]=new File([compBytes],"components.glb",{type:"model/gltf-binary"});+ let pre=sc.meshes.length;+ await viewer.loadModel("file:components.glb");+ const compMeshes=sc.meshes.slice(pre);+ // ensure a live render loop (bundle starts it in loadModel, keep ours as belt+suspenders)+ eng.stopRenderLoop(); eng.runRenderLoop(function(){ sc.render(); });++ // 2) outer copper groups appended into the same scene+ for (const g of [["grp-cu1.glb.png", 18*mm-1.5335*mm, "cu1"],["grp-cu16.glb.png", -18*mm+0.0185*mm, "cu16"]]){+ const bytes=await loadPngBytes(R+g[0]);+ const f=new File([bytes],g[2]+".glb",{type:"model/gltf-binary"});+ pre=sc.meshes.length;+ await B().SceneLoader.AppendAsync("", f, sc, null, ".glb");+ reg(sc.meshes.slice(pre), g[1]);+ }+ reg(compMeshes, 36*mm-1.582*mm);++ // 3) FR4 slabs (prepreg/core/prepreg) as translucent boxes at exact thickness+ const fr4mat=new (B().StandardMaterial)("fr4",sc);+ fr4mat.diffuseColor=new (B().Color3)(0.45,0.42,0.28); fr4mat.alpha=0.72; fr4mat.backFaceCulling=false;+ function slab(name, zc, th, delta){+ const bx=B().MeshBuilder.CreateBox(name,{width:50.4*mm,depth:26.4*mm,height:th},sc);+ bx.position=new (B().Vector3)(25.2*mm,13.2*mm,zc); bx.rotation.x=Math.PI/2; // zUp scene: check axis below+ bx.material=fr4mat;+ reg([bx], delta);+ return bx;+ }+ // zUp:true scenes from this bundle keep Z as up; boxes are Y-up primitives -> rotate so height aligns Z+ slab("ppg_top", 1.416*mm, 0.200*mm, 12*mm-1.416*mm);+ slab("core", 0.758*mm, 1.046*mm, 0-0.758*mm);+ slab("ppg_bot", 0.100*mm, 0.200*mm, -12*mm-0.100*mm);++ // 4) gerber quads: silk, mask, inner copper+ function quad(name, tex, zc, delta, mirror){+ const p=B().MeshBuilder.CreatePlane(name,{width:50.4*mm,height:26.4*mm},sc);+ p.position=new (B().Vector3)(25.2*mm,13.2*mm,zc);+ p.rotation.x=0; // plane faces +Z in zUp after billboard? set below+ const mat=new (B().StandardMaterial)(name+"_m",sc);+ const t=new (B().Texture)(R+tex,sc,false,false);+ t.hasAlpha=true; mat.diffuseTexture=t; mat.opacityTexture=t; mat.emissiveTexture=t;+ mat.backFaceCulling=false; mat.disableLighting=false;+ p.material=mat;+ if(mirror){ p.scaling.x=-1; }+ reg([p], delta);+ return p;+ }+ quad("silk_top","layer-silk-top.png", 1.600*mm, 30*mm-1.600*mm,false);+ quad("mask_top","layer-mask-top.png", 1.592*mm, 24*mm-1.592*mm,false);+ quad("cu_l2","layer-cu-l2.png", 1.2985*mm, 6*mm-1.2985*mm,false);+ quad("cu_l15","layer-cu-l15.png", 0.2175*mm,-6*mm-0.2175*mm,false);+ quad("mask_bot","layer-mask-bot.png",-0.075*mm,-24*mm+0.075*mm,true);+ quad("silk_bot","layer-silk-bot.png",-0.083*mm,-30*mm+0.083*mm,true);++ // 5) environment + lights + shadows+ try{+ const env=new (B().EquiRectangularCubeTexture)(R+"studio-env.png",sc,128);+ sc.environmentTexture=env; sc.environmentIntensity=0.9;+ }catch(e){}+ if(sc.lights.length===0){+ new (B().HemisphericLight)("hemi",new (B().Vector3)(0.2,0.2,1),sc).intensity=0.9;+ }+ const key=new (B().DirectionalLight)("key",new (B().Vector3)(-0.4,-0.5,-1),sc);+ key.intensity=1.3; key.position=new (B().Vector3)(0.1,0.1,0.3);+ const sg=new (B().ShadowGenerator)(2048,key);+ sg.useBlurExponentialShadowMap=true; sg.blurKernel=16;+ LAYERS.forEach(L=>L.meshes.forEach(m=>{ try{ sg.addShadowCaster(m,true);}catch(e){} }));++ // 6) ground: 80% transparent + receives shadows+ sc.meshes.forEach(m=>{+ if(/ground/i.test(m.name||"")){+ try{ if(m.material) m.material.alpha=0.2; m.receiveShadows=true; }catch(e){}+ }+ });++ // 7) frame generously for the exploded extents+ setTimeout(function(){ try{- var sc3=viewer.getScene();- if(sc3.lights.length===0){- var B=window.BABYLON;- var h=new B.HemisphericLight("rm_hemi",new B.Vector3(0.3,0.3,1),sc3); h.intensity=1.1;- var d1=new B.DirectionalLight("rm_key",new B.Vector3(-0.5,-0.6,-1),sc3); d1.intensity=1.4;- var d2=new B.DirectionalLight("rm_fill",new B.Vector3(0.7,0.4,-0.6),sc3); d2.intensity=0.6;- }+ const cam=sc.activeCamera;+ cam.radius=cam.radius*1.9;+ cam.beta=1.15;+ cam.minZ=cam.radius*0.01; cam.maxZ=cam.radius*300; }catch(e){}- setStatus(which+" view");- busy=false;- }).catch(function(err){ setStatus("load failed: "+((err&&err.message)||"unknown")); busy=false; });+ },300);+ set("animating");+ }++ // ---- animation engine: assembled -> explode -> hold -> reassemble, looping ----+ let mode="anim"; // anim | t0 | t1+ let t=0; // 0 assembled, 1 exploded+ let phase=0, phaseT=0; // 0 hold assembled,1 explode,2 hold exploded,3 collapse+ const HOLD_A=1.6, MOVE=3.2, HOLD_E=2.2;+ function ease(x){ return x<0.5 ? 2*x*x : 1-Math.pow(-2*x+2,2)/2; }+ function apply(){+ const k=ease(Math.min(Math.max(t,0),1));+ for(const L of LAYERS) for(const m of L.meshes) m.position.z=m.__z0+k*L.delta;+ }+ function tick(dt){+ if(mode==="t0"){ if(t>0){ t=Math.max(0,t-dt/1.2); apply(); } return; }+ if(mode==="t1"){ if(t<1){ t=Math.min(1,t+dt/1.2); apply(); } return; }+ phaseT+=dt;+ if(phase===0){ t=0; if(phaseT>HOLD_A){phase=1;phaseT=0;} }+ else if(phase===1){ t=phaseT/MOVE; if(t>=1){t=1;phase=2;phaseT=0;} }+ else if(phase===2){ t=1; if(phaseT>HOLD_E){phase=3;phaseT=0;} }+ else { t=1-phaseT/MOVE; if(t<=0){t=0;phase=0;phaseT=0;} }+ apply();+ }+ function setMode(m,btn){+ mode=m;+ [btnAnim,btnA,btnE].forEach(b=>b.classList.remove("active"));+ btn.classList.add("active");+ set(m==="anim"?"animating":(m==="t0"?"assembled":"exploded"));+ if(m==="anim"){ phase=(t>=0.5)?2:0; phaseT=0; } }- btnE.addEventListener("click",function(){show("exploded");});- btnA.addEventListener("click",function(){show("assembled");});- show("exploded");+ btnAnim.addEventListener("click",()=>setMode("anim",btnAnim));+ btnA.addEventListener("click",()=>setMode("t0",btnA));+ btnE.addEventListener("click",()=>setMode("t1",btnE));+ btnFS.addEventListener("click",function(){+ if(document.fullscreenElement){ document.exitFullscreen(); return; }+ (card.requestFullscreen?card.requestFullscreen():Promise.reject()).catch?.(function(){+ set("fullscreen blocked in the embed - open the readme URL directly");+ });+ });++ build().then(function(){+ const sc=viewer.getScene();+ let last=performance.now();+ sc.onBeforeRenderObservable.add(function(){+ const now=performance.now(); const dt=Math.min(0.05,(now-last)/1000); last=now;+ tick(dt);+ });+ }).catch(function(e){ set("load failed: "+((e&&e.message)||"unknown")); }); })(); </script> </body>
studio-env.pngadded⋯ 1 unchanged line ⋯