"""Publish explicitly authorized drawing-N distributions after rigid checks.""" import argparse,copy,json,math,os,subprocess,sys,tempfile,time from pathlib import Path root=Path(__file__).resolve().parents[1] sys.path.insert(0,str(root/'src/backend' if (root/'src/backend').is_dir() else root)) def run(job_id,node,apply=False): import server from cad_rebar_recipe import load_sources from cad_sheet_regions import source_regions from local_fabrication_shape import build_fabrication_local from quantity_body_ties import distribute,target_segments from body_tie_engagement import actual_bar_contacts from body_tie_geometry import source_body_tie_domain server.require_modeling_rules() if len(job_id)!=24 or any(c not in '0123456789abcdef' for c in job_id):raise ValueError('invalid job') path=Path('/tmp/drawing-model-studio-8097-jobs')/(job_id+'.json');raw=path.read_bytes();job=json.loads(raw);plan=job['result'] if job.get('status')=='running':raise ValueError('active job') sources=load_sources(job['sourceContext']['analysis']);inventory=plan['drawingEvidence']['cad_fabrication_inventory']['rows'] kernel=root/'model_kernel.js' if not kernel.is_file():kernel=root/'src/frontend/src/model_kernel.js' replacements={};reports=[] for old in plan['rebar_groups']: if not old.get('source_body_tie_binding') or old['fabrication_measurement'].get('count_pass'):continue report={'id':old['id'],'before':len(old['instance_paths_m']),'applied':False};reports.append(report) try: block=int(old['rebar_block'].replace('BLOCK','')) rows=[r for r in inventory if r.get('blocks')==[block] and r['material']['mark']==old['mark'] and r.get('fabrication_local')] if len(rows)!=1:raise ValueError('own source fabrication row not unique') row=rows[0];source=sources[row['source_sha256']] if row.get('source_region_id'): source=next(r for r in source_regions(source) if r.region_id==row['source_region_id']) detail=build_fabrication_local(source,row);expected=[math.dist(a,b) for a,b in zip(detail['points_m'],detail['points_m'][1:])] count=row['material']['count'];report['drawing']=count new=distribute(plan,old,count,authorized=True) for p in new['instance_paths_m']: lengths=[math.dist(a,b)for a,b in zip(p,p[1:])] if len(lengths)!=len(expected) or min(max(abs(a-b)for a,b in zip(lengths,order))for order in (expected,expected[::-1]))>.001: raise ValueError('fabrication leg length changed') script="const fs=require('fs');require(process.argv[1]);const g=JSON.parse(fs.readFileSync(0,'utf8'));process.stdout.write(JSON.stringify(g.instance_paths_m.map((p,i)=>ModelKernel.bendPath(p,g,i))));" bends=json.loads(subprocess.run([node,'-e',script,str(kernel)],input=json.dumps(new),capture_output=True,text=True,check=True).stdout) domain=source_body_tie_domain(plan,old['host_component_ids'][0]) outside,first=server.tube_path_outside_component_union(new['instance_paths_m'],new['diameter_mm'],domain,sample_step_m=.025) rounded_outside,rounded_first=server.tube_path_outside_component_union([b['path']for b in bends],new['diameter_mm'],domain,sample_step_m=.025) segments=target_segments(plan,new);rounded_contact=[] for b,record in zip(bends,new['quantity_distribution']['records']): witnesses=record['contact']['contacts'] rounded_contact.append(actual_bar_contacts(b['path'],segments,witnesses,new['diameter_mm'])) report.update(after=new['count'],outside=outside,rounded_outside=rounded_outside, rounded_contact_pass=sum(c['pass']for c in rounded_contact),warnings=sum(bool(b['warnings'])for b in bends)) if outside or rounded_outside or report['warnings'] or report['rounded_contact_pass']!=count:raise ValueError('geometry/contact validation failed') # Raw and rounded contact are independently retained; no approval is implied. new['quantity_distribution']['rounded_contact']=rounded_contact new['placement_geometry_check']={'pass':True,'tube_outside_samples':0,'rounded_tube_outside_samples':0,'scope':'own source concrete union; straight and viewer-rounded paths'} new['source_review_authorization']={'explicit_request':True,'scope':'drawing_quantity_distribution','export_allowed':False} new['placement_status']='quantity_distributed_feedback_pending_engineering_review' new['section_projection_check']={'pass':False,'reason':'user-authorized quantity distribution is not a literal drawing-station reconstruction'} replacements[old['id']]=new;report['applied']=apply except (ValueError,KeyError,StopIteration)as error:report['error']=str(error) print(json.dumps(report,ensure_ascii=False),flush=True) if apply and replacements: result=copy.deepcopy(plan);result['rebar_groups']=[replacements.get(g.get('id'),g)for g in result['rebar_groups']] preview=result.get('drawingEvidence',{}).get('preview_geometry') if isinstance(preview,dict):preview['rebar_groups']=copy.deepcopy(result['rebar_groups']) result['bodyTieQuantityDistribution']={'explicit_user_authorization':True,'reports':reports,'full_model_approved':False} result['geometryRevision']='quantity-distribution-'+str(time.time_ns()) job.update(result=result,partialResult=copy.deepcopy(result),geometryRevision=time.time_ns()) if path.read_bytes()!=raw:raise ValueError('job changed during validation') path.with_name(path.stem+'.before-tie-quantity-'+str(time.time_ns())+'.json').write_bytes(raw) with tempfile.NamedTemporaryFile(mode='w',dir=path.parent,delete=False,encoding='utf8')as out: json.dump(job,out,ensure_ascii=False,allow_nan=False);temp=out.name os.replace(temp,path) return reports if __name__=='__main__': parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('job');parser.add_argument('--node',required=True);parser.add_argument('--apply',action='store_true') args=parser.parse_args();run(args.job,args.node,args.apply)