by adom
0402 SMD Red LED · 2.1 V drop · 25 mA max
← Commit history
Add README.html landing page: the wiki serves it in the sandboxed HTML variant and embeds the interactive 3D viewer (Static + Pick-and-Place, datasheet-accurate carrier tape) via a srcdoc iframe, matching the characterization page. All existing sections preserved (Characterization link, Downloads, Thumbnails, Provenance). Asset URLs are absolute for the /readme sandbox context.
1 file changed
+415
README.htmladded+415@@ -0,0 +1,415 @@+<meta charset="utf-8">+<title>IN-S42ATR — Inolux Red 0402 LED</title>+<style>+ :root{--bg:#0d1117;--fg:#e6edf3;--muted:#9fb0c0;--accent:#00b8b0;--border:#30363d;--panel:#161b22;}+ body{margin:0;background:var(--bg);color:var(--fg);+ font:15px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;padding:10px 6px 40px;}+ .wrap{max-width:940px;margin:0 auto;}+ h1{font-size:27px;margin:.2em 0 .05em;letter-spacing:-.01em;}+ h2{font-size:18px;margin:1.7em 0 .55em;border-bottom:1px solid var(--border);padding-bottom:.3em;}+ .sub{color:var(--muted);margin:.1em 0 1.3em;}+ a{color:var(--accent);text-decoration:none;} a:hover{text-decoration:underline;}+ .viewer-card{border:1px solid var(--border);border-radius:12px;overflow:hidden;+ background:#14171d;box-shadow:0 4px 18px #0007;}+ .viewer-card iframe{display:block;width:100%;height:600px;border:0;background:#14171d;}+ .note{color:var(--muted);font-size:13.5px;}+ blockquote{border-left:3px solid var(--accent);margin:1.1em 0;padding:.5em 1em;+ background:var(--panel);border-radius:0 8px 8px 0;color:var(--muted);font-size:13px;}+ table{border-collapse:collapse;width:100%;font-size:13.5px;margin:.7em 0;}+ th,td{border:1px solid var(--border);padding:6px 10px;text-align:left;vertical-align:top;}+ th{background:var(--panel);} tr:nth-child(even) td{background:#11151b;}+ code{background:var(--panel);padding:1px 5px;border-radius:4px;font:12.5px ui-monospace,monospace;}+ pre{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:12px;overflow:auto;}+ pre code{background:none;padding:0;}+ .btn{display:inline-block;background:var(--accent);color:#04231f;font-weight:700;+ padding:9px 18px;border-radius:8px;margin:.3em 0;}+ .btn:hover{text-decoration:none;filter:brightness(1.06);}+</style>+<div class="wrap">+ <h1>IN-S42ATR</h1>+ <p class="sub"><strong>Inolux Corporation</strong> · Red · 0402 · LED<br>+ Top View SMD LED · 0402 (1.0 × 0.5 mm) · AlInGaP</p>++ <h2>Interactive 3D viewer</h2>+ <div class="viewer-card">+ <iframe srcdoc="<meta charset="utf-8">+<title>IN-S42ATR — Static &amp; Pick-and-Place (Colby's viewer + additions)</title>+<style>+ :root{ --accent:#00b8b0; --panel:#161b22; --border:#30363d; }+ html,body{ margin:0; height:100%; overflow:hidden; background:#14171d; color:#e6edf3;+ font:13px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }+ /* Colby's viewer mounts into #app and fills the viewport; our chrome overlays it. */+ #app{ position:fixed; inset:0; }+ #app canvas{ width:100%; height:100%; display:block; outline:none; }++ header{ position:fixed; top:0; left:0; right:0; display:flex; align-items:center; gap:16px;+ padding:10px 16px; z-index:10; pointer-events:none; }+ header h1{ font-size:14px; font-weight:700; margin:0; pointer-events:auto;+ text-shadow:0 1px 3px #0009; }+ header .sub{ color:#a9b6c2; font-size:11px; }+ .seg{ margin-left:auto; display:flex; background:var(--panel); border:1px solid var(--border);+ border-radius:20px; padding:3px; pointer-events:auto; box-shadow:0 2px 8px #0006; }+ .seg button{ background:transparent; border:0; color:#c9d1d9; padding:6px 16px; border-radius:16px;+ font-size:12px; font-weight:600; cursor:pointer; font-family:inherit; }+ .seg button.on{ background:var(--accent); color:#04231f; }++ #legend{ position:fixed; top:112px; right:16px; z-index:10; background:var(--panel); border:1px solid var(--border);+ border-radius:8px; padding:8px 11px; font:11px 'JetBrains Mono',ui-monospace,monospace; color:#c9d1d9;+ display:none; min-width:150px; box-shadow:0 2px 8px #0006; }+ #legend.show{ display:block; }+ #legend .row{ display:flex; align-items:center; gap:7px; padding:2px 0; }+ #legend .sw{ width:11px; height:11px; border-radius:2px; display:inline-block; }++ .btn-row{ position:fixed; bottom:16px; left:50%; transform:translateX(-50%);+ display:flex; gap:8px; z-index:10; }+ .btn{ background:var(--panel); border:1px solid var(--border); color:#e6edf3; padding:6px 14px;+ border-radius:16px; font-size:12px; cursor:pointer; font-family:inherit; box-shadow:0 2px 8px #0006; }+ .btn:hover{ border-color:var(--accent); }+ .btn.on{ background:rgba(0,184,176,0.16); border-color:var(--accent); color:var(--accent); }+ #layer-menu{ position:fixed; bottom:56px; left:50%; transform:translateX(-50%); z-index:10; background:var(--panel);+ border:1px solid var(--border); border-radius:10px; padding:10px 14px; min-width:190px; display:none; box-shadow:0 2px 10px #0007; }+ #layer-menu.open{ display:block; }+ #layer-menu h2{ font-size:10px; letter-spacing:.08em; text-transform:uppercase; color:#8b949e; margin:0 0 8px; font-weight:600; }+ #layer-menu label{ display:flex; align-items:center; gap:8px; padding:4px 0;+ font:12px 'JetBrains Mono',ui-monospace,monospace; cursor:pointer; }+ #layer-menu label.disabled{ opacity:.4; cursor:not-allowed; }+ #layer-menu input{ accent-color:var(--accent); }+ #layer-menu .grp{ margin-top:6px; border-top:1px solid var(--border); padding-top:6px; }+ #status{ position:fixed; top:52px; left:16px; z-index:10; color:#a9b6c2;+ font:11px 'JetBrains Mono',ui-monospace,monospace; text-shadow:0 1px 3px #0009; }+</style>++<div id="app"></div>+<header>+ <h1>IN-S42ATR <span class="sub">· Colby's viewer + Static/Pick&amp;Place additions · local</span></h1>+ <div class="seg">+ <button id="mode-static" class="on">Static</button>+ <button id="mode-pnp">Pick &amp; Place</button>+ </div>+</header>+<div id="status">loading Colby's viewer…</div>+<div id="legend">+ <div class="row"><span class="sw" style="background:#9fb4c8"></span> P&amp;P nozzle (ghost)</div>+ <div class="row"><span class="sw" style="background:#bd964e"></span> Carrier tape (to datasheet)</div>+ <div class="row"><span class="sw" style="background:#d8d0c4"></span> LED in tape (real model)</div>+</div>+<div id="layer-menu"><h2>Layers</h2><div id="layer-list"></div></div>+<div class="btn-row">+ <button class="btn on" id="btn-ground">Ground</button>+ <button class="btn" id="btn-bottom">Bottom light</button>+ <button class="btn" id="btn-layers">Layers</button>+</div>++<!-- Colby/Kyle's ACTUAL shipped viewer bundle (window.Adom3DViewer). Unmodified. -->+<script>+(async () => {+ const statusEl = document.getElementById('status');++ // Wiki blobs render SANDBOXED (no allow-same-origin): localStorage/sessionStorage throw a+ // SecurityError that aborts the viewer's SceneBuilder (leaving no lights / IBL / shadows /+ // skybox). Shim both with no-ops BEFORE his bundle loads so his own init completes normally.+ for (const k of ['localStorage','sessionStorage']) {+ try { window[k] && window[k].getItem('x'); }+ catch (e) { try { Object.defineProperty(window, k, { configurable:true, value:{ getItem:()=>null, setItem:()=>{}, removeItem:()=>{}, clear:()=>{}, key:()=>null, length:0 } }); } catch(_) {} }+ }++ // Wiki blob storage serves .js as application/octet-stream + nosniff, so a <script src>+ // of the bundle is refused by the browser. Fetch it as text and inject it as an inline+ // script (which executes). Behaves the same when served locally.+ if (!window.Adom3DViewer) {+ try {+ const code = await (await fetch('https://wiki.adom.inc/blob/component/in-s42atr/adom-3d-viewer.min.js')).text();+ const s = document.createElement('script'); s.textContent = code; document.head.appendChild(s);+ } catch (e) { statusEl.textContent = 'viewer bundle fetch failed: ' + e.message; return; }+ }+ const V = window.Adom3DViewer;+ if (!V) { statusEl.textContent = 'Adom3DViewer bundle failed to load'; return; }+ const B = V.BABYLON; // his Babylon core instance+ try {++ // 1) His viewer — his camera, SpotLight+ShadowGenerator, studio IBL, skybox, view cube, axes.+ const viewer = V.init(document.getElementById('app'), { zUp: true, showViewCube: true, showGround: true });++ // 2) Load our composite through his pipeline (auto-framed + shadowed).+ await viewer.loadModel('https://wiki.adom.inc/blob/component/in-s42atr/IN-S42ATR.insertion.glb');+ const scene = viewer.getScene();+ scene.animationGroups.slice().forEach(g => g.dispose()); // we drive our own motion++ const led = scene.getNodeByName('IN-S42ATR');+ if (led) led.rotationQuaternion = null; // glTF sets a quaternion that would ignore rotation.z — use Euler+ const ledLit = scene.getNodeByName('LED_lit');+ const fovCone = scene.getNodeByName('fov_cone');+ const ptLight = scene.lights.find(l => l.getClassName && l.getClassName() === 'PointLight');+ const modelParent = (led && led.parent) || null; // share the model's coordinate space++ // Bottom-fill light: the shipped bundle exposes no setBottomLight() and builds no bottomLight,+ // so create our own (matching his design — a downward hemispheric that lights the part's+ // underside), using his Babylon instance via the existing hemi's constructor. Toggled by the button.+ let bottomFill = scene.getLightByName('bottomLight');+ if (!bottomFill) {+ const hemi = scene.getLightByName('light') || scene.lights.find(l => l.getClassName && l.getClassName() === 'HemisphericLight');+ if (hemi) {+ const HL = hemi.constructor;+ bottomFill = new HL('our_bottom_fill', new B.Vector3(0,0,-1), scene);+ bottomFill.diffuse = new B.Color3(1,1,1); bottomFill.groundColor = new B.Color3(0.5,0.5,0.5);+ bottomFill.intensity = 0;+ }+ }++ // Replicate his loadModel shadow step (ThreeDViewer.svelte ~159-166): register the loaded+ // model as shadow casters/receivers. (His current source does this; the shipped bundle is+ // older, so we do it here — same effect, no change to his code.) refreshRate=1 keeps the+ // cast shadow updating as the LED animates.+ const sg = viewer.getShadowGenerator && viewer.getShadowGenerator();+ if (sg) {+ scene.meshes.forEach(m => {+ if (m.getTotalVertices && m.getTotalVertices() > 0 && !/skyBox|shadowGround|^ground$|viewCube|axis/i.test(m.name)) {+ m.receiveShadows = true; sg.addShadowCaster(m);+ }+ });+ const smap = sg.getShadowMap(); if (smap) smap.refreshRate = 1;+ }++ // 3) Translucent ghosts (nozzle + carrier tape + component-in-tape), built with HIS Babylon.+ const PX = 0.0065, LED_TOP = 0.00045;+ // The shipped bundle exposes only a few BABYLON constructors; harvest TransformNode+ // from his scene (ViewerRoot) and use StandardMaterial (exposed) for the translucent ghosts.+ const TN = (scene.transformNodes[0] && scene.transformNodes[0].constructor) || null;+ const makeGroup = (name) => TN ? new TN(name, scene) : B.MeshBuilder.CreateBox(name, { size:1e-6 }, scene);+ function ghostMat(name, rgb, alpha){+ const m = new B.StandardMaterial(name, scene);+ m.diffuseColor = new B.Color3(rgb[0],rgb[1],rgb[2]);+ m.emissiveColor = new B.Color3(rgb[0]*0.5, rgb[1]*0.5, rgb[2]*0.5); // self-lit so it reads as a translucent ghost+ m.specularColor = new B.Color3(0.1,0.1,0.1);+ m.alpha = alpha; m.backFaceCulling = false; m.disableLighting = false;+ return m;+ }+ const ghostRoot = makeGroup('pnp_ghosts');+ ghostRoot.parent = modelParent;++ // ---- Datasheet carrier-tape constants (Inolux IN-S42AT V1.1, p.9 "Packing Size") ----+ const mm = 0.001;+ const TW=8.00*mm, TT=0.20*mm, TP1=4.00*mm, TE=1.75*mm, TF=3.50*mm, TDo=1.55*mm, TP2=2.00*mm, TAo=0.57*mm, TBo=1.12*mm, TKo=0.55*mm;+ const NPOCK = 3, PKX = 7.0*mm; // pockets shown; active pocket X (nozzle picks here). Set clear of the FR4 board (board ends at +1.6mm; tape starts ~+4mm)+ const ySpr = TW/2 - TE, PKY = ySpr - TF; // sprocket-hole row Y; component-pocket row Y (= pick Y)++ const nozzle = makeGroup('pnp_nozzle'); nozzle.parent = ghostRoot;+ const nozMat = ghostMat('nozMat', [0.62,0.70,0.78], 0.34, 0.6, 0.3);+ const tip = B.MeshBuilder.CreateCylinder('nozzle_tip', { height:0.0006, diameterBottom:0.0004, diameterTop:0.0008, tessellation:40 }, scene);+ tip.rotation.x = Math.PI/2; tip.position = new B.Vector3(0,0,0.0003);+ const shaft = B.MeshBuilder.CreateCylinder('nozzle_shaft', { height:0.0032, diameter:0.0008, tessellation:40 }, scene);+ shaft.rotation.x = Math.PI/2; shaft.position = new B.Vector3(0,0,0.0022);+ [tip, shaft].forEach(m => { m.material = nozMat; m.parent = nozzle; });+ nozzle.position = new B.Vector3(PKX, PKY, 0.0032);++ // ---- CAD-accurate embossed carrier tape (Inolux IN-S42AT V1.1, p.9), lying FLAT ----+ const pocketXs = Array.from({length:NPOCK}, (_,k)=> PKX + k*TP1);+ // sprocket holes: pitch Po, offset P2 from the pockets; base extends 1mm past the outermost holes+ const holeXs = pocketXs.map(x => x - TP2).concat([pocketXs[pocketXs.length-1] + (TP1 - TP2)]);+ const tMinX = Math.min(...holeXs) - 0.001, tMaxX = Math.max(...holeXs) + 0.001, tLen = tMaxX - tMinX, tCx = (tMinX+tMaxX)/2;+ const solidMat = (name,rgb) => { const m=new B.StandardMaterial(name,scene); m.diffuseColor=new B.Color3(rgb[0],rgb[1],rgb[2]); m.emissiveColor=new B.Color3(rgb[0]*0.55,rgb[1]*0.55,rgb[2]*0.55); m.specularColor=new B.Color3(0.1,0.1,0.1); m.backFaceCulling=false; return m; };+ const tapeMat = solidMat('tapeMat', [0.90,0.85,0.73]); // opaque BEIGE carrier tape (realistic)+ const wallMat = solidMat('wallMat', [0.62,0.56,0.45]); // embossed pocket cavity (darker beige)+ const holeMat = solidMat('holeMat', [0.05,0.05,0.06]); // sprocket holes (dark)+ const coverMat = ghostMat('coverMat', [0.62,0.76,0.92], 0.09); // clear cover-tape film (stays translucent)+ const POCKET_LED_Z = -TKo + 0.00003; // LED seated on the pocket floor (fully recessed: Ko 0.55 deep, LED 0.45 tall -> top ~0.1mm below the tape surface, as in the reel)+ const IN_TAPE_ROT = -Math.PI/2; // cathode faces +Y (toward sprockets), per datasheet+ const WT = 0.00004; // pocket wall thickness+ const tape = makeGroup('tape_reel'); tape.parent = ghostRoot;+ // base film with the pocket band OPEN at each pocket (embossed pockets + LEDs show through, not covered)+ const yPocT = PKY + TBo/2, yPocB = PKY - TBo/2;+ const strip = (w,h,cx,cy,nm) => { const m=B.MeshBuilder.CreateBox(nm,{width:w,height:h,depth:TT},scene); m.position=new B.Vector3(cx,cy,-TT/2); m.material=tapeMat; m.parent=tape; };+ // strip above the pockets — subdivided plane with REAL sprocket holes cut out (delete triangles inside each hole)+ const stripCy = (yPocT + TW/2)/2, stripH = TW/2 - yPocT;+ const topStrip = B.MeshBuilder.CreateGround('tape_top', { width:tLen, height:stripH, subdivisionsX:Math.round(tLen/0.00007), subdivisionsY:Math.max(24, Math.round(stripH/0.00007)), updatable:true }, scene);+ topStrip.rotation.x = -Math.PI/2; topStrip.position = new B.Vector3(tCx, stripCy, -TT/2); topStrip.material = tapeMat; topStrip.parent = tape;+ { const pos = topStrip.getVerticesData('position'), idx = topStrip.getIndices(), keep = [];+ const hl = holeXs.map(hx => ({ lx: hx - tCx, lz: ySpr - stripCy, r: TDo/2 }));+ for (let t=0;t<idx.length;t+=3){ const a=idx[t],b=idx[t+1],c=idx[t+2];+ const px=(pos[a*3]+pos[b*3]+pos[c*3])/3, pz=(pos[a*3+2]+pos[b*3+2]+pos[c*3+2])/3;+ if (!hl.some(h => (px-h.lx)*(px-h.lx) + (pz-h.lz)*(pz-h.lz) < h.r*h.r)) keep.push(a,b,c); }+ topStrip.setIndices(keep); }+ strip(tLen, yPocB + TW/2, tCx, (-TW/2 + yPocB)/2, 'tape_bot'); // strip below the pockets+ const openHalf = TAo/2 + WT; // pocket-band fillers between the open pockets+ const edges = [tMinX, ...pocketXs.flatMap(x => [x-openHalf, x+openHalf]), tMaxX];+ for (let i=0;i<edges.length;i+=2){ const a=edges[i], b=edges[i+1]; if (b-a>1e-6) strip(b-a, TBo, (a+b)/2, PKY, 'tape_band'+i); }+ // (sprocket holes are real cut-outs in the top strip above — no dark discs)+ pocketXs.forEach((x,k) => { // embossed pocket = 4 walls + floor (exact Ao x Bo x Ko, open top)+ const w = (ww,hh,dd,dx,dy,dz,nm) => { const m=B.MeshBuilder.CreateBox(nm,{width:ww,height:hh,depth:dd},scene); m.position=new B.Vector3(x+dx,PKY+dy,dz); m.material=wallMat; m.parent=tape; };+ w(WT, TBo, TKo, TAo/2, 0, -TKo/2, 'pw'+k+'A'); w(WT, TBo, TKo, -TAo/2, 0, -TKo/2, 'pw'+k+'B');+ w(TAo, WT, TKo, 0, TBo/2, -TKo/2, 'pw'+k+'C'); w(TAo, WT, TKo, 0, -TBo/2, -TKo/2, 'pw'+k+'D');+ w(TAo, TBo, WT, 0, 0, -TKo, 'pf'+k);+ });+ pocketXs.forEach((x,k) => { // actual LED models in NON-active pockets (cathode-correct rotation)+ if (Math.abs(x - PKX) < 1e-9 || !led) return;+ const c = led.clone('tape_led_'+k, tape);+ if (!c) return;+ c.getDescendants(false).forEach(d => { if (d.getClassName && /Light/.test(d.getClassName())) d.dispose(); });+ c.rotationQuaternion = null; // same: use Euler so the 90° in-pocket rotation applies+ c.position = new B.Vector3(x, PKY, POCKET_LED_Z); c.rotation = new B.Vector3(0, 0, IN_TAPE_ROT);+ });+ const cover = B.MeshBuilder.CreateBox('cover_tape', { width:tLen, height:TW*0.6, depth:0.00003 }, scene); // clear cover tape+ cover.position = new B.Vector3(tCx, PKY + TBo*0.2, 0.00004); cover.material = coverMat; cover.parent = tape;++ // 4) Motion — manual per-frame interpolation (no baked groups); positions in the model's space.+ const LIFT=0.00135, NHI=0.0032, NLO=0.00047, CARRY=0.0027;+ function lerpKeys(keys, t){+ for (let i=0;i<keys.length-1;i++){ const a=keys[i], b=keys[i+1];+ if (t>=a[0] && t<=b[0]){ const u=(t-a[0])/(b[0]-a[0]); return [a[1]+(b[1]-a[1])*u, a[2]+(b[2]-a[2])*u, a[3]+(b[3]-a[3])*u]; } }+ const l = keys[keys.length-1]; return [l[1],l[2],l[3]];+ }+ function lerp1(keys, t){ // scalar interpolation (LED pick rotation)+ for (let i=0;i<keys.length-1;i++){ const a=keys[i], b=keys[i+1];+ if (t>=a[0] && t<=b[0]){ const u=(t-a[0])/(b[0]-a[0]); return a[1]+(b[1]-a[1])*u; } }+ return keys[keys.length-1][1];+ }+ const bobKeys = [[0,0,0,0],[1.2,0,0,0],[3.0,0,0,LIFT],[4.2,0,0,LIFT],[6.0,0,0,0]];+ const nozKeys = [[0,PKX,PKY,NHI],[1.0,PKX,PKY,NLO],[1.9,PKX,PKY,NHI],[2.7,PKX,PKY,NHI],[3.6,0,0,NHI],[4.4,0,0,NLO],[5.2,0,0,NHI],[6.8,PKX,PKY,NHI],[8.0,PKX,PKY,NHI]];+ const ledKeys = [[0,PKX,PKY,POCKET_LED_Z],[1.0,PKX,PKY,POCKET_LED_Z],[1.9,PKX,PKY,CARRY],[2.7,PKX,PKY,CARRY],[3.6,0,0,CARRY],[4.4,0,0,0],[6.8,0,0,0],[8.0,PKX,PKY,POCKET_LED_Z]];+ // LED lifts STRAIGHT UP out of the pocket (still at the in-tape angle, so it never clips the pocket walls),+ // THEN the nozzle rotates it to the board orientation (0°) in the air, then carries it over and places it.+ // In-tape angle = IN_TAPE_ROT (cathode toward the sprocket holes, per datasheet); board = 0° (cathode −X).+ const rotKeys = [[0,IN_TAPE_ROT],[1.9,IN_TAPE_ROT],[2.7,0],[8.0,0]];+ let t0 = performance.now();+ scene.onBeforeRenderObservable.add(() => {+ // keep the shadow map re-rendering so the cast shadow follows the animated LED+ if (sg) { const sm = sg.getShadowMap(); if (sm && sm.refreshRate !== 1) sm.refreshRate = 1; }+ const t = (performance.now() - t0) / 1000;+ if (mode === 'static') {+ const p = lerpKeys(bobKeys, t % 6);+ [led, ledLit, fovCone].forEach(n => { if (n) n.position.set(p[0], p[1], p[2]); });+ if (led) led.rotation.z = 0;+ } else {+ const tt = t % 8;+ const np = lerpKeys(nozKeys, tt); nozzle.position.set(np[0], np[1], np[2]);+ const lp = lerpKeys(ledKeys, tt); if (led) { led.position.set(lp[0], lp[1], lp[2]); led.rotation.z = lerp1(rotKeys, tt); }+ const show = tt <= 6.8; if (led && led.isEnabled() !== show) led.setEnabled(show); // in the pocket from t=0, hidden only during the loop reset+ }+ });++ // 5) Layers — includes the two the wiki viewer lacks: Light (LED_lit) and FOV (fov_cone).+ const layers = [+ { key:'light', label:'Light (LED lit)', get:()=>ledLit, extra:on=>{ if(ptLight) ptLight.setEnabled(on); } },+ { key:'fov', label:'FOV (130° cone)', get:()=>fovCone },+ { key:'body', label:'LED body', get:()=>led, grp:true },+ { key:'board', label:'fr4_board', get:()=>scene.getNodeByName('fr4_board') },+ { key:'pad', label:'pad_top', get:()=>scene.getNodeByName('pad_top') },+ { key:'silk', label:'silk', get:()=>scene.getNodeByName('silk') },+ { key:'solder', label:'solder_top', get:()=>scene.getNodeByName('solder_top') },+ ];+ const state = { light:true, fov:true, body:true, board:true, pad:true, silk:true, solder:true };+ const listEl = document.getElementById('layer-list'); const rows = {};+ const setNodeEnabled = (node,on)=>{ if(!node) return; node.setEnabled(on); if(node.getChildMeshes) node.getChildMeshes().forEach(c=>c.setEnabled(on)); };+ layers.forEach(L => {+ const row = document.createElement('label'); if (L.grp) row.className='grp';+ const cb = document.createElement('input'); cb.type='checkbox'; cb.checked=state[L.key];+ cb.onchange = () => { state[L.key]=cb.checked; setNodeEnabled(L.get(),cb.checked); if(L.extra)L.extra(cb.checked); };+ row.appendChild(cb); row.append(' '+L.label); listEl.appendChild(row); rows[L.key]={row,cb};+ });+ const lockLayer = (key,locked)=>{ rows[key].row.classList.toggle('disabled',locked); rows[key].cb.disabled=locked; };++ // His frameModel() mis-frames in this embed (it framed the 5000-unit skybox → camera at+ // radius ~2500 m, so the ~3mm part is a sub-pixel dot). Run it for its spotlight/ground+ // positioning, then set a sane mm-scale camera explicitly.+ function applyFrame(mode){+ try { viewer.frameModel(); } catch (e) {}+ const c = scene.activeCamera;+ c.lowerRadiusLimit = 0.0015; c.upperRadiusLimit = 0.08; c.minZ = 0.00005; c.maxZ = 10;+ c.alpha = 1.05; c.beta = 1.0;+ if (mode === 'pnp') { c.target.copyFromFloats(0.003, 0, 0.0006); c.radius = 0.026; }+ else { c.target.copyFromFloats(0, 0, -0.0002); c.radius = 0.0065; }+ }++ // 6) Mode switching — ghosts are a content root only in P&P (so Static frames just the part).+ let mode = 'static';+ function setMode(m){+ mode = m; t0 = performance.now();+ document.getElementById('mode-static').classList.toggle('on', m==='static');+ document.getElementById('mode-pnp').classList.toggle('on', m==='pnp');+ document.getElementById('legend').classList.toggle('show', m==='pnp');+ if (m==='static'){+ setNodeEnabled(ghostRoot, false); viewer.removeContentRoot(ghostRoot);+ lockLayer('light',false); lockLayer('fov',false);+ if (led) setNodeEnabled(led, true);+ if (ledLit) setNodeEnabled(ledLit, state.light); if (ptLight) ptLight.setEnabled(state.light);+ if (fovCone) setNodeEnabled(fovCone, state.fov);+ applyFrame(m);+ statusEl.textContent = 'STATIC · LED insertion (up/down) · toggle Light & FOV in Layers';+ } else {+ setNodeEnabled(ghostRoot, true); viewer.addContentRoot(ghostRoot, { castShadows:false, receiveShadows:false });+ if (ledLit) setNodeEnabled(ledLit, false); if (fovCone) setNodeEnabled(fovCone, false); if (ptLight) ptLight.setEnabled(false);+ lockLayer('light',true); lockLayer('fov',true);+ if (led) setNodeEnabled(led, true);+ applyFrame(m);+ statusEl.textContent = 'PICK & PLACE · nozzle picks LED from tape → places on pads';+ }+ }++ // 7) Buttons — Ground / Bottom light call HIS API; Layers is our menu.+ document.getElementById('mode-static').onclick = () => setMode('static');+ document.getElementById('mode-pnp').onclick = () => setMode('pnp');+ document.getElementById('btn-ground').onclick = function(){ this.classList.toggle('on'); viewer.setGroundVisible(this.classList.contains('on')); };+ document.getElementById('btn-bottom').onclick = function(){ this.classList.toggle('on'); if (bottomFill) bottomFill.intensity = this.classList.contains('on') ? 0.6 : 0; };+ document.getElementById('btn-layers').onclick = function(){ document.getElementById('layer-menu').classList.toggle('open'); this.classList.toggle('on'); };++ // Close the Layers menu on any click outside it (and outside the Layers button).+ document.addEventListener('click', (e) => {+ const menu = document.getElementById('layer-menu'), btn = document.getElementById('btn-layers');+ if (menu.classList.contains('open') && !menu.contains(e.target) && !btn.contains(e.target)) {+ menu.classList.remove('open'); btn.classList.remove('on');+ }+ }, true);++ setMode('static');+ window._v = { viewer, scene, B, setMode }; // debug hook+ } catch (e) { statusEl.textContent = 'init error: ' + e.message; window._err = (e.stack || e.message); console.error(e); }+})();+</script>+" title="IN-S42ATR interactive 3D viewer" allow="fullscreen"></iframe>+ </div>+ <p class="note">Interactive assembly view on the Adom 3D viewer with <strong>Static</strong> and+ <strong>Pick & Place</strong> modes. In Pick & Place the nozzle lifts an+ LED straight out of a datasheet-accurate carrier tape, rotates it to the board orientation, and seats+ it on the pads. Use <strong>Layers</strong> to toggle the board, pads, solder, silk, the LED body,+ <strong><code>LED_lit</code></strong> (red emitter on/off), and the <strong>FOV</strong> cone.</p>++ <h2>Characterization</h2>+ <p><a class="btn" href="/blob/component/in-s42atr/IN-S42ATR.html">View full characterization page →</a></p>+ <p class="note">Interactive specs, footprint viewer, symbol viewer, and design charts.</p>++ <blockquote><strong>Provenance — 3D assembly.</strong> <code>IN-S42ATR.glb</code> is a composite+ built from the page's own <code>IN-S42ATR.step</code> (via cadquery for the sunken side embossing)+ plus a generated FR4 board, copper pads, solder, and cathode silk mark. The LED body keeps its three+ original material regions (pale epoxy, gold terminations, green cathode dot); the top lens is tinted red+ with a toggleable emissive <code>LED_lit</code> layer. The insert animation is baked into the GLB.</blockquote>++ <h2>Downloads</h2>+ <table>+ <tr><th>File</th><th>Description</th></tr>+ <tr><td><a href="/blob/component/in-s42atr/IN-S42ATR.html">IN-S42ATR.html</a></td><td>Characterization Page</td></tr>+ <tr><td><a href="/blob/component/in-s42atr/IN-S42ATR.kicad_sym">IN-S42ATR.kicad_sym</a></td><td>KiCad Symbol</td></tr>+ <tr><td><a href="/blob/component/in-s42atr/IN-S42ATR.kicad_mod">IN-S42ATR.kicad_mod</a></td><td>KiCad Footprint</td></tr>+ <tr><td><a href="/blob/component/in-s42atr/IN-S42ATR.step">IN-S42ATR.step</a></td><td>3D STEP Model</td></tr>+ </table>++ <h2>Thumbnails (<code>ct/</code>)</h2>+ <p class="note">Standard chip-thumbnail bundle for this part — the full family of icons every+ downstream surface needs (index tiles, library cards, wiki heroes, <code>adom-symbol</code> centre+ overlay, print outlines) so no consumer has to re-invoke OCCT / KiCad rendering.</p>+ <pre><code>adom-chip-thumbnailer once IN-S42ATR # 3D chip icons+adom-symbol render --file IN-S42ATR.kicad_sym --out ct/IN-S42ATR-symbol.svg+adom-footprint render --file IN-S42ATR.kicad_mod --out ct/IN-S42ATR-footprint.svg</code></pre>+ <table>+ <tr><th>Kind</th><th>Consumer</th></tr>+ <tr><td>Shaded 3D iso (6 orientations)</td><td>Card / index hero</td></tr>+ <tr><td>Transparent alpha-keyed icons</td><td>UI overlay, <code>adom-symbol</code> centre</td></tr>+ <tr><td>Vector outlines (11 styles × plain/-named/-named90)</td><td>Print, blueprint, neon UI, x-ray</td></tr>+ <tr><td>Symbol thumbnail (<a href="https://wiki.adom.inc/adom/adom-symbol">adom-symbol</a>)</td><td>Library card, tooltip</td></tr>+ <tr><td>Footprint thumbnail (<a href="https://wiki.adom.inc/adom/adom-footprint">adom-footprint</a>)</td><td>Library card, tooltip</td></tr>+ <tr><td>Manifest (<code>ct/IN-S42ATR-thumbs.json</code>)</td><td>Consumer index</td></tr>+ </table>+ <blockquote><strong>Provenance — <code>ct/</code> thumbnails.</strong> 3D icons by+ <code>adom-chip-thumbnailer once</code> (v0.6.0) driving <code>service-step2glb</code>'s OCCT+ pipeline from <code>IN-S42ATR.step</code>. Symbol/footprint SVGs by <code>adom-symbol</code> /+ <code>adom-footprint</code>. Canonical Adom dark palette (<code>#0d1117</code> / <code>#00b8b0</code>).</blockquote>+</div>