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