← Commit history

Reorder characterization page: move the interactive 3D viewer to the top as the hero for this part; retitle the old section to Symbol and Footprint. Update the interactive viewer to the current build - datasheet-accurate carrier tape and reel (real sprocket-hole cut-outs, beige, embossed pockets), cathode-correct lift-rotate-place from tape to pad, tape clear of the FR4, plus the earlier camera/lighting/bottom-light/layers fixes.

AravK ·45a844529b ·3mo ago ·parent b5a192e
2 files changed +82−29
IN-S42ATR-viewer.html+72−23
@@ -56,8 +56,8 @@ <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:#c9a24a"></span> Carrier tape (ghost)</div>-  <div class="row"><span class="sw" style="background:#b06a66"></span> Component in tape (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">@@ -102,6 +102,7 @@   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');@@ -152,6 +153,12 @@   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);@@ -159,24 +166,56 @@   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(PX, 0, 0.0032);+  nozzle.position = new B.Vector3(PKX, PKY, 0.0032); -  const tapeMat = ghostMat('tapeMat', [0.79,0.63,0.29], 0.28, 0.1, 0.7);-  const strip = B.MeshBuilder.CreateBox('tape_strip', { width:0.008, height:0.0036, depth:0.0004 }, scene);-  strip.rotation.x = Math.PI/2; strip.position = new B.Vector3(PX,0,-0.0002); strip.material = tapeMat; strip.parent = ghostRoot;-  [-0.002,0,0.002].forEach((dx,i) => {-    const p = B.MeshBuilder.CreateBox('pocket'+i, { width:0.0012, height:0.0007, depth:0.00035 }, scene);-    p.rotation.x = Math.PI/2; p.position = new B.Vector3(PX+dx,0,-0.00022);-    p.material = ghostMat('pmat'+i, [0.15,0.13,0.10], 0.4, 0.1, 0.8); p.parent = ghostRoot;+  // ---- 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);   });-  for (let i=0;i<5;i++){-    const h = B.MeshBuilder.CreateCylinder('sproc'+i, { height:0.0005, diameter:0.0005, tessellation:20 }, scene);-    h.rotation.x = Math.PI/2; h.position = new B.Vector3(PX-0.0032 + i*0.0016, 0.0015, -0.0002);-    h.material = ghostMat('smat'+i, [0.05,0.05,0.06], 0.45, 0.1, 0.8); h.parent = ghostRoot;-  }-  const tapeLed = B.MeshBuilder.CreateBox('tape_component', { width:0.001, height:0.0005, depth:LED_TOP }, scene);-  tapeLed.rotation.x = Math.PI/2; tapeLed.position = new B.Vector3(PX,0,LED_TOP/2);-  tapeLed.material = ghostMat('tapeLedMat', [0.69,0.42,0.40], 0.36, 0.2, 0.5); tapeLed.parent = ghostRoot;+  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;@@ -185,9 +224,18 @@       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,PX,0,NHI],[1.0,PX,0,NLO],[1.5,PX,0,NHI],[3.0,0,0,NHI],[4.0,0,0,NLO],[5.0,0,0,NHI],[6.0,PX,0,NHI],[8.0,PX,0,NHI]];-  const ledKeys = [[0,PX,0,0],[1.0,PX,0,0],[1.5,PX,0,CARRY],[3.0,0,0,CARRY],[4.0,0,0,0],[7.0,0,0,0],[8.0,PX,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@@ -196,11 +244,12 @@     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]);-      const show = tt >= 1.0 && tt <= 7.0; if (led && led.isEnabled() !== show) led.setEnabled(show);+      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     }   }); @@ -233,7 +282,7 @@     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.0034, 0, 0.0011); c.radius = 0.019; }+    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; }   } 
IN-S42ATR.html+10−6
@@ -158,6 +158,14 @@ document.getElementById('copyFullUrl').addEventListener('click',function(){var t  <div class="container"> +  <!-- Interactive 3D Model — hero for this part -->+  <div class="section">+    <div class="card" style="padding:0;overflow:hidden;margin-bottom:12px">+      <div style="font-size:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-dim);padding:12px 16px 0">3D Model &mdash; Static &amp; Pick-and-Place (interactive)</div>+      <iframe src="IN-S42ATR-viewer.html" style="width:100%;height:600px;border:none;display:block" allow="fullscreen"></iframe>+    </div>+  </div>+   <!-- Key Specs -->   <div class="section">     <div class="spec-cards">@@ -661,7 +669,7 @@ document.getElementById('copyFullUrl').addEventListener('click',function(){var t    <!-- Symbol, Footprint & 3D -->  <!-- Symbol, Footprint & 3D View -->   <div class="section">-    <h2 class="section-title">Symbol, Footprint &amp; 3D Model</h2>+    <h2 class="section-title">Symbol &amp; Footprint</h2>     <div class="grid-2" style="margin-bottom:12px">       <!-- Interactive SymView -->       <div class="card" style="padding:0;overflow:hidden">@@ -674,11 +682,7 @@ document.getElementById('copyFullUrl').addEventListener('click',function(){var t         <iframe src="IN-S42ATR-fpview.html" style="width:100%;height:340px;border:none;display:block" allow="fullscreen"></iframe>       </div>     </div>-    <!-- 3D Model — interactive: Adom 3D viewer (Colby's) + Static/Pick-and-Place additions. Local build, embedded here as an interim while the homepage viewer is in progress. -->-    <div class="card" style="padding:0;overflow:hidden;margin-bottom:12px">-      <div style="font-size:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-dim);padding:12px 16px 0">3D Model &mdash; Static &amp; Pick-and-Place (interactive)</div>-      <iframe src="IN-S42ATR-viewer.html" style="width:100%;height:560px;border:none;display:block" allow="fullscreen"></iframe>-    </div>+    <!-- (Interactive 3D model is now the hero section at the top of the page.) -->     <!-- Download / Export Bar -->     <div class="card" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">       <span style="font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px">Export:</span>