Files
Pier_v1/RAG/design-model-box-sample_2/tools/publish_body_tie_pose.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:20:18 +09:00

94 lines
5.9 KiB
Python

"""Publish a fresh-source body tie candidate only if its change is rigid pose."""
import argparse
import copy
import json
import math
import os
from pathlib import Path
import tempfile
import time
import sys
root=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(root/'src/backend'if (root/'src/backend').is_dir()else root))
from server import component_surface_z_at_xy
from mesh_surface_aliases import prism_surface_view
def merge_pose(current, candidate):
result=copy.deepcopy(current)
old={g['id']:g for g in current['rebar_groups']if g.get('source_body_tie_binding')}
new={g['id']:g for g in candidate['rebar_groups']if g.get('source_body_tie_binding')}
if old.keys()!=new.keys():raise ValueError('body tie occurrences changed')
if current['concrete_components']!=candidate['concrete_components']:raise ValueError('concrete changed')
changed=[];removed=0
hosts={c['id']:prism_surface_view(c)for c in current['concrete_components']}
station_key=lambda s:json.dumps([s['station_m'],s['domain'],s['section_projection']],sort_keys=True)
for key,g in old.items():
n=new[key]
for field in ['diameter_mm','unit_length_m','host_component_ids']:
if g[field]!=n[field]:raise ValueError('not a pose-only change: '+field)
for field in ['section','host_role']:
if g['source_body_tie_binding'][field]!=n['source_body_tie_binding'][field]:
raise ValueError('source station binding changed: '+field)
binding=n['source_body_tie_binding']
if not binding['pose_rule'].startswith('body-source-wrap-v5:'):raise ValueError('unexpected pose rule')
previous={station_key(s):(p,i)for i,(s,p)in enumerate(zip(g['source_body_tie_binding']['stations'],g['instance_paths_m']))}
following={station_key(s)for s in binding['stations']}
excluded={station_key(s)for s in binding.get('excluded_unpaired_candidates',[])
if s['cycle_pair'].get('outside_pair_domain')and not s['cycle_pair']['pass']}
if following-set(previous)or set(previous)-following-excluded:raise ValueError('unexplained station addition/removal')
if n['count']!=len(n['instance_paths_m'])or len(binding['stations'])!=n['count']:raise ValueError('count metadata')
if len(binding['engagement'])!=n['count']:raise ValueError('missing per-bar audit')
if n['occurrence_instance_ids']!=[n['host_component_ids'][0]]*n['count']:raise ValueError('occurrence mismatch')
removed+=len(previous)-len(following)
for s,q,e in zip(binding['stations'],n['instance_paths_m'],binding['engagement']):
p,old_index=previous[station_key(s)]
if len(p)!=len(q):raise ValueError('detail topology changed')
for i in range(len(p)):
for j in range(i):
if abs(math.dist(p[i],p[j])-math.dist(q[i],q[j]))>1e-8:
raise ValueError('non-rigid fabrication change')
centre=lambda path:[(min(v[i]for v in path)+max(v[i]for v in path))/2 for i in range(3)]
old_c=centre(p);new_c=centre(q)
if math.dist(old_c,new_c)>1e-7:
pair=e['cycle_pair']
if binding['axis_proof']['axis']!='return_legs':raise ValueError('open-U moved')
anchor=pair.get('centre_station_m',s['station_m'])
if abs(new_c[0]-s['portal_slope']*new_c[1]-anchor)>1e-7 or abs(new_c[1]-old_c[1])>1e-7:
raise ValueError('not the authored pair centre')
host=hosts[n['host_component_ids'][0]]
old_z=component_surface_z_at_xy(host,'top_z_m',*old_c[:2]);new_z=component_surface_z_at_xy(host,'top_z_m',*new_c[:2])
old_e=g['source_body_tie_binding'].get('engagement',[])
shift_keys=('diameter_face_centre_shift_m','source_model_centre_shift_m')
old_shift=sum(old_e[old_index]['cycle_pair'].get(k,0)for k in shift_keys)if old_e else 0
shift=sum(pair.get(k,0)for k in shift_keys)-old_shift
if old_z is None or new_z is None or abs(new_c[2]-old_c[2]-(new_z-old_z)-shift)>1e-6:
raise ValueError('unexplained height shift')
if g['instance_paths_m']!=n['instance_paths_m']:changed.append(key)
result['rebar_groups']=[copy.deepcopy(new[g['id']])if g['id']in new else g for g in result['rebar_groups']]
result['drawingEvidence'].setdefault('preview_geometry',{})['rebar_groups']=copy.deepcopy(result['rebar_groups'])
result['drawingEvidence']['body_tie_pose_audit']={'rule':'body-source-wrap-v5','changed_groups':changed,
'concrete_and_other_rebar_preserved':True,'detail_lengths_preserved':True,
'excluded_unpaired_end_candidates':removed,'centres_bound_to_authored_cycle_pairs':True,
'engineering_approval':False}
return result
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('job',type=Path);p.add_argument('candidate',type=Path)
args=p.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')
result=merge_pose(job['result'],json.loads(args.candidate.read_text())['result'])
if args.job.read_bytes()!=raw:raise ValueError('concurrent job update')
backup=args.job.with_name(args.job.stem+f'.before-body-face-return-{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.resolve().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({'backup':str(backup),'audit':result['drawingEvidence']['body_tie_pose_audit']},ensure_ascii=False))
if __name__=='__main__':main()