← 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.

AravK ·033833753e ·3mo ago ·parent 45a8445
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> &middot; Red &middot; 0402 &middot; LED<br>+     Top View SMD LED &middot; 0402 (1.0 &times; 0.5&nbsp;mm) &middot; AlInGaP</p>++  <h2>Interactive 3D viewer</h2>+  <div class="viewer-card">+    <iframe srcdoc="<meta charset=&quot;utf-8&quot;>+<title>IN-S42ATR — Static &amp;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,&quot;Segoe UI&quot;,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=&quot;app&quot;></div>+<header>+  <h1>IN-S42ATR <span class=&quot;sub&quot;>· Colby's viewer + Static/Pick&amp;amp;Place additions · local</span></h1>+  <div class=&quot;seg&quot;>+    <button id=&quot;mode-static&quot; class=&quot;on&quot;>Static</button>+    <button id=&quot;mode-pnp&quot;>Pick &amp;amp; Place</button>+  </div>+</header>+<div id=&quot;status&quot;>loading Colby's viewer…</div>+<div id=&quot;legend&quot;>+  <div class=&quot;row&quot;><span class=&quot;sw&quot; style=&quot;background:#9fb4c8&quot;></span> P&amp;amp;P nozzle (ghost)</div>+  <div class=&quot;row&quot;><span class=&quot;sw&quot; style=&quot;background:#bd964e&quot;></span> Carrier tape (to datasheet)</div>+  <div class=&quot;row&quot;><span class=&quot;sw&quot; style=&quot;background:#d8d0c4&quot;></span> LED in tape (real model)</div>+</div>+<div id=&quot;layer-menu&quot;><h2>Layers</h2><div id=&quot;layer-list&quot;></div></div>+<div class=&quot;btn-row&quot;>+  <button class=&quot;btn on&quot; id=&quot;btn-ground&quot;>Ground</button>+  <button class=&quot;btn&quot; id=&quot;btn-bottom&quot;>Bottom light</button>+  <button class=&quot;btn&quot; id=&quot;btn-layers&quot;>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] &amp;&amp; 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 &amp;&amp; l.getClassName() === 'PointLight');+  const modelParent = (led &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; viewer.getShadowGenerator();+  if (sg) {+    scene.meshes.forEach(m => {+      if (m.getTotalVertices &amp;&amp; m.getTotalVertices() > 0 &amp;&amp; !/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] &amp;&amp; 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 &quot;Packing Size&quot;) ----+  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 &amp;&amp; /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] &amp;&amp; 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] &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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&amp;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 &amp; 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 &amp; 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') &amp;&amp; !menu.contains(e.target) &amp;&amp; !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&nbsp;&amp;&nbsp;Place</strong> modes. In Pick&nbsp;&amp;&nbsp;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 &rarr;</a></p>+  <p class="note">Interactive specs, footprint viewer, symbol viewer, and design charts.</p>++  <blockquote><strong>Provenance &mdash; 3D assembly.</strong> <code>IN-S42ATR.glb</code> is a composite+    built from the page&#39;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 &mdash; 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 &times; 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 &mdash; <code>ct/</code> thumbnails.</strong> 3D icons by+    <code>adom-chip-thumbnailer once</code> (v0.6.0) driving <code>service-step2glb</code>&#39;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>