by adom
0402 SMD Red LED · 2.1 V drop · 25 mA max
← Commit history
Viewer refinements: (1) exclude the FOV cone from shadows (light-field indicator, not a solid); (2) rebuild the carrier-tape sprocket holes as smooth vector circles (72-segment radial tessellation) instead of jagged grid cut-outs; (3) give the holed top strip real thickness - a solid 0.2mm slab (datasheet T=0.20) with cylindrical hole walls and edge walls. Updates the characterization-page hero viewer and the README embed.
2 files changed
+92−22
IN-S42ATR-viewer.html+46−11@@ -137,7 +137,7 @@ if (sg.setTransparencyShadow) sg.setTransparencyShadow(true); else sg.transparencyShadow = true; scene.meshes.forEach(m => { if (!m.getTotalVertices || m.getTotalVertices() === 0) return;- if (/skyBox|viewCube|axis|gizmo/i.test(m.name)) return;+ if (/skyBox|viewCube|axis|gizmo|fov/i.test(m.name)) return; // FOV cone is a light-field indicator, not a solid — no shadows m.receiveShadows = true; // everything receives (incl. ground) if (!/shadowGround|^ground$/i.test(m.name) && !shadowed.has(m)) { // ground receives but doesn't cast sg.addShadowCaster(m, true); shadowed.add(m); // (Set guard avoids duplicate casters)@@ -195,16 +195,50 @@ // 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 above the pockets — SMOOTH circular sprocket holes in a SOLID slab (thickness TT, matching the+ // rest of the tape) so the beige part reads as a physical part, not zero-thickness film. Hand-built:+ // radial-tessellated TOP + BOTTOM faces (clean vector circles), cylindrical WALLS through each hole,+ // and outer EDGE walls around the strip. Buffers written directly (no earcut/CSG needed).+ const HSEG = 72, holeR = TDo/2, holeCy = ySpr, zTop = 0, zBot = -TT;+ const sortedHx = holeXs.slice().sort((a,b)=>a-b);+ const bandB = [tMinX]; // vertical band boundaries = midpoints between holes+ for (let i=0;i<sortedHx.length-1;i++) bandB.push((sortedHx[i]+sortedHx[i+1])/2);+ bandB.push(tMaxX);+ const P = [], NR = [], IDX = [];+ const addV = (x,y,z,nx,ny,nz) => { P.push(x,y,z); NR.push(nx,ny,nz); return P.length/3 - 1; };+ sortedHx.forEach((cx,hi) => {+ const bx0 = bandB[hi], bx1 = bandB[hi+1];+ const tIn=[], tOut=[], bIn=[], bOut=[], wT=[], wB=[]; // top/bottom face rings + hole-wall rings+ for (let k=0;k<=HSEG;k++){+ const th = 2*Math.PI*k/HSEG, dx = Math.cos(th), dy = Math.sin(th);+ const ix = cx + holeR*dx, iy = holeCy + holeR*dy; // point ON the hole circle+ let t = Infinity; // ray from hole centre to the band-rectangle edge+ if (dx > 1e-12) t = Math.min(t,(bx1-cx)/dx); else if (dx < -1e-12) t = Math.min(t,(bx0-cx)/dx);+ if (dy > 1e-12) t = Math.min(t,(TW/2-holeCy)/dy); else if (dy < -1e-12) t = Math.min(t,(yPocT-holeCy)/dy);+ const ox = cx + t*dx, oy = holeCy + t*dy; // point on the rectangle edge+ tIn.push(addV(ix,iy,zTop, 0,0,1)); tOut.push(addV(ox,oy,zTop, 0,0,1)); // TOP face ring+ bIn.push(addV(ix,iy,zBot, 0,0,-1)); bOut.push(addV(ox,oy,zBot, 0,0,-1)); // BOTTOM face ring+ wT.push(addV(ix,iy,zTop, -dx,-dy,0)); wB.push(addV(ix,iy,zBot, -dx,-dy,0)); // hole WALL ring (inward normal)+ }+ for (let k=0;k<HSEG;k++){+ IDX.push(tIn[k], tIn[k+1], tOut[k], tOut[k], tIn[k+1], tOut[k+1]); // top face annulus+ IDX.push(bIn[k], bOut[k], bIn[k+1], bOut[k], bOut[k+1], bIn[k+1]); // bottom face annulus (reversed)+ IDX.push(wT[k], wB[k], wT[k+1], wT[k+1], wB[k], wB[k+1]); // cylindrical hole wall+ }+ });+ // outer edge walls around the strip rectangle [tMinX,tMaxX] x [yPocT,TW/2] — gives the tape edges thickness+ const edgeWall = (x0,y0,x1,y1,nx,ny) => { const a=addV(x0,y0,zTop,nx,ny,0), b=addV(x1,y1,zTop,nx,ny,0), c=addV(x0,y0,zBot,nx,ny,0), d=addV(x1,y1,zBot,nx,ny,0); IDX.push(a,c,b, b,c,d); };+ edgeWall(tMinX,TW/2, tMaxX,TW/2, 0,1); // top edge (free edge of the tape)+ edgeWall(tMaxX,yPocT, tMinX,yPocT, 0,-1); // bottom edge (meets the pocket band)+ edgeWall(tMinX,yPocT, tMinX,TW/2, -1,0); // left end+ edgeWall(tMaxX,TW/2, tMaxX,yPocT, 1,0); // right end+ const topStrip = B.MeshBuilder.CreateGround('tape_top', { width:1, height:1, subdivisions:1 }, scene);+ topStrip.setVerticesData('position', P);+ topStrip.setVerticesData('normal', NR);+ topStrip.setVerticesData('uv', new Array((P.length/3)*2).fill(0));+ topStrip.setIndices(IDX);+ topStrip.position = new B.Vector3(0,0,0); topStrip.material = tapeMat; topStrip.parent = tape; // coords baked into vertices+ if (topStrip.refreshBoundingInfo) topStrip.refreshBoundingInfo(); 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];@@ -227,6 +261,7 @@ 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; registerShadows(); // now that ALL geometry exists (model + tape/reel + nozzle + pocket LEDs + cover), enable shadows on everything+ if (fovCone) [fovCone].concat(fovCone.getChildMeshes ? fovCone.getChildMeshes() : []).forEach(m => { if ('receiveShadows' in m) m.receiveShadows = false; if (sg && sg.removeShadowCaster) sg.removeShadowCaster(m); }); // FOV cone: no shadows (belt-and-suspenders vs the /fov/ exclusion) // 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;
README.html+46−11@@ -171,7 +171,7 @@ if (sg.setTransparencyShadow) sg.setTransparencyShadow(true); else sg.transparencyShadow = true; scene.meshes.forEach(m => { if (!m.getTotalVertices || m.getTotalVertices() === 0) return;- if (/skyBox|viewCube|axis|gizmo/i.test(m.name)) return;+ if (/skyBox|viewCube|axis|gizmo|fov/i.test(m.name)) return; // FOV cone is a light-field indicator, not a solid — no shadows m.receiveShadows = true; // everything receives (incl. ground) if (!/shadowGround|^ground$/i.test(m.name) && !shadowed.has(m)) { // ground receives but doesn't cast sg.addShadowCaster(m, true); shadowed.add(m); // (Set guard avoids duplicate casters)@@ -229,16 +229,50 @@ // 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 above the pockets — SMOOTH circular sprocket holes in a SOLID slab (thickness TT, matching the+ // rest of the tape) so the beige part reads as a physical part, not zero-thickness film. Hand-built:+ // radial-tessellated TOP + BOTTOM faces (clean vector circles), cylindrical WALLS through each hole,+ // and outer EDGE walls around the strip. Buffers written directly (no earcut/CSG needed).+ const HSEG = 72, holeR = TDo/2, holeCy = ySpr, zTop = 0, zBot = -TT;+ const sortedHx = holeXs.slice().sort((a,b)=>a-b);+ const bandB = [tMinX]; // vertical band boundaries = midpoints between holes+ for (let i=0;i<sortedHx.length-1;i++) bandB.push((sortedHx[i]+sortedHx[i+1])/2);+ bandB.push(tMaxX);+ const P = [], NR = [], IDX = [];+ const addV = (x,y,z,nx,ny,nz) => { P.push(x,y,z); NR.push(nx,ny,nz); return P.length/3 - 1; };+ sortedHx.forEach((cx,hi) => {+ const bx0 = bandB[hi], bx1 = bandB[hi+1];+ const tIn=[], tOut=[], bIn=[], bOut=[], wT=[], wB=[]; // top/bottom face rings + hole-wall rings+ for (let k=0;k<=HSEG;k++){+ const th = 2*Math.PI*k/HSEG, dx = Math.cos(th), dy = Math.sin(th);+ const ix = cx + holeR*dx, iy = holeCy + holeR*dy; // point ON the hole circle+ let t = Infinity; // ray from hole centre to the band-rectangle edge+ if (dx > 1e-12) t = Math.min(t,(bx1-cx)/dx); else if (dx < -1e-12) t = Math.min(t,(bx0-cx)/dx);+ if (dy > 1e-12) t = Math.min(t,(TW/2-holeCy)/dy); else if (dy < -1e-12) t = Math.min(t,(yPocT-holeCy)/dy);+ const ox = cx + t*dx, oy = holeCy + t*dy; // point on the rectangle edge+ tIn.push(addV(ix,iy,zTop, 0,0,1)); tOut.push(addV(ox,oy,zTop, 0,0,1)); // TOP face ring+ bIn.push(addV(ix,iy,zBot, 0,0,-1)); bOut.push(addV(ox,oy,zBot, 0,0,-1)); // BOTTOM face ring+ wT.push(addV(ix,iy,zTop, -dx,-dy,0)); wB.push(addV(ix,iy,zBot, -dx,-dy,0)); // hole WALL ring (inward normal)+ }+ for (let k=0;k<HSEG;k++){+ IDX.push(tIn[k], tIn[k+1], tOut[k], tOut[k], tIn[k+1], tOut[k+1]); // top face annulus+ IDX.push(bIn[k], bOut[k], bIn[k+1], bOut[k], bOut[k+1], bIn[k+1]); // bottom face annulus (reversed)+ IDX.push(wT[k], wB[k], wT[k+1], wT[k+1], wB[k], wB[k+1]); // cylindrical hole wall+ }+ });+ // outer edge walls around the strip rectangle [tMinX,tMaxX] x [yPocT,TW/2] — gives the tape edges thickness+ const edgeWall = (x0,y0,x1,y1,nx,ny) => { const a=addV(x0,y0,zTop,nx,ny,0), b=addV(x1,y1,zTop,nx,ny,0), c=addV(x0,y0,zBot,nx,ny,0), d=addV(x1,y1,zBot,nx,ny,0); IDX.push(a,c,b, b,c,d); };+ edgeWall(tMinX,TW/2, tMaxX,TW/2, 0,1); // top edge (free edge of the tape)+ edgeWall(tMaxX,yPocT, tMinX,yPocT, 0,-1); // bottom edge (meets the pocket band)+ edgeWall(tMinX,yPocT, tMinX,TW/2, -1,0); // left end+ edgeWall(tMaxX,TW/2, tMaxX,yPocT, 1,0); // right end+ const topStrip = B.MeshBuilder.CreateGround('tape_top', { width:1, height:1, subdivisions:1 }, scene);+ topStrip.setVerticesData('position', P);+ topStrip.setVerticesData('normal', NR);+ topStrip.setVerticesData('uv', new Array((P.length/3)*2).fill(0));+ topStrip.setIndices(IDX);+ topStrip.position = new B.Vector3(0,0,0); topStrip.material = tapeMat; topStrip.parent = tape; // coords baked into vertices+ if (topStrip.refreshBoundingInfo) topStrip.refreshBoundingInfo(); 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];@@ -261,6 +295,7 @@ 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; registerShadows(); // now that ALL geometry exists (model + tape/reel + nozzle + pocket LEDs + cover), enable shadows on everything+ if (fovCone) [fovCone].concat(fovCone.getChildMeshes ? fovCone.getChildMeshes() : []).forEach(m => { if ('receiveShadows' in m) m.receiveShadows = false; if (sg && sg.removeShadowCaster) sg.removeShadowCaster(m); }); // FOV cone: no shadows (belt-and-suspenders vs the /fov/ exclusion) // 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;