AnasSarkiz/muse-voice-remote
Muse Voice Remote — bare ESP32-S3R8, 155 TOP JLCPCB parts with real supplier CAD, native 3D assembly and GLB exports. Revised 17 mm case allocation; 0.30 mm via drill /0.45 mm pad. Checked prototype PCB; mechanical, RF and MUSE qualification pending.
- Version
- 1.3.2
- License
- unset
- Stars
- 0
design/cables.ts
import type { V3 } from "./enclosure"
// Mechanical harness studies, not replacement PCB parts or qualified harness
// MPNs. All points use front-surface Z in mm, exactly like the enclosure.
const add=(a:V3,b:V3):V3=>a.map((x,i)=>x+b[i]) as V3
const sub=(a:V3,b:V3):V3=>a.map((x,i)=>x-b[i]) as V3
const mul=(a:V3,s:number):V3=>a.map(x=>x*s) as V3
const dot=(a:V3,b:V3)=>a.reduce((s,x,i)=>s+x*b[i],0)
const cross=(a:V3,b:V3):V3=>[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]]
const norm=(a:V3):V3=>mul(a,1/Math.hypot(...a))
export const pathLength=(p:V3[])=>p.slice(1).reduce((s,v,i)=>s+Math.hypot(...sub(v,p[i])),0)
// Tangent circular fillets, with the actual reduced radius reported where
// short straight sections limit the requested bend. No sharp tube elbows.
function fillet(points:V3[],radius:number){
const result:V3[]=[points[0]],radii:number[]=[]
for(let i=1;i<points.length-1;i++){
const p=points[i],u=norm(sub(p,points[i-1])),v=norm(sub(points[i+1],p))
const theta=Math.acos(Math.max(-1,Math.min(1,dot(u,v))))
if(theta<1e-6){result.push(p);continue}
const t=Math.min(radius*Math.tan(theta/2),Math.hypot(...sub(p,points[i-1]))*.45,Math.hypot(...sub(points[i+1],p))*.45)
const r=t/Math.tan(theta/2),axis=norm(cross(u,v)),start=sub(p,mul(u,t)),centre=add(start,mul(cross(axis,u),r)),rad=sub(start,centre)
radii.push(r)
for(let k=0;k<=Math.ceil(theta/(Math.PI/16));k++){
const a=k*theta/Math.ceil(theta/(Math.PI/16))
result.push(add(centre,add(mul(rad,Math.cos(a)),mul(cross(axis,rad),Math.sin(a)))))
}
}
result.push(points.at(-1)!)
return {points:result.filter((p,i)=>!i||Math.hypot(...sub(p,result[i-1]))>1e-7),minimumBendRadius:Math.min(...radii)}
}
export type CableMesh={type:"createGeom3";polygons:{vertices:V3[]}[]}
// A watertight parallel-transport sweep. Native JSCAD createGeom3 avoids
// hundreds of overlapping cylinders and expensive boolean approximations.
function sweep(rings:V3[][]):CableMesh{
const polygons:{vertices:V3[]}[]=[{vertices:[...rings[0]].reverse()},{vertices:rings.at(-1)!}]
for(let i=0;i<rings.length-1;i++)for(let j=0;j<rings[i].length;j++){
const a=rings[i][j],b=rings[i][(j+1)%rings[i].length],c=rings[i+1][(j+1)%rings[i].length],d=rings[i+1][j]
polygons.push({vertices:[a,b,c]},{vertices:[a,c,d]})
}
return {type:"createGeom3",polygons}
}
function tube(points:V3[],r:number):CableMesh{
let normal:V3=[0,0,1]
const rings=points.map((p,i)=>{
const t=norm(sub(points[Math.min(i+1,points.length-1)],points[Math.max(0,i-1)]))
if(Math.abs(dot(t,normal))>.99)normal=[0,1,0]
normal=norm(sub(normal,mul(t,dot(normal,t))))
const b=cross(t,normal)
return Array.from({length:12},(_,j)=>add(p,mul(add(mul(normal,Math.cos(j*Math.PI/6)),mul(b,Math.sin(j*Math.PI/6))),r)))
})
return sweep(rings)
}
const lead=(name:string,label:string,net:string,connector:string,pin:number,target:string,diameter:number,
color:number[],waypoints:V3[],explode:number,opensWithRear=false)=>{
const route=fillet(waypoints,1.8)
return {name,label,kind:"cable" as const,net,connector,pin,target,diameter,color,explode,opensWithRear,
qualification:"proposed insulated lead; wire/crimp/pack termination and allowable bend radius require qualification",
centreline:route.points,minimum_bend_radius_mm:route.minimumBendRadius,length_mm:pathLength(route.points),
plan:tube(route.points,diameter/2)}
}
const battery=(i:number,net:string,color:number[])=>lead(`CABLE_BAT_${net}`,`Battery ${net} · proposed Ø1.0 mm insulated lead`,
net,"J_BAT",i+1,"ENV_BATTERY_UNSELECTED",1,color,[
[8.0,-15.9-i*2,8.65],[3.6+i*1.1,-15.9-i*2,8.65],[3.6+i*1.1,-26.8+i*1.1,8.65],
[28-i*1.1,-26.8+i*1.1,8.65],[28-i*1.1,-4,8.65],[28-i*1.1,1,13.4],
[28-i*1.1,12.2-i*2.2,13.4],[24.85,14.2-i*2.2,13.4]],17,true)
// Datasheet p.6: 26.7 mm flat extension, 45.9 mm fanout and 25.5 mm contact
// end. This is a folded clearance study, NOT a developed replacement FPC.
// Its 25.5 mm band is intentionally identified as a lower-bound envelope:
// the real fanout, stiffener, contacts and excess length still need folding.
const flexPoints:V3[]=Array.from({length:17},(_,i)=>{
const t=-Math.PI/2+i*Math.PI/16
return [34.65+1.6*Math.cos(t),3.5,5.15+1.6*Math.sin(t)]
})
flexPoints.push([26.5,3.5,6.75])
const flexRings=flexPoints.map((p,i)=>{
const t=norm(sub(flexPoints[Math.min(i+1,flexPoints.length-1)],flexPoints[Math.max(0,i-1)])),n:V3=[t[2],0,-t[0]]
return [[-12.75,-.075],[-12.75,.075],[12.75,.075],[12.75,-.075]].map(([y,z])=>add(p,add([0,y,0],mul(n,z))))
})
export const cableParts=[
battery(0,"BAT",[.82,.08,.06]),battery(1,"GND",[.025,.03,.035]),battery(2,"BAT_NTC",[.95,.75,.1]),
lead("CABLE_SPK_P","Speaker + · proposed insulated lead","SPK_P","J_SPK",1,"ENV_SPEAKER_UNSELECTED",.6,[.85,.12,.08],
[[-24.5,-17.96,6.2],[-28.5,-18.8,8.6],[-26.9,-18.8,13]],13),
lead("CABLE_SPK_N","Speaker − · bridge output, not ground","SPK_N","J_SPK",2,"ENV_SPEAKER_UNSELECTED",.6,[.035,.04,.045],
[[-23.5,-17.96,6.2],[-27.8,-20.4,8.6],[-26.9,-20.4,13]],13),
lead("CABLE_MOTOR_P","Motor + · proposed insulated lead","MOTOR_3V0","J_MOTOR",1,"ENV_MOTOR_UNSELECTED",.6,[.85,.1,.07],
[[15,-9.96,6.2],[15,-11.5,8.1],[21,-11.5,8.1],[21,-13.15,8.1],[21,-13.15,13],[23.6,-13.15,13]],13),
lead("CABLE_MOTOR_N","Motor switched return · proposed lead","MOTOR_LOW","J_MOTOR",2,"ENV_MOTOR_UNSELECTED",.6,[.07,.12,.2],
[[16,-9.96,6.2],[16,-10.5,8.1],[24.9,-10.5,8.1],[24.9,-13.15,8.1],[24.9,-13.15,13.8]],13),
{name:"CABLE_LCD_FLEX",label:"LCD flex · folded routing study, fit unresolved",kind:"flex" as const,connector:"J_LCD",target:"ENV_LCD_ER_TFT028A2_4",
color:[.85,.42,.07],explode:-16,opensWithRear:false,plan:sweep(flexRings),centreline:flexPoints,
width_mm:25.5,assumed_film_thickness_mm:.15,terminal_stiffener_mm:.3,available_flat_extension_mm:26.7,fanout_width_mm:45.9,
minimum_bend_radius_mm:1.6,length_mm:pathLength(flexPoints),
qualification:"Unresolved lower-bound fold study. Full 45.9 mm fanout, 0.30 mm stiffener, contact-side orientation and unused developed length are not qualified; not a production flex drawing."},
]