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>
This commit is contained in:
ahnyongjooandClaude Sonnet 5.5 committed 2026-10-28 11:20:18 +09:00
commit 964a841de1
3978 files changed
+58013356

No files matched your search

@@ -0,0 +1,140 @@
"""Current-drawing dowel detail inventory, separate from body D-mark bars.
This module imports source evidence; it never infers a helix, an installation
phase, a bearing gap, or a host transform from a familiar mark or note.
"""
import copy
import math
import re
from cad_accessory_detail import detail_role, installed_detail_candidates
from cad_standard_section import _text_anchor
from local_assembly_discovery import selector
def parse_note(text):
text=re.sub(r'\s+','',text).upper()
bar=re.fullmatch(r'(?:H|S|D|Ø|Φ|⌀)(\d+(?:\.\d+)?)@(\d+(?:\.\d+)?)(?:MM)?',text)
if bar and min(float(v)for v in bar.groups())>0:
return {'kind':'dowel_bar_pitch','diameter_mm':float(bar[1]),'pitch_mm':float(bar[2])}
pipe=re.fullmatch(r'(?:Ø|Φ|⌀)(\d+(?:\.\d+)?)[X×](\d+(?:\.\d+)?)(?:MM)?',text)
if pipe and min(float(v)for v in pipe.groups())>0:
return {'kind':'pipe_dimensions','diameter_mm':float(pipe[1]),'length_mm':float(pipe[2])}
thick=re.fullmatch(r'T=(\d+(?:\.\d+)?)MM',text)
if thick and float(thick[1])>0:return {'kind':'layer_thickness','thickness_mm':float(thick[1])}
layers=re.fullmatch(r'(\d+)-?LAYERS?',text)
if layers and int(layers[1])>0:return {'kind':'layer_count','layers':int(layers[1])}
return None
def outlined_straight_bar(source, row, note, interfaces):
"""A drawn cylinder outline is not a bent four-leg bar.
Resolve its centreline only when its width is the annotated diameter,
its length is the original fabrication L, and the *dowel note leader*
reaches that actual closed outline. A nearby pipe cannot qualify.
"""
from local_assembly_discovery import leader_targets
shape=row.get('fabrication_local')or{}
if len(shape.get('points_m',[]))!=2:return []
material=row['material'];length=shape['length_m'];diameter=material['diameter_mm']/1000
if abs(note['diameter_mm']-material['diameter_mm'])>1e-6:return []
labels=[source.entity(r)for r in note['label_candidates']if re.search('다[웰월]바|DOWEL',r['text'],re.I)]
if len(labels)!=1:return []
label=labels[0];x,y=_text_anchor(label);h=label.get('text_height')or 0
lines=[e for e in source.entities if e.get('type')=='LINE' and len(e.get('points',[]))==2]
underlines=[e for e in lines if any(tag in e.get('layer','').upper()for tag in ('LEAL','DIM'))
and abs(e['points'][0][1]-e['points'][1][1])<1e-6
and 0<=y-e['points'][0][1]<=h
and min(p[0]for p in e['points'])-h<=x<=max(p[0]for p in e['points'])]
if len(underlines)!=1:return []
try:targets,leaders=leader_targets(source,label,1.,underline=underlines[0])
except ValueError:return []
bars=[e for e in lines if 'RBAR'in e.get('layer','').upper()]
key=lambda p:tuple(round(v,6)for v in p)
lookup={tuple(sorted((key(e['points'][0]),key(e['points'][1])))):e for e in bars}
results=[]
for interface in interfaces:
scale=interface['cad_to_m']
for target in targets:
a,b=target['points']
for axis in (0,1):
if abs(a[axis]-b[axis])>1e-6 or abs(math.dist(a,b)*scale-diameter)>.001:continue
# The leader reaches an end cap. Both side legs and the
# opposite cap must exist in the original source graph.
for sign in (-1,1):
c=a[:];d=b[:];c[axis]+=sign*length/scale;d[axis]+=sign*length/scale
edges=[]
for p,q in ((a,c),(c,d),(d,b)):
found=lookup.get(tuple(sorted((key(p),key(q)))))
if found is None:break
edges.append(found)
if len(edges)!=3:continue
centres=[[(a[k]+b[k])/2 for k in (0,1)],[(c[k]+d[k])/2 for k in (0,1)]]
results.append({'source_sha256':source.sha256,'source_region_id':getattr(source,'region_id',None),
'mark':material['mark'],'diameter_mm':material['diameter_mm'],'length_m':length,
'pitch_mm':note['pitch_mm'],'centreline_points_cad':centres,'cad_to_m':scale,
'outline_entities':[selector(e)for e in [target,*edges]],
'note':copy.deepcopy(note),'note_leaders':leaders,'note_underline':selector(underlines[0]),
'interface':copy.deepcopy(interface),'placement_authorized':False,'host_binding_complete':False})
unique={tuple(sorted(tuple(round(v,6)for v in p)for p in r['centreline_points_cad'])):r for r in results}
return list(unique.values())
def detail_inventory(sources, rows):
from cad_sheet_regions import all_regions
from source_rebar_catalog import sheet_block_witnesses
result=[]
for source in all_regions(sources):
titles=[e for e in source.entities if e.get('points') and detail_role(e.get('text',''))=='dowel']
if not titles:continue
all_titles=[e for e in source.entities if e.get('points') and
('상세'in e.get('text','') or 'DETAIL'in e.get('text','').upper())]
installed=installed_detail_candidates(source,roles=('dowel',))
from cad_approach_detail import approach_interfaces
interfaces=approach_interfaces(source.entities)
blocks=sorted({v['block']for v in sheet_block_witnesses(source.entities)})
for title in titles:
tx,ty=_text_anchor(title);height=title.get('text_height')or 0
if height<=0:continue
near=[]
for e in source.entities:
if not e.get('text') or not e.get('points') or e.get('type')=='ATTDEF':continue
x,y=_text_anchor(e)
if not(abs(x-tx)<24*height and 0<ty-y<32*height):continue
owners=[t for t in all_titles if 0<_text_anchor(t)[1]-y<32*(t.get('text_height')or 0)]
if owners and min(owners,key=lambda t:math.dist(_text_anchor(t),(x,y))) is not title:continue
near.append(e)
notes=[]
for e in near:
parsed=parse_note(e['text'])
if not parsed:continue
x,y=_text_anchor(e);h=e.get('text_height')or 0
owners=[v for v in near if v is not e and
re.search(r'다[웰월]바|DOWEL|ST\s*PIPE|스티로폼|TAR\s*PAPER',v.get('text',''),re.I)
and 0<_text_anchor(v)[1]-y<=2*h and abs(_text_anchor(v)[0]-x)<=2*h]
notes.append({**parsed,'source':{**selector(e),'text':e['text']},
'label_candidates':[{**selector(v),'text':v['text']}for v in owners],
'placement_authorized':False})
bars=[]
for item in installed:
if item['title']!=selector(title):continue
# Exact sheet ownership matters: body D1/D2/D3 and accessory
# D1/D2/D3 can have the same marks and even the same diameter.
matches=[r for r in rows if r['source_sha256']==source.sha256 and
r.get('source_region_id')==getattr(source,'region_id',None) and
r['material']['mark'].strip().upper()==item['mark'].strip().upper()]
straight=[]
if len(matches)==1:
for note in notes:
if note['kind']=='dowel_bar_pitch':straight.extend(outlined_straight_bar(source,matches[0],note,interfaces))
bars.append({'mark':item['mark'],'installation':copy.deepcopy(item),
'fabrication_rows':copy.deepcopy(matches),
'straight_outline_bindings':straight,
'row_binding_unique':len(matches)==1,'host_binding_complete':False,
'pending':'full detail/pipe/bearing relationship and physical station binding required'})
result.append({'source_sha256':source.sha256,'source_region_id':getattr(source,'region_id',None),
'title':{**selector(title),'text':title['text']},'blocks':blocks,'notes':notes,'reinforcement':bars,
'installation_bound':False,'placement_authorized':False,
'pending':'do not treat a pipe outline or break symbol as the straight dowel centreline'})
return {'details':result,'source_sha256s':sorted(sources),'external_calls':0,
'placement_authorized':False,'body_mark_binding_unchanged':True}