main
John Lauer adom-fields 0.7.17: the open PRs merged onto 0.7.16. Plated pad barrels and drill voids with wall coupling (5, 9), expl… df40420
#!/usr/bin/env python3
"""Export solved thermal surfaces to a GLB, using only Python's standard library.
Temperature is piecewise constant per cell. Axes are Z-up; GLB coordinates metres.
Color limits are explicit and shared across scenarios; no invented interior gradient.
"""
import argparse,json,struct
from pathlib import Path

def export(result,out,lo,hi):
    if hi<=lo:raise ValueError('color-max must exceed color-min')
    cells=result['cells']; groups=[[] for _ in result['bodies']]
    for f in result['exposedFaces']:groups[cells[f['cell']]['body']].append((f['cell'],f['axis'],f['positive']))
    for f in result['conductiveFaces']:
        a,b=f['from'],f['to']
        if cells[a]['body']!=cells[b]['body']:
            groups[cells[a]['body']].append((a,f['axis'],True));groups[cells[b]['body']].append((b,f['axis'],False))
    doc={'asset':{'version':'2.0','generator':'Adom Fields thermal3d'},'scene':0,'scenes':[{'nodes':[]}],'nodes':[],'meshes':[],'bufferViews':[],'accessors':[],'materials':[{'doubleSided':True,'pbrMetallicRoughness':{'metallicFactor':0,'roughnessFactor':1},'extensions':{'KHR_materials_unlit':{}}}],'extensionsUsed':['KHR_materials_unlit'],'extras':{'temperatureRangeC':[lo,hi],'coordinates':'metres, Z up','inputSha256':result.get('inputSha256'),'method':result['method']}}
    data=bytearray()
    def acc(values,kind):
        n=3;offset=len(data)
        for row in values:data.extend(struct.pack('<3f',*row))
        bv=len(doc['bufferViews']);doc['bufferViews'].append({'buffer':0,'byteOffset':offset,'byteLength':len(data)-offset})
        a={'bufferView':bv,'componentType':5126,'count':len(values),'type':'VEC3'}
        if kind=='position':a.update(min=[min(v[d] for v in values) for d in range(n)],max=[max(v[d] for v in values) for d in range(n)])
        idx=len(doc['accessors']);doc['accessors'].append(a);return idx
    for bi,faces in enumerate(groups):
        if not faces:continue
        positions=[];colors=[]
        for ci,d,positive in faces:
            c=cells[ci];bb=c['boundsMm'];u=(d+1)%3;v=(d+2)%3;pts=[]
            for x,y in [(0,0),(1,0),(1,1),(0,1)]:
                p=[0.,0.,0.];p[d]=bb[d+3] if positive else bb[d];p[u]=bb[u+3*x];p[v]=bb[v+3*y];pts.append([t/1000 for t in p])
            t=max(0,min(1,(c['temperatureC']-lo)/(hi-lo)))
            color=(min(1,2*t),min(1,2*(1-t)),1-t)
            order=(0,1,2,0,2,3) if positive else (0,2,1,0,3,2)
            for j in order:positions.append(pts[j]);colors.append(color)
        pa=acc(positions,'position');ca=acc(colors,'color');mi=len(doc['meshes'])
        name=result['bodies'][bi]['name'];doc['meshes'].append({'name':name,'primitives':[{'attributes':{'POSITION':pa,'COLOR_0':ca},'material':0}]});doc['nodes'].append({'name':name,'mesh':mi});doc['scenes'][0]['nodes'].append(mi)
    doc['buffers']=[{'byteLength':len(data)}];j=json.dumps(doc,separators=(',',':')).encode();j+=b' '*((-len(j))%4)
    raw=struct.pack('<III',0x46546c67,2,12+8+len(j)+8+len(data))+struct.pack('<II',len(j),0x4e4f534a)+j+struct.pack('<II',len(data),0x004e4942)+data
    Path(out).write_bytes(raw)
if __name__=='__main__':
    p=argparse.ArgumentParser();p.add_argument('result');p.add_argument('out');p.add_argument('--color-min',type=float,required=True);p.add_argument('--color-max',type=float,required=True);a=p.parse_args();export(json.loads(Path(a.result).read_text()),a.out,a.color_min,a.color_max)