- 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>
141 lines
8.3 KiB
Python
141 lines
8.3 KiB
Python
"""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}
|