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

AravK ·4cd2855047 ·3mo ago ·parent 3f34a71
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) &amp;&amp; !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 &amp;&amp; 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;