"""Regenerate only source-bound approach ties; preserve concrete and other bars.""" import argparse import copy import hashlib import json import os from pathlib import Path import sys import tempfile import time root=Path(__file__).resolve().parents[1] sys.path.insert(0,str(root/'src'/'backend' if (root/'src'/'backend').is_dir() else root)) from cad_rebar_recipe import DrawingSource from local_detail_discovery import bind_fabrication_inputs from local_approach_ties import generate_approach_tie_groups def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('job',type=Path) parser.add_argument('--cad',type=Path,nargs='+',required=True) parser.add_argument('--apply',action='store_true') parser.add_argument('--output',type=Path) args=parser.parse_args();raw=args.job.read_bytes();job=json.loads(raw) if job.get('status')not in ('completed','awaiting_feedback','failed'): raise ValueError('active job cannot be changed') source_list=[DrawingSource(p.read_bytes())for p in args.cad] sources={s.sha256:s for s in source_list};working=copy.deepcopy(job['result']) inventory=bind_fabrication_inputs(working,sources) groups,failures=generate_approach_tie_groups(working,sources,inventory) result=copy.deepcopy(job['result']);replaced={g['id']for g in groups} if any(g.get('id')in replaced and not g.get('source_approach_tie_binding')for g in result['rebar_groups']): raise ValueError('replacement overlaps another assembly') retained=[g for g in result['rebar_groups']if g.get('id')not in replaced] result['rebar_groups']=[*retained,*groups] assert result['concrete_components']==job['result']['concrete_components'] assert result['rebar_groups'][:len(retained)]==retained summary={'groups':len(groups),'bars':sum(g['count']for g in groups),'failures':failures, 'rows':[{'host':g['host_component_ids'][0],'mark':g['mark'],'count':g['count'], 'spacing_mm':g['spacing_mm'],'tube_outside_samples':g['placement_geometry_check']['tube_outside_samples']} for g in groups],'concrete_and_other_bars_preserved':True} result['drawingEvidence']['source_approach_tie_audit']=summary result['drawingEvidence'].setdefault('preview_geometry',{})['rebar_groups']=copy.deepcopy(result['rebar_groups']) print(json.dumps(summary,ensure_ascii=False),flush=True) if args.output: with args.output.open('x')as stream:json.dump({'summary':summary,'result':result},stream,ensure_ascii=False,allow_nan=False) if not args.apply or not groups:return if args.job.read_bytes()!=raw:raise ValueError('concurrent job update') backup=args.job.with_name(args.job.stem+f'.before-approach-ties-{time.time_ns()}.json') with backup.open('xb')as stream:stream.write(raw) job.update(result=result,partialResult=copy.deepcopy(result),geometryRevision=time.time_ns()) with tempfile.NamedTemporaryFile(mode='w',dir=args.job.parent,delete=False)as stream: json.dump(job,stream,ensure_ascii=False,allow_nan=False);temporary=stream.name if args.job.read_bytes()!=raw:raise ValueError('concurrent job update before publish') os.replace(temporary,args.job.resolve()) print(json.dumps({'saved':True,'backup':str(backup)}),flush=True) if __name__=='__main__':main()