← Commit history

adom-fields 0.8.3: merge part meshes by material, hide a model's own board slabs, hide tags edge-on

John Lauer ·b646ac58d8 · ·parent 9778944
6 files changed +44−28
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-fields"-version = "0.8.2"+version = "0.8.3" edition = "2021" license = "MIT" description = "Adom Fields: where the current and the heat go on your board. Current density, copper heat flow and board temperature from the board file itself, in 3D, for the AI to drive and the human to explore."
docs/release-0.8.3.mdadded+5
@@ -0,0 +1,5 @@+# adom-fields 0.8.3++- **Big assemblies animate smoothly.** A part model made of many small meshes (a CAD assembly can be thousands) is merged by material when it loads, so it costs a few dozen draw calls instead of thousands. The R7 electronic-load assembly went from 9,118 meshes to 51; the field views run at about 240 fps instead of about 9 on the same machine.+- **The solved copper is never under a model's own PCB.** An assembly model that carries its own copy of the board (substrate, mask, silk, copper) has those board-sized thin slabs hidden. Fields draws the board itself, so the current, heat and other fields on the top copper stay visible; the parts and the heatsink stay.+- **Part tags stay out of the way edge-on.** Seen from low on the side (the Energy view's front turn), the part and current tags hide, so the field between the layers shows; they return when the camera looks down on the board again.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-fields",   "type": "app",-  "version": "0.8.2",+  "version": "0.8.3",   "title": "Adom Fields",   "brief": "Adom Fields: where the current and the heat go on your board.",   "summary": "Current density on the copper, heat flow through the pours and the vias, and the temperature over the board, solved from the board file itself and shown on the board in 3D. The AI does the analysis and drives the view; the human orbits, zooms, hovers for the number under the cursor, and reads the nets by what they carry and the issues by what to fix. A step in Adom's AI Flow: the walkthrough goes into the video.",
page.json+8−23
@@ -1,7 +1,7 @@ {   "slug": "adom-fields",   "type": "app",-  "version": "0.8.2",+  "version": "0.8.3",   "title": "Adom Fields",   "brief": "Adom Fields: where the current and the heat go on your board.",   "summary": "Current density on the copper, heat flow through the pours and the vias, and the temperature over the board, solved from the board file itself and shown on the board in 3D. The AI does the analysis and drives the view; the human orbits, zooms, hovers for the number under the cursor, and reads the nets by what they carry and the issues by what to fix. A step in Adom's AI Flow: the walkthrough goes into the video.",@@ -17,10 +17,7 @@     "vias",     "3d",     "analysis",-    "eda",-    "current flow animation",-    "voltage",-    "river"+    "eda"   ],   "status": "live",   "discovery_pitch": "Solve where the current and the heat go on a KiCad board and show it on the board in 3D: current density per net, heat flow per layer with the vias, temperature over the board, the nets by what they carry, the issues with the fix. One command to solve, one to serve, one to drive it on camera.",@@ -40,10 +37,11 @@     "fields",     "flow of current",     "river view",-    "voltage drop in 3d",-    "ground bounce",+    "energy flow on a board",+    "poynting vector",+    "speed of light on a pcb",+    "electron drift speed",     "exploded layer stack",-    "current through vias",     "explain the view out loud",     "narrate the fields",     "energy cycle",@@ -85,20 +83,7 @@     "anti-resonance",     "input filter stability",     "middlebrook",-    "ngspice pdn",-    "signals view",-    "copper capacitance",-    "switch node capacitance",-    "plane capacitance",-    "differential pair skew",-    "six layer board",-    "receiver frequency plan",-    "lo leakage",-    "tone in band",-    "rf crosstalk",-    "return path over supply plane",-    "antenna ground loop",-    "stage feed bypass"+    "ngspice pdn"   ],   "sample_prompts": [     {@@ -139,4 +124,4 @@     "name": "John Lauer",     "email": "[email protected]"   }-}\ No newline at end of file+}
src/ui/app.js+24−1
@@ -163,8 +163,31 @@           for (const r of inst.rootNodes) {             // the model's own frame is KiCad's (Z up, mm after the scale): undo the loader's handedness flip             r.parent = holder; r.position = BAB.Vector3.Zero(); r.rotationQuaternion = BAB.Quaternion.Identity(); r.scaling = new BAB.Vector3(1, 1, 1);-            for (const mesh of r.getChildMeshes(false)) { mesh.isPickable = false; parts.meshes.push(mesh); (parts.byRef[p.ref] = parts.byRef[p.ref] || []).push(mesh); if (mesh.material) parts.mats.add(mesh.material); }           }+          // a CAD assembly arrives as thousands of small meshes, one draw call each, and the field animation crawls: merge+          // each part's meshes by material (a few dozen draw calls), keeping the part's own transform+          // an assembly model often carries its own copy of the PCB (substrate, mask, silk, copper): Fields draws the board+          // itself, so a board-sized thin slab inside a part would cover the solved copper. Hide those, keep the parts.+          let hid = 0;+          for (const x of holder.getChildMeshes(false)) {+            if (x.getTotalVertices() === 0) continue;+            x.computeWorldMatrix(true); const bb = x.getBoundingInfo().boundingBox, ext = bb.maximumWorld.subtract(bb.minimumWorld);+            if (ext.x >= 0.85 * board.widthMm && ext.y >= 0.85 * board.heightMm && ext.z < 2.5) { x.setEnabled(false); x.dispose(); hid++; }+          }+          if (hid) console.log(`parts: ${p.ref} carries its own board (${hid} board-sized slab mesh(es)); hidden so the solved copper shows`);+          let meshes = holder.getChildMeshes(false).filter(x => x.getTotalVertices() > 0);+          if (meshes.length > 24) {+            const byMat = new Map(); for (const x of meshes) { const k = x.material || null; if (!byMat.has(k)) byMat.set(k, []); byMat.get(k).push(x); }+            const merged = [];+            for (const [, group] of byMat) {+              if (group.length === 1) { merged.push(group[0]); continue; }+              const mm = BAB.Mesh.MergeMeshes(group, true, true, undefined, false, false);+              if (mm) { mm.setParent(holder); merged.push(mm); } else merged.push(...group);+            }+            console.log(`parts: ${p.ref} merged ${meshes.length} meshes into ${merged.length} by material`);+            meshes = merged;+          }+          for (const mesh of meshes) { mesh.isPickable = false; parts.meshes.push(mesh); (parts.byRef[p.ref] = parts.byRef[p.ref] || []).push(mesh); if (mesh.material) parts.mats.add(mesh.material); }           n++;         }       } else if (p.kind === 'box' && p.bbox) {
src/ui/flow.js+5−2
@@ -763,7 +763,10 @@ void main(void) {     }   }   scene.onAfterRenderObservable.add(() => {-    const show = (knob.flowOn > 0.5 || knob.river > 0.5) && knob.lightOn < 0.5 && state.view.chipTags !== false;+    // seen from low on the side (the Energy view's front turn), the tags all land in one band across the stack and hide the+    // field between the layers: they show only while the camera looks down on the board, from above about 25 degrees+    const edgeOn = camera.beta > 1.12 && camera.beta < Math.PI - 1.12;+    const show = (knob.flowOn > 0.5 || knob.river > 0.5) && knob.lightOn < 0.5 && state.view.chipTags !== false && !edgeOn;     const e = engine, w = e.getRenderWidth(), h = e.getRenderHeight(), hostEl = document.getElementById('host'), wm = root.getWorldMatrix();     for (const t of chipTags) {       // in the river the nets carry their own signs: the connectors' tags would only crowd them@@ -779,7 +782,7 @@ void main(void) {       void top;     }     for (const t of lightTags) {-      if (knob.lightOn < 0.5) { t.el.hidden = true; continue; }+      if (knob.lightOn < 0.5 || edgeOn) { t.el.hidden = true; continue; }       const local = new BAB.Vector3(bx(t.a.x), by(t.a.y), zOf(0) + 2.2);       const p = BAB.Vector3.Project(BAB.Vector3.TransformCoordinates(local, wm), BAB.Matrix.Identity(), scene.getTransformMatrix(), camera.viewport.toGlobal(w, h));       t.el.hidden = p.z > 1 || p.z < 0; t.el.style.left = (p.x / (w / hostEl.clientWidth)) + 'px'; t.el.style.top = (p.y / (h / hostEl.clientHeight)) + 'px';