- 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>
94 lines
5.9 KiB
Python
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()
|