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
scripts/place-imported.ts
// Adjust the supporting parts around the requested assembly using real imported
// pad/courtyard bounds. This is a 2D placement check, not enclosure approval.
import { readFileSync, writeFileSync } from "node:fs"
import type { AnyCircuitElement } from "circuit-json"
import { chipFixedNames } from "../design/chip"
if (!process.argv.includes("--repack")) {
throw new Error("Placement is frozen for checked routing. Explicit --repack invalidates copper and requires a complete reroute and native DRC.")
}
const json: AnyCircuitElement[] = JSON.parse(readFileSync("dist/circuit.json","utf8"))
const components = json.filter(e => e.type === "source_component")
const placed = json.filter(e => e.type === "pcb_component")
type Box = { name:string; x:number; y:number; left:number; right:number; bottom:number; top:number }
const bounds: Box[] = placed.map(p => {
const points: {x:number;y:number}[] = []
for (const e of json) {
if (!("pcb_component_id" in e) || e.pcb_component_id !== p.pcb_component_id) continue
if (e.type === "pcb_courtyard_outline") points.push(...e.outline)
if (e.type === "pcb_courtyard_rect") points.push(
{x:e.center.x-e.width/2,y:e.center.y-e.height/2},
{x:e.center.x+e.width/2,y:e.center.y+e.height/2})
if (e.type === "pcb_smtpad") {
if (e.shape === "polygon") { points.push(...e.points); continue }
const w = "width" in e ? e.width : "radius" in e ? e.radius*2 : 0
const h = "height" in e ? e.height : "radius" in e ? e.radius*2 : 0
points.push({x:e.x-w/2,y:e.y-h/2},{x:e.x+w/2,y:e.y+h/2})
}
if (e.type === "pcb_plated_hole") {
const w = "outer_diameter" in e ? e.outer_diameter : "outer_width" in e ? e.outer_width : 0
const h = "outer_diameter" in e ? e.outer_diameter : "outer_height" in e ? e.outer_height : 0
points.push({x:e.x-w/2,y:e.y-h/2},{x:e.x+w/2,y:e.y+h/2})
}
}
const x=Number(p.display_offset_x ?? p.center.x); const y=Number(p.display_offset_y ?? p.center.y)
return { name:components.find(c => c.source_component_id===p.source_component_id)!.name, x,y,
left:Math.min(...points.map(p=>p.x))-x, right:Math.max(...points.map(p=>p.x))-x,
bottom:Math.min(...points.map(p=>p.y))-y, top:Math.max(...points.map(p=>p.y))-y }
})
const fixedNames = new Set([...chipFixedNames,"MIC1","MIC2","SW_TALK","SW_PRIVACY","SW_POWER","J_USB","J_LCD","J_BAT","LED_R","LED_G","LED_B","D_USB_ESD"])
const fixed = bounds.filter(b=>fixedNames.has(b.name))
const fits = (a: Box, b: Box) => a.x+a.right+0.25<=b.x+b.left || b.x+b.right+0.25<=a.x+a.left ||
a.y+a.top+0.25<=b.y+b.bottom || b.y+b.top+0.25<=a.y+a.bottom
for (const a of fixed) for (const b of fixed) if (a!==b && !fits(a,b)) throw new Error(`Fixed allocation conflict: ${a.name}/${b.name}`)
const occupied = [...fixed]
// Reserve the seeded crystal routes from unrelated supporting components.
occupied.push({name:"clock_vertical",x:2.4,y:8.8,left:-0.25,right:0.25,bottom:-2.8,top:2.8})
occupied.push({name:"clock_horizontal",x:0.5,y:6.25,left:-2.1,right:2.15,bottom:-0.25,top:0.25})
for (const x of [-31.5,31.5]) for (const y of [-22,22]) occupied.push({name:"mount",x,y,left:-1.8,right:1.8,bottom:-1.8,top:1.8})
// Electrical groups precede general packing. Never optimize a converter's
// capacitors independently of its IC and inductor merely to fit larger parts.
const groups: Record<string,[number,number]> = {
R_LED_R:[6.5,21],R_LED_G:[10,21],R_LED_B:[13.5,21],
U_REG:[-23,-4.5],L_REG:[-28,-4.5],C_REG_IN:[-24,-8],C_REG_OUT1:[-23,-1],C_REG_OUT2:[-20,-1],R_REG_EN:[-19,-4.5],
U_CHARGE:[5.5,-12],C_CHG_IN:[6,-8.5],C_CHG_OUT:[9,-10.5],C_CHG_BAT:[1.5,-14.5],R_ISET:[1.5,-10.5],R_ILIM:[9,-8.5],R_ITERM:[5,-15.5],R_TMR:[12,-7],R_CHG:[7,-16.5],R_PGOOD:[-10,-18],
J_BAT:[13,-9],
U_BACKLIGHT:[-19.5,7],L_BL:[-24.5,7],D_BL:[-24.5,11.5],C_BL_IN:[-16,7.5],C_BL_OUT:[-24.5,15],R_BL_FB:[-20,4],Q_BL_OFF:[-19,1],Q_BL_DRIVE:[-19,-3],R_BL_GATE_PD:[-16,0],R_BL_GATE:[-16,-3],R_BL_BASE_PD:[-16,-5],R_BL_PWM_PD:[-18,7],
U_AMP:[-19,-11],C_AMP1:[-14.5,-13],C_AMP2:[-16,-11],R_AMP_PD:[-22,-11],J_SPK:[-24,-15],
U_MOTOR_POWER:[26,-12],C_MOTOR_IN:[23,-12],C_MOTOR:[29,-12],Q_MOTOR:[26,-16],D_FLYBACK:[22,-16],R_MOTOR_GATE:[29,-16],R_MOTOR_PD:[29,-18],J_MOTOR:[25,-20.5],
D_USB_ESD:[0,-16],F_USB:[5,-19.5],C_USB:[-10,-15],R_CC1:[-5,-19],R_CC2:[-5,-21],R_USB_DM:[5,-6.5],R_USB_DP:[7,-6.5],R_USB_EN1_PD:[-12,-6],R_USB_EN2_PD:[5,7],
L_ANA:[-7,-1],C_RF_BULK:[-9.5,1.5],C_ANA1:[-5,-0.25],C_ANA2:[-5,2],C_ANA3:[-6.5,2.5],C_VDDA1:[-5.75,6.55],C_VDDA2:[-4,6.55],C_CPU:[5,6.5],C_SPI1:[5,-.5],C_SPI2:[5,-3],C_MCU1:[-.6,-3.25],C_MCU2:[-7,-3],C_FLASH:[10,-4.5],R_VDD_SPI_EXT:[2.5,-6.5],
U_ISO1:[-23,17],C_ISO1:[-23,14],U_ISO2:[-18,17],C_ISO2:[-18,14],U_ISO3:[-13,17],C_ISO3:[-13,14],C_MIC1:[-28,19],C_MIC2:[28,19],C_MIC_BULK:[-28,16],
U_LCD_POWER:[27,11],C_LCD_IN:[29,8],C_LCD_OUT:[27,14],C_LCD_PANEL:[26,17],C_LCD1:[29,11],C_LCD2:[29,15],
U_LCD_BUF1:[15,18],C_LCD_BUF1:[18,18],U_LCD_BUF2:[15,14],C_LCD_BUF2:[18,14],U_LCD_BUF3:[15,10],C_LCD_BUF3:[18,10],U_LCD_BUF4:[15,6],C_LCD_BUF4:[18,6],U_LCD_BUF5:[15,2],C_LCD_BUF5:[18,2],
R_BL_BALANCE1:[27,-6],R_BL_BALANCE2:[27,-4],R_BL_BALANCE3:[27,-2],R_BL_BALANCE4:[27,0],
}
const priority=Object.keys(groups)
const support = bounds.filter(b=>!fixedNames.has(b.name)).sort((a,b)=> {
const ia=priority.indexOf(a.name),ib=priority.indexOf(b.name)
if(ia>=0 || ib>=0)return (ia<0?10000:ia)-(ib<0?10000:ib)
return (b.right-b.left)*(b.top-b.bottom)-(a.right-a.left)*(a.top-a.bottom)
})
for (const part of support) {
const candidates: { x:number;y:number;cost:number }[] = []
for (let x=-32.5;x<=32.5;x+=0.5) for (let y=-23.5;y<=23.5;y+=0.5) {
const moved = {...part,x,y}
if (x+part.left< -32.6 || x+part.right>32.6 || y+part.bottom< -23.6 || y+part.top>23.6) continue
if (occupied.some(b=>!fits(moved,b))) continue
const target=groups[part.name] ?? [part.x,part.y]
candidates.push({x,y,cost:(x-target[0])**2+(y-target[1])**2})
}
candidates.sort((a,b)=>a.cost-b.cost)
if (!candidates.length) throw new Error(`No legal position for ${part.name}`)
occupied.push({...part,...candidates[0]})
}
const output = Object.fromEntries(occupied.filter(b=>bounds.some(p=>p.name===b.name)).map(b=>[b.name,{x:b.x,y:b.y}]))
writeFileSync("design/placement-overrides.json",JSON.stringify(output,null,2)+"\n")
console.log(`Placed ${bounds.length} imported components with courtyard separation; mounting posts and the real LCD connector reserved.`)