Devesh36/pocket-audio-player

A portable, rechargeable microSD audio player built around an RP2040, SPI flash, USB‑C charging/power management, LCD/button controls, PCM5102A I²S DAC, and TPA6132A headphone amplifier in a 3D‑printed enclosure.

Version
1.0.12
License
unset
Stars
0

scripts/export-enclosure.tsx

import { renderToJscadPlan } from "jscad-fiber/headless"
import { executeJscadOperations, resolveReferencePlanes } from "jscad-planner"
import modeling from "@jscad/modeling"
import { CaseBase, CaseLid, ButtonPlunger, RingButton, caseDimensions } from "../enclosure"

const m = modeling as any
const parts = [
  ["case-base", <CaseBase />], ["case-lid", <CaseLid />],
  ["button-plunger", <ButtonPlunger />], ["ring-button", <RingButton />],
] as const
const measurements: any[] = []
for (const [name, jsx] of parts) {
  const { geometry } = resolveReferencePlanes(renderToJscadPlan(jsx))
  const solid = executeJscadOperations(m, geometry)
  const bounds = m.measurements.measureBoundingBox(solid)
  const volume = m.measurements.measureVolume(solid)
  if (!(volume > 0)) throw new Error(`${name} has no printable volume`)
  // Normalize each print to Z=0. Lid/plunger are inverted for their flat face.
  let print = name === "case-base" ? solid : m.transforms.rotateX(Math.PI, solid)
  const pb = m.measurements.measureBoundingBox(print)
  print = m.transforms.translate([0, 0, -pb[0][2]], print)
  const polygons = m.geometries.geom3.toPolygons(m.modifiers.generalize({snap:false}, print))
  // CSG polygons can have T junctions. Preserve every boundary vertex and use
  // a face-centre fan, so adjacent STL faces share identical subdivided edges.
  const weld=(p:number[])=>p.map(n=>Math.round(n*1e5)/1e5)
  const allVertices=[...new Map<string,number[]>(polygons.flatMap((p:any)=>p.vertices.map((v:number[])=>{const w=weld(v);return [w.join(","),w] as [string,number[]]}))).values()]
  const lines = [`solid ${name}`]
  for (const polygon of polygons) {
    const original=polygon.vertices.map(weld), boundary:number[][]=[]
    for(let i=0;i<original.length;i++){
      const a=original[i],b=original[(i+1)%original.length],d=b.map((n:number,j:number)=>n-a[j]),length2=d.reduce((q:number,n:number)=>q+n*n,0)
      if(length2<1e-12)continue
      const onEdge=allVertices.map(v=>({v,t:v.reduce((q,n,j)=>q+(n-a[j])*d[j],0)/length2})).filter(({v,t})=>t>=-1e-8&&t<1-1e-8&&Math.hypot(...v.map((n,j)=>n-a[j]-t*d[j]))<0.00003).sort((a,b)=>a.t-b.t)
      boundary.push(...onEdge.map(p=>p.v))
    }
    const centre=weld([0,1,2].map(j=>boundary.reduce((q,v)=>q+v[j],0)/boundary.length))
    for(let i=0;i<boundary.length;i++){
      const a=centre,b=boundary[i],c=boundary[(i+1)%boundary.length]
      const u=b.map((n:number,j:number)=>n-a[j]),w=c.map((n:number,j:number)=>n-a[j])
      const n=[u[1]*w[2]-u[2]*w[1],u[2]*w[0]-u[0]*w[2],u[0]*w[1]-u[1]*w[0]],norm=Math.hypot(...n)
      if(norm<1e-10)continue
      lines.push(`facet normal ${n.map(k=>k/norm).join(" ")}`,"outer loop",... [a,b,c].map(p=>`vertex ${p.join(" ")}`),"endloop","endfacet")
    }
  }
  lines.push(`endsolid ${name}`)
  await Bun.write(`dist/enclosure/${name}.stl`, lines.join("\n"))
  await Bun.write(`dist/enclosure/${name}.jscad.json`, JSON.stringify(geometry,null,2))
  measurements.push({name, bounds, volumeMm3:volume, printBounds:m.measurements.measureBoundingBox(print), triangles:lines.filter(l=>l.startsWith("facet ")).length})
}
await Bun.write("dist/enclosure/dimensions.json", JSON.stringify({caseDimensions,parts:measurements},null,2))
console.log(JSON.stringify(measurements,null,2))