muse/book-reading-clip-lamp
A tscircuit package created by muse.
- Version
- 0.4.6
- License
- unset
- Stars
- 0
route-base-service-wires.ts
/// <reference types="node" />
/** Reproducible static wire POSES, not an electrical/physical harness solver.
* 0.25 mm free-space grid from Manifold slices, inflated for insulation radius.
* Final export-enclosure.ts independently checks the continuous 3D solids.
*/
import Module from "./mechanical-tools/node_modules/manifold-3d"
import {baseRoutingSolids} from "./base-assembly-geometry"
import baseWireAnchors from "./base-wire-anchors.json"
import {connectorPort,splicePort} from "./base-service-connectors"
import {box,type Plan,type V3} from "./mechanical-primitives"
import {writeFileSync} from "node:fs"
const kernel=await Module();kernel.setup();const M=kernel.Manifold
const evaluate=(p:Plan):any=>{switch(p.type){case"cuboid":return M.cube(p.size,true);case"cylinder":return M.cylinder(p.height,p.radius,p.radius,p.segments??96,true);case"translate":return evaluate(p.shape).translate(p.vector);case"rotate":return evaluate(p.shape).rotate(p.angles.map((n:number)=>n*180/Math.PI));case"colorize":return evaluate(p.shape);case"union":return M.union(p.shapes.map(evaluate));case"subtract":return evaluate(p.shapes[0]).subtract(M.union(p.shapes.slice(1).map(evaluate)));case"extrudeLinear":return new kernel.CrossSection(p.shape.points).extrude(p.options.height);default:throw new Error(p.type)}}
const step=.25,lo:V3=[-28.75,-199.5,-199.5],dims=[231,345,139],N=dims[0]*dims[1]*dims[2],plane=dims[0]*dims[1]
const idx=(x:number,y:number,z:number)=>x+y*dims[0]+z*plane
const xyz=(n:number):V3=>{const z=Math.floor(n/plane),y=Math.floor((n-z*plane)/dims[0]);return [n%dims[0],y,z]}
const world=(n:number):V3=>xyz(n).map((v,i)=>lo[i]+v*step) as V3
const near=(p:V3)=>idx(...p.map((v,i)=>Math.round((v-lo[i])/step)) as V3)
const solids=baseRoutingSolids.filter(p=>!['head-harness-plug'].includes(p.id)).map(p=>evaluate(p.plan))
// Thermistor has factory leads: no custom solder/heatshrink splice obstacles.
const obstacle=M.union(solids).intersect(evaluate(box([64,94,40],[0,-157,-181])))
const masks=new Map<number,Uint8Array>()
function maskFor(radius:number) {
if(masks.has(radius))return masks.get(radius)!
console.log(`Voxelizing static clearance ${radius} mm`)
const mask=new Uint8Array(N),r=radius+.04
for(let z=0;z<dims[2];z++) {
const zz=lo[2]+z*step
const section=kernel.CrossSection.union([-r,0,r].map(d=>obstacle.slice(zz+d))).offset(r)
const rows:number[][]=Array.from({length:dims[1]},()=>[])
for(const poly of section.toPolygons())for(let e=0;e<poly.length;e++) {
const a=poly[e],b=poly[(e+1)%poly.length];if(a[1]===b[1])continue
const y0=Math.max(0,Math.ceil((Math.min(a[1],b[1])-lo[1])/step)), y1=Math.min(dims[1]-1,Math.ceil((Math.max(a[1],b[1])-lo[1])/step)-1)
for(let y=y0;y<=y1;y++){const yy=lo[1]+y*step;rows[y].push(a[0]+(yy-a[1])*(b[0]-a[0])/(b[1]-a[1]))}
}
rows.forEach((xs,y)=>{xs.sort((a,b)=>a-b);for(let i=0;i+1<xs.length;i+=2){const x0=Math.max(0,Math.ceil((xs[i]-lo[0])/step)),x1=Math.min(dims[0]-1,Math.floor((xs[i+1]-lo[0])/step));if(x1>=x0)mask.fill(1,idx(x0,y,z),idx(x1,y,z)+1)}})
section.delete?.()
}
masks.set(radius,mask);return mask
}
type Job={key:string,i:number,r:number,start:V3[],end:V3[],path?:V3[]}
const original=(baseWireAnchors as V3[]).map(p=>[p])
const map=[["battery-service-connector",0],["battery-service-connector",1],["battery-service-connector",2],["battery-service-connector",3],["speaker-service-connector",0],["speaker-service-connector",1],["chat-service-connector",0],["power-service-connector",0]] as const
const endFor=(port:V3,rot=false):V3[]=>[[port[0]+(rot?1.25:0),port[1]+(rot?0:1.25),port[2]],port]
const jobs:Job[]=map.map(([c,n],i)=>{const a=original[i][0] as V3;return {key:"head",i,r:i<2?.55:.45,start:[[a[0],a[1],-169],a,[a[0],a[1],-176.5]],end:endFor(connectorPort(c,n,"head"),i>=6)}})
for(const [key,x,c] of [["chat",-15,"chat-service-connector"],["power",15,"power-service-connector"]] as const)for(let i=0;i<2;i++)jobs.push({key,i,r:.65,start:[[x-3+6*i,-126,-196.8],[x-3+6*i,-126,-198]],end:endFor(connectorPort(c,i,"load"),i===0)})
for(let i=0;i<2;i++)jobs.push({key:"ntc",i,r:.9,start:[[-23+2*i,-147.5,-195],[-23+2*i,-145.5,-195]],end:endFor(splicePort(i?"ntc-return-splice":"ntc-splice",1))})
for(let i=0;i<2;i++) {
const z=-195-2*i, x=-3+2*i
const arc:V3[]=Array.from({length:25},(_,k)=>{const a=Math.PI*(1-k/24);return [x+3*Math.cos(a),-121+3*Math.sin(a),z]})
jobs.push({key:"battery",i,r:.5,start:[[-7+i*4,-132,-195],[x-3,-130.75,z],...arc,[x+3,-130.75,z]],end:[[-24.5+i*2,-173,-195],[-24.5+i*2,-174.1,-195]]})
}
for(let i=0;i<2;i++)jobs.push({key:"extension",i,r:.45,start:[[-24.5+i*2,-188,-195],[-24.5+i*2,-189.2,-195]],end:endFor(connectorPort("battery-service-connector",i,"load"))})
const distSegment=(p:V3,a:V3,b:V3)=>{const ab=b.map((v,i)=>v-a[i]),ap=p.map((v,i)=>v-a[i]);let t=ap.reduce((n,v,i)=>n+v*ab[i],0)/ab.reduce((n,v)=>n+v*v,0);t=Math.max(0,Math.min(1,t));return Math.hypot(...ap.map((v,i)=>v-t*ab[i]))}
function paint(mask:Uint8Array,a:V3,b:V3,r:number) {
const lows=a.map((v,i)=>Math.max(0,Math.floor((Math.min(v,b[i])-r-lo[i])/step))), highs=a.map((v,i)=>Math.min(dims[i]-1,Math.ceil((Math.max(v,b[i])+r-lo[i])/step)))
for(let z=lows[2];z<=highs[2];z++)for(let y=lows[1];y<=highs[1];y++)for(let x=lows[0];x<=highs[0];x++){const n=idx(x,y,z);if(!mask[n]&&distSegment([lo[0]+x*step,lo[1]+y*step,lo[2]+z*step],a,b)<r)mask[n]=1}
}
class Heap{a:{n:number,f:number}[]=[];push(n:number,f:number){const v={n,f};let i=this.a.length;this.a.push(v);while(i){const p=(i-1)>>1;if(this.a[p].f<=f)break;this.a[i]=this.a[p];i=p}this.a[i]=v}pop(){const v=this.a[0],t=this.a.pop()!;if(this.a.length){let i=0;while(2*i+1<this.a.length){let c=2*i+1;if(c+1<this.a.length&&this.a[c+1].f<this.a[c].f)c++;if(this.a[c].f>=t.f)break;this.a[i]=this.a[c];i=c}this.a[i]=t}return v}}
const gs=new Float32Array(N),prev=new Int32Array(N),closed=new Uint8Array(N)
function clean(p:V3[]){let out:V3[]=[];for(const v of p){if(out.length&&Math.hypot(...v.map((n,j)=>n-out[out.length-1][j]))<1e-7)continue;while(out.length>=2){const a=out[out.length-2],b=out[out.length-1],u=b.map((n,j)=>n-a[j]),w=v.map((n,j)=>n-b[j]);const cross=[u[1]*w[2]-u[2]*w[1],u[2]*w[0]-u[0]*w[2],u[0]*w[1]-u[1]*w[0]];if(Math.hypot(...cross)>1e-8||u.reduce((n,v,j)=>n+v*w[j],0)<0)break;out.pop()}out.push(v)}return out}
// Constrained starts/ends for every other job are reserved before planning.
for(const job of jobs) {
const mask=new Uint8Array(maskFor(job.r))
for(const other of jobs)if(other!==job){const sets=other.path?[other.path]:[other.start,other.end];for(const path of sets)for(let i=1;i<path.length;i++)paint(mask,path[i-1],path[i],job.r+other.r+.04)}
const choose=(p:V3)=>{const center=xyz(near(p));const candidates:{n:number,d:number}[]=[];for(let dz=-3;dz<=3;dz++)for(let dy=-3;dy<=3;dy++)for(let dx=-3;dx<=3;dx++){const q=[center[0]+dx,center[1]+dy,center[2]+dz];if(q.some((v,i)=>v<0||v>=dims[i]))continue;const n=idx(q[0],q[1],q[2]);if(!mask[n])candidates.push({n,d:Math.hypot(...world(n).map((v,i)=>v-p[i]))})}candidates.sort((a,b)=>a.d-b.d);if(!candidates.length)throw new Error(`Blocked terminal ${job.key}.${job.i}: ${JSON.stringify(p)}`);return candidates[0].n}
const s=choose(job.start[job.start.length-1]),t=choose(job.end[0]),target=xyz(t)
gs.fill(Infinity);prev.fill(-1);closed.fill(0);gs[s]=0;const heap=new Heap();heap.push(s,0);let count=0
const h=(n:number)=>xyz(n).reduce((a,v,i)=>a+Math.abs(v-target[i]),0)*1.015
while(heap.a.length){const {n}=heap.pop();if(closed[n])continue;if(n===t)break;closed[n]=1;count++;const [x,y,z]=xyz(n);const ns=[x? n-1:-1,x<dims[0]-1?n+1:-1,y?n-dims[0]:-1,y<dims[1]-1?n+dims[0]:-1,z?n-plane:-1,z<dims[2]-1?n+plane:-1];for(const k of ns){if(k<0||mask[k]||closed[k])continue;const g=gs[n]+1;if(g>=gs[k])continue;gs[k]=g;prev[k]=n;heap.push(k,g+h(k))}if(count>3e6)throw new Error("Search budget")}
if(!Number.isFinite(gs[t]))throw new Error(`No route ${job.key}.${job.i}`)
const route:V3[]=[];for(let k=t;k!==-1;k=prev[k])route.push(world(k));route.reverse();job.path=clean([...job.start,...route,...job.end]);console.log(`${job.key}.${job.i} ${count} expansions, ${job.path.length} bends`)
}
// Preserve the battery's nominal 100 mm factory leads without cutting them.
// Shift only the two front service-loop legs; retain an assumed 3 mm bend R.
for(const job of jobs.filter(j=>j.key==="battery")) {
const p=job.path!,length=p.slice(1).reduce((a,b,i)=>a+Math.hypot(...b.map((n,j)=>n-p[i][j])),0),delta=(100-length)/2
for(const v of p)if(v[1]>=-121-1e-8 && Math.abs(v[2]-(-195-2*job.i))<1e-7)v[1]+=delta
console.log(`Battery ${job.i} loop adjustment ${delta.toFixed(3)} mm`)
}
const result:Record<string,V3[][]>={};for(const job of jobs)(result[job.key]??=[])[job.i]=job.key==="head"?[original[job.i][0],...job.path!.slice(1)]:job.path!
writeFileSync("harness-base-paths.json",JSON.stringify(result.head,null,2)+"\n");delete result.head
writeFileSync("base-catalog-wires.json",JSON.stringify(result,null,2)+"\n")