Devesh36/key-finder

A compact BLE key finder for keys, bags and remotes: replaceable CR2032, piezo alert, status LED and pair button in a 38 mm black keyring enclosure. Includes routed PCB, enclosed/exploded assembly and BOM. Firmware/app and physical qualification remain separate.

Version
0.1.2
License
unset
Stars
0

scripts/export-mechanics.tsx

import {writeFileSync,mkdirSync,readFileSync} from 'node:fs'
import modeling from '@jscad/modeling'
import {executeJscadOperations} from 'jscad-planner'
import {renderToJscadPlan} from 'jscad-fiber/headless'
import {Base,Lid,Button,Lens,Cell,Piezo} from '../mechanics'
import {any_circuit_element} from 'circuit-json'
import {runAllChecks} from '@tscircuit/checks'
import type {Geom3} from '@jscad/modeling/src/geometries/types'
mkdirSync('mechanical',{recursive:true})
// Bridge the planner's generic kernel interface to JSCAD's overloaded TS declarations.
const kernel=modeling as unknown as Parameters<typeof executeJscadOperations>[0]
const solid=(node:React.ReactNode)=>executeJscadOperations(kernel,renderToJscadPlan(node) as Parameters<typeof executeJscadOperations>[1]) as Geom3
const generalize=modeling.modifiers.generalize as unknown as (options:{snap:boolean,triangulate:boolean},shape:Geom3)=>Geom3
const bodies={base:solid(<Base/>),lid:solid(<Lid/>),button:solid(<Button/>),lens:solid(<Lens/>),cell:solid(<Cell/>),piezo:solid(<Piezo/>)}
const parts=[]
for(const name of ['base','lid','button','lens'] as const){
 let shape=bodies[name]
 const bounds=modeling.measurements.measureBoundingBox(shape)
 const volume=modeling.measurements.measureVolume(shape)
 if(!(volume>0))throw new Error(`${name} has zero volume`)
 shape=modeling.transforms.translate([0,0,-bounds[0][2]],shape)
 shape=generalize({snap:true,triangulate:true},shape)
 const triangles=modeling.geometries.geom3.toPolygons(shape).flatMap(p=>p.vertices.slice(2).map((v,i)=>[p.vertices[0],p.vertices[i+1],v]))
 const bytes=new Uint8Array(84+50*triangles.length)
 bytes.set(new TextEncoder().encode(`Key Finder prototype ${name} / millimeters`))
 const data=new DataView(bytes.buffer);data.setUint32(80,triangles.length,true)
 let pos=84
 for(const [a,b,c] of triangles){
  const u=b.map((q,i)=>q-a[i]),v=c.map((q,i)=>q-a[i])
  const n=[u[1]*v[2]-u[2]*v[1],u[2]*v[0]-u[0]*v[2],u[0]*v[1]-u[1]*v[0]]
  const len=Math.hypot(...n)||1
  for(const q of [...n.map(x=>x/len),...a,...b,...c]){data.setFloat32(pos,q,true);pos+=4}
  data.setUint16(pos,0,true);pos+=2
 }
 writeFileSync(`mechanical/${name}.stl`,bytes)
 parts.push({name,bounds,volume,triangles:triangles.length})
}

const overlaps:{a:string,b:string,volume:number}[]=[]
const names=Object.keys(bodies) as (keyof typeof bodies)[]
for(let i=0;i<names.length;i++)for(let k=i+1;k<names.length;k++){
 const volume=Math.abs(modeling.measurements.measureVolume(modeling.booleans.intersect(bodies[names[i]],bodies[names[k]])))
 if(volume>0.01) overlaps.push({a:names[i],b:names[k],volume})
}
const report={parts,overlaps,scope:'Enclosure, cap, lens, cell and piezo solids. Electrical package fit and tolerances require physical checks; wire solder joints and cell contacts are intentional contacts.'}
console.log(JSON.stringify(report,null,2))
process.exitCode=overlaps.length?1:0