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

+191
View File
@@ -0,0 +1,191 @@
# 철근 생성·검토·재료표 파이프라인 (대호지교 P1 두부보 작업에서 검증, 2026-10-02)
나중에 "도면(DXF) → 라이노 철근 생성 → 검토 → 재료표 DXF 출력" 프로그램으로 만들기 위한 방법 정리.
모든 Rhino 코드는 rhino_mcp `execute_rhino_code` (IronPython 2.7) 기준.
---
## 0. 환경 제약 (반드시 지킬 것)
- f-string 금지, walrus 금지, 타입힌트 금지.
- **exec 스코프 문제**: 함수 안에서 모듈 전역 변수를 못 본다. 리스트 컴프리헨션·lambda 안에서 바깥 변수 참조 시 `name 'x' is not defined` 에러.
- 해결: 루프는 `while/for`로 풀어 쓰기, 함수가 필요하면 기본인자로 바인딩 `def L(x, ents=ents):`.
- 출력은 마지막에 `result = "..."; print(result)`.
- 무거운 계산(`AreaMassProperties.Compute` 다수 호출)은 MCP 소켓 타임아웃 → 마크별로 나눠 호출, 면적 대신 isocurve 길이 사용.
- `sc.sticky[...]`에 중간 결과를 저장해서 다음 호출에서 이어 쓰기.
- 이 PC에는 Python/ezdxf 없음 → DXF 파싱은 bash `tr -d '\r' | awk`, DXF 쓰기는 IronPython으로 UTF-8 저장 후 `iconv -t CP949`.
- 라이노 오브젝트 삭제는 사용자 확인 후에만.
---
## 1. 도면(DXF) 읽기
DXF는 AC1021(UTF-8 텍스트), `$DWGCODEPAGE=ANSI_949`. group code/value가 2줄씩 반복.
- `awk/txt.awk` : TEXT/MTEXT/ATTRIB → `x y 타입 문자열` (좌표 정렬하면 표가 행 단위로 읽힘)
- `awk/geo.awk` : LINE / LWPOLYLINE / ARC → 선형 추출 (레이어 포함)
- `awk/ins.awk` : INSERT / CIRCLE 등 → 블록 삽입점 (철근 단면 점 = 블록 `CSBSEC1`)
```bash
tr -d '\r' < "철근재료표 및 상세(...).dxf" | awk -f txt.awk | sort -t$'\t' -k2,2nr -k1,1n > mat.tsv
tr -d '\r' < "코핑구조도(...).dxf" | awk -f geo.awk > cop_geo.tsv
```
### 1-1. 재료표 해석 (같은 y = 같은 행)
열 x 경계(원본): 번호|형식|직경|개수|A|B|C|D|E|R|X1|X2|J|길이|총길이|단위중량|총중량|총중량(할증)
- `"` = 위 행과 같은 직경.
- 1000mm 이상은 m(소수 3자리), 미만은 mm 정수로 표기.
- 단위중량: H16 1.560, H19 2.250, H22 3.040, H25 3.980, H29 5.040 (kg/m).
- 할증: H22 이상 6%, H19 이하 3%. 별도로 "소계(겹이음 할증 6.80%)" 행 = 소계중량×1.068.
- **형식 V(⊓뚜껑+⊔바닥 박스) 길이 규칙: L = 2A + 2B + 2C** (A=폭, 1250/1005 두 종류면 평균; B=박스 전체높이(바닥 다리); C=뚜껑 다리 510=겹침). 재료표 L은 평균, 총길이=Σ.
- **형식 VII(C8/C10)**: L = a+b+c+d, X1/X2 = 꺾임 사잇각.
### 1-2. 조립도/평면도에서 형상·위치 읽기
- 레이어 `CS-RBAR-MAJR` 선 = 철근 중심선. `CS-CONC-MAJR` = 콘크리트 외곽. `CS-CNTL` = 중심선.
- 텍스트 "주철근 조립도(정면도) ①CYCLE / ②CYCLE" 근처 ATTRIB(마크명 + "H25 @ 250")로 어떤 마크가 어느 CYCLE인지 판단.
- 조립도 좌표 → 코핑 로컬 s: `s = x - (코핑 중심 x)`; 코핑 중심 = 길이 23960 주철근 양 끝 중점 등으로 구함.
- 평면도에서 폭방향 위치: 수평 RBAR 선들의 y 값 → `w = y - 중심선y`. 치수 tick(CZDIMA1 블록) 간격(120, 130, 24@125 …)으로 피복·간격 확인.
- **검증 요령**: 도면 선 길이가 재료표 치수와 정확히 같으면(예: 4167.1, 3158.8) 그 도면 선이 철근 중심선임을 확신할 수 있다.
---
## 2. 코핑 로컬 좌표계 & 단면 외곽
```python
ang = math.radians(19.105) # 코핑 길이방향 (수평 edge 방향 히스토그램으로 구함)
u = Vector3d(cos, sin, 0) # 길이방향 s
wv = Vector3d(-sin, cos, 0) # 폭방향 w
c = coping.GetBoundingBox(True).Center # 원점
```
- 길이방향 각도: Brep.Edges 중 수평(|dz|<1)·길이>1000 edge들의 atan2 각도 히스토그램.
- 단면 외곽: `Intersection.BrepPlane(brep, Plane(c, u, Z))` → `DuplicateSegments()` → 각 세그먼트를 (s, z)로 출력.
- 뷰 정렬(좌표 안 바꾸고 보기만): `vp.SetCameraLocations(target, target - dir*1e5); vp.CameraUp = Z` (Left: dir=u, Front: dir=u를 90° 회전), `ChangeToParallelProjection(True)`.
---
## 3. 철근 중심선 생성 (피복 = 철근 중심 기준 120)
1. 단면 외곽의 각 면을 **안쪽으로 120 오프셋한 직선**들로 표현 (점 + 방향 + 안쪽 법선).
2. 인접 오프셋선끼리 교점 → 꺾임점. 2D (s,z)에서 직접 풀면 됨.
3. 도면에 고정 길이 구간이 있으면(예: a=630 수직다리, d=2331 끝 연장) 그 길이만큼 연장해 끝점 결정.
4. 3D 변환: `P = c + u*s + wv*w + Z*(z - c.Z)`; 대칭 마크는 s 부호만 반전.
5. 폭방향 위치(w)는 도면 평면도 기준. 1CYCLE: ±1630, ±1375 … ±125 (14본) / 2CYCLE: ±1500 … 0 (13본).
6. 굽힘: KDS 24 14 21 표 4.5-1 최소 내면지름(이형 D16이하 2db, D19이상 3.5db → 내면 반지름 1db/1.75db) 이상의 내면 R + 철근반지름 = 중심선 R. 적용할 R값은 사용자 결정 [미확정]. (기존 모델의 R=3db/4db는 과거 값)
```python
pc = Polyline(pts).ToPolylineCurve()
fc = Curve.CreateFilletCornersCurve(pc, Rc, tol, angtol) # 모든 내부 모서리 한 번에
brep = Brep.CreatePipe(fc, r, False, PipeCapMode.NONE, False, tol, angtol)[0]
```
검증: `1/fc.CurvatureAt(t).Length` 최소값 == Rc.
7. 블록에 넣을 때: 인스턴스 Xform 역변환 후 `InstanceDefinitions.ModifyGeometry(idx, geoms, attrs)` (**3인자 형태, 원본 attrs Duplicate** — 색상 깨짐 방지). 수정 전 기존 형상은 `<마크>_backup_YYYYMMDD` 블록정의로 보관.
### 색상 규칙 (직경별, ACI)
H10/H13=74, H16=5, H19=210, **H22 이상 전부 (104,0,25)**.
- 얕은 구조(문서인스턴스→leaf): leaf `ColorFromLayer`, 마크 레이어 색.
- 깊은 구조(C12~C18: mark→station→box→leaf): **station·box·leaf 3단계 모두 `ColorFromParent`**. 하나라도 ColorFromLayer면 엉뚱한 레이어색(코핑 황토색)으로 보임.
---
## 4. 검토(감사) 방법
### 4-1. 파이프 Brep에서 중심선 복원 (빠름)
각 face에서 isocurve 한 방향이 닫힌 원(길이 2πr)이면 파이프 면 → 다른 방향 isocurve를 원주 0과 0.5 위치에서 2개 뽑아 평균:
- 중심선 길이 = (len(iso@0) + len(iso@0.5)) / 2 (굽힘 토러스에서도 정확)
- 직선 면이면 양 끝점 평균 = 중심선 선분.
- 선분들을 끝점 근접으로 체인 연결 → 인접 선분 `Intersection.LineLine`으로 가상 교점 → **재료표 방식 치수(교점 간 거리)**와 사잇각.
- 막힌(캡) 파이프: 평면 face 중심 = 열린 끝점. 열린 파이프: `DuplicateNakedEdgeCurves` 원 중심.
### 4-2. 피복
- **철근 표면을 `BrepFace.PointAt` 격자(60×24)로 샘플 → `coping.ClosestPoint` 최소거리 + r = 중심 피복.**
- FastRenderMesh 정점 사용 금지(엉터리 값 91~104 나옴).
- 수직 레이캐스트(`Intersection.RayShoot`)는 상·하면까지 거리 참고용. 경사면 근처는 ClosestPoint 사용.
- 상단 직선 철근: "그 철근이 지나는 구간의 가장 낮은 상면"에서 120 (C26 교훈).
### 4-3. 개수
- 단순 마크: 인스턴스 정의 leaf 개수.
- 박스 마크: mark idef → station 인스턴스 → box 인스턴스 개수(인스턴스 수로 셈, 정의 수 아님).
### 4-4. 박스(V형) 측정
box 정의의 조각 중 높이가 큰 2개 = 뚜껑/바닥(아래쪽이 높은 것이 뚜껑). 나머지 = 연장 필러.
- A = 뚜껑 양 끝점 거리, 뚜껑top = bbox.Max.Z − r, 바닥bot = bbox.Min.Z + r
- 필러가 뚜껑 끝에 닿으면 뚜껑 연장, 바닥 끝에 닿으면 바닥 연장.
- C = 뚜껑top − 뚜껑끝, B = 바닥끝 − 바닥bot, 겹침 = 바닥끝 − 뚜껑끝 (510 미만이면 결함).
- 재료표 비교: 재료표 A·높이 = 모델 + 47.6 (= 2×(주철근 r + 스터럽 r)) → 도면은 스터럽이 주철근 바깥을 감쌈.
---
## 5. 재료표 DXF 출력
- 형식: **R12 (AC1009)**, `$DWGCODEPAGE=ANSI_949`, TABLES에 LTYPE CONTINUOUS / LAYER / STYLE(KOR, malgun.ttf), ENTITIES에 LINE, TEXT(72=1, 73=2 가운데정렬 + 11/21 정렬점).
- 원본 표 규격: 열 경계 상대 x = 0,1200,2400,3600,4800,6800,8800,10800,12800,14800,16800,18800,20800,22800,24800,26400,28000,29600,31200 (+비고 열 ~40000), 헤더 960(2단), 행 480, 글자 200, 제목 560.
- 행 순서: [H29 그룹][소계][소계(겹이음 6.80%)] → H25 → H22 → H19 → H16 … → 총계, 총계(겹이음).
- 소계 행은 세로선을 왼쪽 끝 + 길이 열 이후만 그림(병합셀 흉내).
- 계산: 중량(t) = 총길이 × 단위중량 / 1000, 할증 = ×1.06 또는 ×1.03, 겹이음 = ×1.068. 총계는 반올림된 소계들의 합(원본과 동일 방식).
- 저장: IronPython `System.IO.File.WriteAllText(p, txt.replace("\n","\r\n"), UTF8Encoding(False))` → bash `iconv -f UTF-8 -t CP949`.
- 검증: `RhinoDoc.CreateHeadless(None).Import(path)`로 별도 문서에 불러와 텍스트/선 개수·한글 확인 후 Dispose (루프 변수 None 처리 필수).
- 파일명 규칙: `YYMMDD_철근재료표 작업중.dxf` (도면 폴더에 저장).
---
## 5-2. 측면철근/캡타이 (C19~C25, 2026-10-02 검증)
- **배치 근거**: 단면 A-A/B-B/C-C의 철근 점(블록 `CSBSEC1`) y값 = 측면철근 높이(200 간격), x값 = 폭 위치. 정면도 점 = 폭방향으로 가로지르는 철근(C25)의 (s, z). 정면도 점 개수 합 = 재료표 수량으로 검증.
- **도면→모델 좌표**: 정면도 `s = x − (코핑 중심 x)`, `z = 7738 + (y − 기둥위 하면 y)`. 방향 반전 여부는 비대칭 마크(C12 = s 최소 쪽)로 판단.
- **VI 닫힌 고리**: 높이별로 120 오프셋 경계(헌치 대각/끝면) + WRAP(주철근 r + 고리 r, 바깥 감싸기) → 재료표 A와 정확히 일치. 닫힌 폴리라인은 **변 중간에서 시작**해야 4모서리 모두 필렛됨. 띠철근 R=2db.
- **VIII 갈고리 직선**: 135° 갈고리, 중심 R=60 (호 길이 = 재료표 b 141), 연장 c=100, L=A+2(b+c). A = 고리 A − 9. **갈고리는 수평면 안에서, 폭 바깥쪽(w 부호 방향)으로** (사용자 지시 2026-10-02). 위·아래로 꺾지 않음 (아래는 헌치 관통, 위는 잘못된 방향). 안쪽으로 꺾으면 중앙 ±106 철근끼리 갈고리가 겹침.
- **III ⊓ (C25)**: 윗변 A=3260 폭방향(w ±1630), 다리 300 아래. 점 위치가 하면에 가까우면 다리 관통 → 생성 시 `(z−300) − 하면z < 120` 자동 검출해서 보고.
- 생성 방식: 마크별 InstanceDefinition 1개(월드좌표 형상, 기준점 원점) + Identity 인스턴스 1개, leaf ColorFromLayer, 레이어 `..._1CYCLE::C{n}_H16` 파랑(0,0,255).
- 피복 검증: 긴 철근 전체를 Brep.ClosestPoint로 샘플링하면 MCP 타임아웃 → 대표 지점 몇 개를 Brep.ClosestPoint로 재고, 관통 여부는 Edge 점 `IsPointInside`로.
- 화면 격리 보기: 다른 레이어 IsVisible=False (인덱스 목록 sticky에 저장) → 파일 저장 전 복구 → 저장 → 다시 숨김.
---
## 5-3. 간섭 / 튀어나옴 / 피복 일괄 체크 (2026-10-02 검증, 전체 26마크 ~30초)
1. **중심선 선분 추출**: 각 파이프 면에서 원주 0 위치 isocurve를 `ToPolyline(0,0,0.05,0,0,0.3,0,3000,True)`로 적응 분할 → 각 정점의 곡선 매개변수 t에서 `face.PointAt(t, v0)`와 `PointAt(t, v0.5)`의 중점 = 중심점. 중첩 블록은 Xform을 곱해 내려가며 처리, 같은 leaf 형상은 캐시.
2. **간섭**: 마크별 `RTree`(선분 bbox를 r만큼 키움) → 마크 bbox가 겹치는 쌍만 `RTree.SearchOverlaps(treeA, treeB, 0, System.EventHandler[RTreeEventArgs](cb))` (`e.Id`, `e.IdB`) → `Intersection.LineLine(..., finite=True)`, 실패(평행)면 `Line.MinimumDistanceTo` → 관입 = rA + rB − 거리 > 0.5 이면 기록(철근쌍별 최대).
3. **피복/튀어나옴**: 코핑이 폭방향 압출 형상이므로, 점을 (s,w,z)로 바꿔 2D 단면 다각형 내부 판정 + 변까지 거리, 측면은 1750−|w|. 중심피복 < r 이면 튀어나옴. (Brep.ClosestPoint 대량 호출은 타임아웃, 메시 ClosestPoint는 값이 틀어짐)
4. **출력**: CSV(UTF-8 BOM, 엑셀 한글) + 요약 md + 라이노 체크 레이어(TextDot, PointCloud) — 모델 자체는 건드리지 않음.
---
## 5-4. CTC 검사 · 박스 철근 보정 · 갈고리 반전 · 재료표 정렬 (2026-10-06 검증)
**CTC(철근 중심 간격) 검사** — `rhino/07_ctc_extract.py`
- 마크별 리프 Brep 중심점을 코핑 로컬 (s,t,z)로 뽑아 JSON → 고유값(허용오차 1~2mm) 정렬 → 차이를 도면 `@` 표기와 대조.
- 도면 간격 표기는 `코핑구조도` DXF의 `CS-DIMT` TEXT (`24@125=3.000`, `11@250=2.750`, 평면 스터럽 `7@300|14@150|8@300|16@150|19@300|…`, `38@600=22.800`).
- 주의: 같은 칸의 뚜껑/바닥 조각은 s가 다를 수 있다. 클러스터의 **첫 값만** 쓰면 가짜 어긋남이 생긴다(실제로 한 번 오판함) → 칸 안의 모든 리프 s의 min/max를 같이 볼 것.
- 이 값들은 철근 마크 레이어가 꺼져 있으면 `doc.Objects` 기본 열거에서 빠진다 → `ObjectEnumeratorSettings.HiddenObjects=True`.
**박스(⊓+⊔) 바닥 조각 보정** — `rhino/05_realign_base_pieces.py`
- 증상: 피복 보정 때 바닥 조각을 헌치 법선 방향으로 옮겨서 뚜껑과 s 평면이 1~3mm 어긋남 (C12/C18 2.68, C14/C16 1.95).
- 보정: 어긋남 −dev 만큼 s 이동 + 헌치 경사 접선 방향으로 dz = (경사)·ds 이동(수직 피복 유지) → 뚜껑 윗변 중심 z에서 `Brep.Trim` 으로 상단 재절단(겹침 510).
- 박스 idef는 좌우 대칭 인스턴스가 공유하므로 idef 하나만 고치면 4개 박스에 동시 적용됨(검증은 모든 인스턴스로).
- `ModifyGeometry(idx, geoms, attrs)` 3-인자 + `attrs=ob.Attributes.Duplicate()` 로 ColorFromParent 보존. 백업 idef `<이름>_bk1006b`.
**갈고리 방향 반전** — `rhino/06_flip_hooks.py`
- 철근 축을 지나는 s-수직 평면으로 자른 단면 원 중심 = 축 위의 점 → `Transform.Mirror(축점, 폭방향 법선)` + `Brep.Flip()`. 길이 불변.
- 선택은 centroid t (갈고리 때문에 축에서 ~2mm 벗어남) 대역으로 하고, **개수가 예상과 다르면 수정하지 않고 보고**.
**재료표 DXF 텍스트 정렬** — `dxf/leftalign.ps1`
- R12/CP949 DXF에서 표 칸 왼쪽 경계(세로선) + 200 위치로 TEXT 왼쪽 정렬(72=0, 73=2). `powershell -File leftalign.ps1 -In 원본.dxf -Out 새파일.dxf -Pad 200`.
- 원본 재료표는 가운데(72=1) 242개 / 오른쪽(72=2) 58개 정렬 — 이 파이프라인의 표는 왼쪽 정렬 규칙을 쓴다(사용자 지시).
**화면/레이어 함정**
- 블록 안쪽 리프가 **다른 레이어**(예: `P1_두부(코핑)`)에 있으면, 마크 레이어를 켜도 그 레이어가 꺼져 있으면 안 그려진다. C12~C18 스터럽 리프가 코핑 레이어에 있음 → 철근만 보려면 코핑 **레이어는 켜 두고 솔리드/보조선만 객체 단위로 숨긴다**.
- 화면 격리 후 저장할 때는: 현재 표시 상태 저장 → 원래 상태 복구 + 숨긴 객체 Show → `doc.Save()` → 격리 상태 재적용. (저장은 127MB라 MCP 소켓 타임아웃이 나지만 Rhino 안에서는 계속 진행됨 → 결과 파일 생길 때까지 until-loop로 대기.)
- Left 뷰 정렬: `SetCameraLocations(target, target - dir*60000)`, dir=(cos a, sin a, 0), `CameraUp=Z`, 그 뒤 `ZoomBoundingBox`.
---
## 6. 프로그램화 시 모듈 구성 제안
1. `dxf_reader` – txt/geo/ins 추출, 재료표 행 파서, CYCLE별 마크 목록, 조립도 철근 polyline.
2. `section` – 콘크리트 Brep → 로컬 프레임(u,w,원점) → 단면 외곽 (s,z) 세그먼트.
3. `rebar_builder` – 형식(I~VIII)별 중심선 생성기(오프셋선 교점 + 고정길이), 폭방향 배열, KDS R 필렛, 파이프, 블록/레이어/색.
4. `auditor` – isocurve 중심선 복원, 치수/각도/개수/피복/겹침 비교 → 차이 리포트.
5. `table_writer` – R12 CP949 재료표 DXF (원본 규격 + 비고 열).
## 5-5. 간섭 재검사 스크립트 (2026-10-06) — `rhino/08_clash_check.py`
- 읽기 전용. 철근별 단면 원 중심 → 중심선 선분 → RTree → 선분 최단거리, 관입 = rA+rB−거리 > 0.5. 박스(뚜껑·바닥·연장 조각) 내부와 같은 철근은 제외, P1 레이어만 (P2/P10/P12 숨김 레이어 복사본 제외).
- 함정: ① `BoundingBox(p0, p1)` 는 점이 min→max 순서가 아니면 무효 상자 → 큰 교차가 통째로 누락됨. `BoundingBox([p0, p1])` 사용. ② 숨김 레이어 객체는 `ObjectEnumeratorSettings.HiddenObjects=True` 로 열거해야 함. ③ NURBS 파라미터 0.5 지점은 반대편 점이 아님 → 단면 원 `TryGetCircle` 중심 사용. ④ 실행이 길어 소켓 타임아웃이 나도 Rhino 안에서는 계속 돈다 → 결과 파일 갱신을 until-루프로 대기.
+11
View File
@@ -0,0 +1,11 @@
# prints LINE: L layer x1 y1 x2 y2 ; LWPOLYLINE: P layer x y x y ... ; ARC: A layer cx cy r a0 a1
function flush(){ if(ent=="LINE") print "L\t" lay "\t" x1 "\t" y1 "\t" x2 "\t" y2; else if(ent=="LWPOLYLINE") print "P\t" lay "\t" pts; else if(ent=="ARC") print "A\t" lay "\t" x1 "\t" y1 "\t" r "\t" a0 "\t" a1; ent="";pts="";lay="" }
NR%2==1{c=$0; gsub(/ /,"",c); next}
{v=$0; gsub(/^ +/,"",v)
if(c=="0"){ flush(); if(v=="LINE"||v=="LWPOLYLINE"||v=="ARC") ent=v; if(v=="ENDSEC"||v=="BLOCK") inblk=(v=="BLOCK"); next }
if(ent=="") next
if(c=="8") lay=v
if(ent=="LWPOLYLINE"){ if(c=="10") pts=pts " " v; if(c=="20") pts=pts "," v; if(c=="42") pts=pts "(b" v ")" }
else { if(c=="10")x1=v; if(c=="20")y1=v; if(c=="11")x2=v; if(c=="21")y2=v; if(c=="40")r=v; if(c=="50")a0=v; if(c=="51")a1=v }
}
END{flush()}
+8
View File
@@ -0,0 +1,8 @@
function flush(){ if(ent!="") print ent "\t" lay "\t" nm "\t" x "\t" y "\t" r; ent="";nm="";x="";y="";r="" }
NR%2==1{c=$0; gsub(/ /,"",c); next}
{v=$0; gsub(/^ +/,"",v)
if(c=="0"){ flush(); if(v=="INSERT"||v=="CIRCLE"||v=="ARC"||v=="LWPOLYLINE"||v=="POLYLINE") ent=v; next }
if(ent=="") next
if(c=="8") lay=v; if(c=="2") nm=v; if(c=="10" && x=="") x=v; if(c=="20" && y=="") y=v; if(c=="40") r=v
}
END{flush()}
+8
View File
@@ -0,0 +1,8 @@
function flush(){ if(ent!="" && t!="") printf "%.0f\t%.0f\t%s\t%s\n", x, y, ent, t; ent=""; t=""; x=0; y=0 }
NR%2==1{c=$0; gsub(/ /,"",c); next}
{v=$0
if(c=="0"){ flush(); if(v=="TEXT"||v=="MTEXT"||v=="ATTRIB") ent=v; next }
if(ent=="") next
if(c=="10") x=v; else if(c=="20") y=v; else if(c=="1") t=v; else if(c=="3") t=t v
}
END{flush()}
+125
View File
@@ -0,0 +1,125 @@
param([string]$In, [string]$Out, [double]$W = 14000)
# 재료표 DXF(R12/CP949)에 "비고 3 (KDS 24 14 21 기준 상이)" 열을 오른쪽에 추가하고, 마크별 KDS 상이 내용을 자홍(62=6) 글씨로 적는다.
# 대호지교 P1 (2026-10-06) 용 노트 사전은 아래 $notes. 다른 프로젝트는 $notes 와 하단 범례만 바꿔서 사용.
# 주의: 이 파일은 UTF-8 BOM 으로 저장해야 PowerShell 5.1 이 한글을 바르게 읽는다. CP949 에 없는 문자(⊔ 등)는 쓰지 말 것.
$enc = [System.Text.Encoding]::GetEncoding(949)
$inv = [System.Globalization.CultureInfo]::InvariantCulture
$lines = New-Object 'System.Collections.Generic.List[string]'
$lines.AddRange([System.IO.File]::ReadAllLines($In, $enc))
# 마크 -> 비고3 두 줄 (줄당 ~45자 이내 권장: 칸 폭 W / 글자 150)
$notes = [ordered]@{
'C1' = @('KDS 4.5.3.1: 스터럽 C12~C18·고리 C23과 중심선 교차 - 순간격 미확보', '겹침이음(C2·C3·C8·C10)도 같은 중심선에 모델링 (10/06 재검사)');
'C2' = @('KDS 4.5.3.1: 스터럽 C15~C18·고리 C23·C24와 교차 - 순간격 미확보', '끝 겹침이음이 C1 다리와 같은 중심선 (10/06 재검사)');
'C3' = @('KDS 4.5.3.1: 스터럽 C12~C15·고리 C23·C24와 교차 - 순간격 미확보', '끝 겹침이음이 C1 다리와 같은 중심선 (10/06 재검사)');
'C4' = @('KDS 4.5.3.1: 스터럽 C17·C18, C26 다리와 교차', '순간격 미확보 (10/06 재검사)');
'C5' = @('KDS 4.5.3.1: C26 다리와 교차 - 순간격 미확보', '(10/06 재검사)');
'C6' = @('KDS 4.5.3.1: 스터럽 C12·C13, C26 다리와 교차', '순간격 미확보 (10/06 재검사)');
'C7' = @('KDS 4.5.3.1: C26 다리와 교차 - 순간격 미확보', '(10/06 재검사)');
'C8' = @('KDS 4.5.3.1: 스터럽 C15~C18·고리 C19·C21·C23, VIII C20·C22·C24와', '교차, C9·C1(겹침)과 같은 중심선 (10/06 재검사)');
'C9' = @('KDS 4.5.3.1: 스터럽 C14·C15·C16 바닥과 같은 중심선,', 'C8·C10 대각구간과 교차 - 순간격 미확보 (10/06 재검사)');
'C10' = @('KDS 4.5.3.1: 스터럽 C12~C15·고리 C19·C21·C23, VIII C20·C22·C24와', '교차, C9·C1(겹침)과 같은 중심선 (10/06 재검사)');
'C11' = @('KDS 4.5.3.1: 스터럽 C14·C15·C16과 교차', '순간격 미확보 (10/06 재검사)');
'C12' = @('KDS 4.5.3.1: 주철근 C1·C3·C6·C10과 중심선 일치 - 순간격 미확보', '고리 C19~C23·C25와 교차, 같은 칸 박스끼리 같은 선에서 겹침');
'C13' = @('KDS 4.5.3.1: 주철근 C1·C3·C6·C10과 중심선 일치 - 순간격 미확보', '고리 C19·C21·C23, C25·C26과 교차, 박스끼리 같은 선 겹침');
'C14' = @('KDS 4.5.3.1: 주철근 C1·C3·C9·C10·C11과 중심선 일치 - 순간격 미확보', '고리 C19~C23, C25·C26과 교차, 박스끼리 같은 선 겹침');
'C15' = @('KDS 4.5.3.1: 주철근 C1·C2·C3·C8~C11과 중심선 일치 - 순간격 미확보', '고리 C21·C23, C25·C26과 교차, 박스끼리 같은 선 겹침');
'C16' = @('KDS 4.5.3.1: 주철근 C1·C2·C8·C9·C11과 중심선 일치 - 순간격 미확보', '고리 C19~C23, C25·C26과 교차, 박스끼리 같은 선 겹침');
'C17' = @('KDS 4.5.3.1: 주철근 C1·C2·C4·C8과 중심선 일치 - 순간격 미확보', '고리 C19·C21·C23, C25·C26과 교차, 박스끼리 같은 선 겹침');
'C18' = @('KDS 4.5.3.1: 주철근 C1·C2·C4·C8과 중심선 일치 - 순간격 미확보', '고리 C19~C23·C25와 교차, 같은 칸 박스끼리 같은 선에서 겹침');
'C19' = @('KDS 4.5.3.1: 고리가 스터럽 C12~C18, C8·C10 대각, C20·C25와 교차', '순간격 미확보 (10/06 재검사)');
'C20' = @('KDS 4.5.3.1: VIII 끝·갈고리가 고리 C19, C8·C10, 스터럽 C12·C14·C16·C18과', '교차 - 순간격 미확보 (10/06 재검사)');
'C21' = @('KDS 4.5.3.1: 고리가 스터럽 C12~C18, C8·C10 대각, C22·C25와 교차', '순간격 미확보 (10/06 재검사)');
'C22' = @('KDS 4.5.3.1: VIII가 C25, 고리 C21, C8·C10, 스터럽 C12·C18과 교차', '순간격 미확보 (10/06 재검사)');
'C23' = @('KDS 4.5.3.1: 고리가 스터럽 C12~C18, C1~C3·C8·C10, C24·C25·C26과 교차', '순간격 미확보 (10/06 재검사)');
'C24' = @('KDS 4.5.3.1: VIII가 C25, 고리 C23, C1~C3·C8·C10과 교차', '순간격 미확보 (10/06 재검사)');
'C25' = @('KDS 4.5.3.1: 스터럽 C12~C18, C22~C24, C26, C1~C3·C10과 교차', '일부는 같은 선(w=±1630) - 순간격 미확보 (10/06 재검사)');
'C26' = @('KDS 4.5.3.1: 스터럽 C13~C17, C25, 주철근 C4~C7·C1~C3과 교차', '뚜껑 윗변과 같은 높이 - 순간격 미확보 (10/06 재검사)')
}
$legend = @(
'※ 자홍색 글씨(비고 3) = KDS 24 14 21 콘크리트교 설계기준(한계상태설계법, 2025.08.22 개정) 기준과 다르게 배근된 곳',
'※ KDS 확인 결과 만족: 구부림 내면지름(표 4.5-1, 전 마크 - 스터럽 중심선 R=66.6, 내면 55.5 ≥ 38.85), 표준갈고리 연장 ≥5db(C20·C22·C24 c=100 ≥ 79.5), 피복(표면 107 이상, 최대 요구 65),',
' 전단철근 간격(종방향 ≤0.75d, 폭방향 ≤0.75d·600), 최소 전단철근비(ρv ≥ 0.08√fck/fy), 다단 철근 동일 연직면(4.5.3.1(3))',
'※ 순간격(간섭) 항목 = 2026.10.06 중심선 간섭 재검사(읽기 전용, 15,101 철근쌍, 목록 261006_간섭재검사_목록.csv) 기준이며 모델은 변경하지 않음',
'※ 간섭 = 두 철근 중심선 거리 < 반지름 합(관입 > 0.5mm). 겹침이음부·박스(뚜껑+바닥) 내부는 같은 중심선 모델링이라 제외하고 집계함'
)
# --- LAYER table: add KDS layer (color 6) after DIFF
$cnt = -1
for ($i = 0; $i -lt $lines.Count - 3; $i++) {
if ($lines[$i].Trim() -eq '2' -and $lines[$i + 1].Trim() -eq 'LAYER' -and $lines[$i + 2].Trim() -eq '70') { $cnt = $i + 3; break }
}
if ($cnt -lt 0) { throw 'LAYER table not found' }
$lines[$cnt] = ([int]$lines[$cnt].Trim() + 1).ToString()
$di = -1
for ($i = 0; $i -lt $lines.Count - 1; $i++) { if ($lines[$i].Trim() -eq '2' -and $lines[$i + 1].Trim() -eq 'DIFF') { $di = $i; break } }
if ($di -lt 0) { throw 'DIFF layer not found' }
$ins = $di + 2 + 6 # after: 70 0 62 c 6 CONTINUOUS
$lines.InsertRange($ins, [string[]]@('0', 'LAYER', '2', 'KDS', '70', '0', '62', '6', '6', 'CONTINUOUS'))
# --- parse entities (after the LAYER insert, so indices are final)
$start = -1
for ($i = 0; $i -lt $lines.Count - 1; $i++) { if ($lines[$i].Trim() -eq '2' -and $lines[$i + 1].Trim() -eq 'ENTITIES') { $start = $i + 2; break } }
$ents = New-Object System.Collections.ArrayList
$cur = $null
for ($i = $start; $i -lt $lines.Count - 1; $i += 2) {
$c = $lines[$i].Trim(); $v = $lines[$i + 1]
if ($c -eq '0') {
if ($cur -ne $null) { [void]$ents.Add($cur) }
if ($v.Trim() -eq 'ENDSEC') { $endIdx = $i; break }
$cur = @{ type = $v.Trim(); idx = @{}; val = @{} }; continue
}
if ($cur -ne $null) { $cur.idx[$c] = $i + 1; $cur.val[$c] = $v.Trim() }
}
# right edge x and mark rows
$xr = 0.0
foreach ($e in $ents) { if ($e.type -eq 'LINE') { $x2 = [double]::Parse($e.val['11'], $inv); if ($x2 -gt $xr) { $xr = $x2 } } }
$xn = $xr + $W
$rowY = @{}
$hdrY = $null
foreach ($e in $ents) {
if ($e.type -ne 'TEXT') { continue }
$x = [double]::Parse($e.val['10'], $inv)
$t = $lines[$e.idx['1']].Trim()
if ([math]::Abs($x - 200) -lt 0.5 -and $notes.Contains($t)) { $rowY[$t] = [double]::Parse($e.val['21'], $inv) }
if ($t -like '비 고 2*') { $hdrY = [double]::Parse($e.val['21'], $inv) }
}
$new = New-Object 'System.Collections.Generic.List[string]'
# extend horizontals, add right border (copy of the old right border segments)
$nLine = 0
foreach ($e in $ents) {
if ($e.type -ne 'LINE') { continue }
$x1 = [double]::Parse($e.val['10'], $inv); $x2 = [double]::Parse($e.val['11'], $inv)
$y1 = [double]::Parse($e.val['20'], $inv); $y2 = [double]::Parse($e.val['21'], $inv)
if ([math]::Abs($y1 - $y2) -lt 0.01 -and [math]::Abs($x2 - $xr) -lt 0.01) {
$lines[$e.idx['11']] = $xn.ToString('0.000', $inv) # 가로선 연장
$nLine++
} elseif ([math]::Abs($x1 - $x2) -lt 0.01 -and [math]::Abs($x1 - $xr) -lt 0.01) {
foreach ($p in @('0', 'LINE', '8', 'TABL-LIN', '10', $xn.ToString('0.000', $inv), '20', $y1.ToString('0.000', $inv), '30', '0.0', '11', $xn.ToString('0.000', $inv), '21', $y2.ToString('0.000', $inv), '31', '0.0')) { $new.Add($p) }
}
}
function AddText($lst, $layer, $x, $yMid, $h, $txt) {
$yb = $yMid - $h / 2
foreach ($p in @('0', 'TEXT', '8', $layer, '10', $x.ToString('0.000', $inv), '20', $yb.ToString('0.000', $inv), '30', '0.0', '40', $h.ToString('0.0', $inv), '1', $txt, '7', 'KOR', '72', '0', '11', $x.ToString('0.000', $inv), '21', $yMid.ToString('0.000', $inv), '31', '0.0', '73', '2')) { $lst.Add($p) }
}
if ($hdrY -ne $null) { AddText $new 'TABL-TEX' ($xr + 200) $hdrY 180.0 '비 고 3 (KDS 24 14 21 기준 상이)' }
$placed = 0
foreach ($k in $notes.Keys) {
if (-not $rowY.ContainsKey($k)) { continue }
$y = $rowY[$k]
AddText $new 'KDS' ($xr + 200) ($y + 125) 150.0 $notes[$k][0]
AddText $new 'KDS' ($xr + 200) ($y - 125) 150.0 $notes[$k][1]
$placed++
}
# legend under the existing notes
$ymin = 0.0
foreach ($e in $ents) { if ($e.type -eq 'TEXT' -and $e.val['8'] -eq 'NOTE') { $yy = [double]::Parse($e.val['20'], $inv); if ($yy -lt $ymin) { $ymin = $yy } } }
$y = $ymin - 420
foreach ($lg in $legend) { AddText $new 'KDS' 200 ($y + 90) 180.0 $lg; $y -= 420 }
$lines.InsertRange($endIdx, $new)
[System.IO.File]::WriteAllLines($Out, $lines, $enc)
"right edge $xr -> $xn ; horizontals extended $nLine ; note rows placed $placed ; header y $hdrY"
# round-trip check (characters lost in CP949 become '?')
$chk = [System.IO.File]::ReadAllLines($Out, $enc)
$bad = @($chk | ? { $_ -match '\?' -and $_ -match '[가-힣]' })
"lines with '?' next to Hangul: $($bad.Count)"
+81
View File
@@ -0,0 +1,81 @@
param([string]$In, [string]$Out, [double]$Pad = 200)
$enc = [System.Text.Encoding]::GetEncoding(949)
$lines = New-Object 'System.Collections.Generic.List[string]'
$lines.AddRange([System.IO.File]::ReadAllLines($In, $enc))
$inv = [System.Globalization.CultureInfo]::InvariantCulture
# locate ENTITIES section
$start = -1
for ($i = 0; $i -lt $lines.Count - 1; $i++) {
if ($lines[$i].Trim() -eq '2' -and $lines[$i + 1].Trim() -eq 'ENTITIES') { $start = $i + 2; break }
}
if ($start -lt 0) { throw 'no ENTITIES' }
$ents = New-Object System.Collections.ArrayList
$cur = $null
for ($i = $start; $i -lt $lines.Count - 1; $i += 2) {
$c = $lines[$i].Trim()
$v = $lines[$i + 1]
if ($c -eq '0') {
if ($cur -ne $null) { [void]$ents.Add($cur) }
if ($v.Trim() -eq 'ENDSEC') { $cur = $null; break }
$cur = @{ type = $v.Trim(); idx = @{}; val = @{} }
continue
}
if ($cur -ne $null) { $cur.idx[$c] = $i + 1; $cur.val[$c] = $v.Trim() }
}
# vertical cell-border lines: x, ymin, ymax
$vx = New-Object System.Collections.ArrayList
foreach ($e in $ents) {
if ($e.type -ne 'LINE') { continue }
$x1 = [double]::Parse($e.val['10'], $inv); $x2 = [double]::Parse($e.val['11'], $inv)
$y1 = [double]::Parse($e.val['20'], $inv); $y2 = [double]::Parse($e.val['21'], $inv)
if ([math]::Abs($x1 - $x2) -lt 0.01) {
[void]$vx.Add([pscustomobject]@{ x = $x1; y0 = [math]::Min($y1, $y2); y1 = [math]::Max($y1, $y2) })
}
}
$changed = 0; $skipped = 0; $overflow = @()
foreach ($e in $ents) {
if ($e.type -ne 'TEXT') { continue }
$layer = $e.val['8']
if ($layer -eq 'TITLE' -or $layer -eq 'NOTE') { $skipped++; continue }
$has72 = $e.val.ContainsKey('72') -and $e.val['72'] -ne '0'
$has73 = $e.val.ContainsKey('73') -and $e.val['73'] -ne '0'
if ($has72 -or $has73) {
$cx = [double]::Parse($e.val['11'], $inv); $cy = [double]::Parse($e.val['21'], $inv)
} else {
$cx = [double]::Parse($e.val['10'], $inv); $cy = [double]::Parse($e.val['20'], $inv)
}
$h = [double]::Parse($e.val['40'], $inv)
# for left-baseline text (no alignment) test the vertical mid of the glyph row
if (-not ($has72 -or $has73)) { $cy = $cy + $h / 2 }
$left = $null; $right = $null
foreach ($ln in $vx) {
if ($cy -lt $ln.y0 - 0.5 -or $cy -gt $ln.y1 + 0.5) { continue }
if ($ln.x -le $cx + 0.5) { if ($left -eq $null -or $ln.x -gt $left) { $left = $ln.x } }
else { if ($right -eq $null -or $ln.x -lt $right) { $right = $ln.x } }
}
if ($left -eq $null) { $skipped++; continue } # not inside a table box
$nx = $left + $Pad
$s = $nx.ToString('0.000', $inv)
# TEXT: 72 -> 0 (left), keep 73 (vertical middle); 10 and 11 x -> new left
$lines[$e.idx['10']] = $s
if ($e.idx.ContainsKey('11')) { $lines[$e.idx['11']] = $s }
if ($e.idx.ContainsKey('72')) { $lines[$e.idx['72']] = '0' }
$changed++
# rough overflow estimate (hangul ~1.0h, others ~0.6h)
$txt = $lines[$e.idx['1']]
$t = $enc.GetString([byte[]]$enc.GetBytes($txt))
$w = 0.0
foreach ($ch in $t.ToCharArray()) { if ([int]$ch -gt 255) { $w += $h * 1.0 } else { $w += $h * 0.6 } }
if ($right -ne $null -and ($nx + $w) -gt ($right - 50)) { $overflow += ("{0}: '{1}' est.width {2:N0} room {3:N0}" -f $layer, $t, $w, ($right - $nx)) }
}
[System.IO.File]::WriteAllLines($Out, $lines, $enc)
"changed=$changed skipped=$skipped"
"possible overflow: " + $overflow.Count
$overflow
File diff suppressed because it is too large. Load diff
+144
View File
@@ -0,0 +1,144 @@
// Minimal HWP5 text extractor: CFB (OLE2) reader + zlib + PARA_TEXT records. No external packages.
const fs = require('fs');
const zlib = require('zlib');
const [,, inFile, outFile] = process.argv;
const buf = fs.readFileSync(inFile);
const sig = buf.slice(0, 8).toString('hex');
if (sig !== 'd0cf11e0a1b11ae1') { console.error('not an OLE file', sig); process.exit(1); }
const secShift = buf.readUInt16LE(0x1e);
const secSize = 1 << secShift;
const miniShift = buf.readUInt16LE(0x20);
const miniSize = 1 << miniShift;
const nFat = buf.readUInt32LE(0x2c);
const dirStart = buf.readUInt32LE(0x30);
const miniCutoff = buf.readUInt32LE(0x38);
const miniFatStart = buf.readUInt32LE(0x3c);
let difatStart = buf.readUInt32LE(0x44);
const nDifat = buf.readUInt32LE(0x48);
const ENDOFCHAIN = 0xfffffffe;
const FREE = 0xffffffff;
function secOff(s) { return (s + 1) * secSize; }
function readSector(s) { return buf.slice(secOff(s), secOff(s) + secSize); }
// FAT sector list from header DIFAT + DIFAT chain
const fatSecs = [];
for (let i = 0; i < 109 && fatSecs.length < nFat; i++) {
const v = buf.readUInt32LE(0x4c + i * 4);
if (v === FREE || v === ENDOFCHAIN) break;
fatSecs.push(v);
}
let d = difatStart;
for (let i = 0; i < nDifat && d !== ENDOFCHAIN && d !== FREE; i++) {
const sec = readSector(d);
for (let j = 0; j < secSize / 4 - 1 && fatSecs.length < nFat; j++) {
const v = sec.readUInt32LE(j * 4);
if (v !== FREE && v !== ENDOFCHAIN) fatSecs.push(v);
}
d = sec.readUInt32LE(secSize - 4);
}
const fat = [];
for (const s of fatSecs) {
const sec = readSector(s);
for (let j = 0; j < secSize / 4; j++) fat.push(sec.readUInt32LE(j * 4));
}
function chain(start) {
const out = [];
let s = start;
let guard = 0;
while (s !== ENDOFCHAIN && s !== FREE && guard++ < 10000000) { out.push(s); s = fat[s]; }
return out;
}
function readStream(start, size) {
const parts = chain(start).map(readSector);
return Buffer.concat(parts).slice(0, size);
}
// directory
const dirBuf = readStream(dirStart, chain(dirStart).length * secSize);
const entries = [];
for (let i = 0; i < dirBuf.length / 128; i++) {
const e = dirBuf.slice(i * 128, i * 128 + 128);
const nl = e.readUInt16LE(0x40);
if (nl === 0) { entries.push(null); continue; }
const name = e.slice(0, nl - 2).toString('utf16le');
entries.push({
name, type: e.readUInt8(0x42), left: e.readUInt32LE(0x44), right: e.readUInt32LE(0x48), child: e.readUInt32LE(0x4c),
start: e.readUInt32LE(0x74), size: Number(e.readBigUInt64LE(0x78)), id: i,
});
}
const root = entries[0];
const miniStream = readStream(root.start, root.size);
const miniFat = [];
{
const mf = Buffer.concat(chain(miniFatStart).map(readSector));
for (let j = 0; j < mf.length / 4; j++) miniFat.push(mf.readUInt32LE(j * 4));
}
function readMini(start, size) {
const parts = [];
let s = start;
let guard = 0;
while (s !== ENDOFCHAIN && s !== FREE && guard++ < 10000000) {
parts.push(miniStream.slice(s * miniSize, s * miniSize + miniSize));
s = miniFat[s];
}
return Buffer.concat(parts).slice(0, size);
}
function streamOf(e) { return e.size < miniCutoff ? readMini(e.start, e.size) : readStream(e.start, e.size); }
// walk tree to build path -> entry
const paths = {};
function walk(id, prefix) {
if (id === 0xffffffff || id >= entries.length || !entries[id]) return;
const e = entries[id];
walk(e.left, prefix);
const p = prefix + '/' + e.name;
paths[p] = e;
if (e.type === 1) walk(e.child, p);
walk(e.right, prefix);
}
walk(entries[0].child, '');
const header = streamOf(paths['/FileHeader']);
const flags = header.readUInt32LE(36);
const compressed = (flags & 1) !== 0;
const encrypted = (flags & 2) !== 0;
if (encrypted) { console.error('encrypted document'); process.exit(2); }
console.error('HWP version', header.readUInt32LE(32).toString(16), 'compressed', compressed, 'sections', Object.keys(paths).filter(p => /BodyText\/Section\d+$/.test(p)).length);
const secNames = Object.keys(paths).filter(p => /^\/BodyText\/Section\d+$/.test(p)).sort((a, b) => parseInt(a.match(/\d+$/)[0]) - parseInt(b.match(/\d+$/)[0]));
const EXT = new Set([1, 2, 3, 11, 12, 14, 15, 16, 17, 18, 21, 22, 23]);
const INL = new Set([4, 5, 6, 7, 8, 9, 19, 20]);
let outTxt = [];
for (const sn of secNames) {
let data = streamOf(paths[sn]);
if (compressed) data = zlib.inflateRawSync(data);
let pos = 0;
while (pos + 4 <= data.length) {
const h = data.readUInt32LE(pos); pos += 4;
const tag = h & 0x3ff; let size = (h >>> 20) & 0xfff;
if (size === 0xfff) { size = data.readUInt32LE(pos); pos += 4; }
const rec = data.slice(pos, pos + size); pos += size;
if (tag === 0x43) { // PARA_TEXT
let s = '';
for (let i = 0; i + 1 < rec.length;) {
const ch = rec.readUInt16LE(i);
if (ch >= 32) { s += String.fromCharCode(ch); i += 2; }
else if (EXT.has(ch) || INL.has(ch)) { if (ch === 9) s += '\t'; i += 16; }
else if (ch === 10 || ch === 13) { s += '\n'; i += 2; }
else if (ch === 24 || ch === 25 || ch === 26 || ch === 27 || ch === 28 || ch === 29 || ch === 30 || ch === 31) { if (ch === 24) s += '-'; if (ch === 30 || ch === 31) s += ' '; i += 2; }
else { i += 2; }
}
outTxt.push(s.replace(/\n+$/, ''));
} else if (tag === 0x58) { // EQEDIT (equation script)
try {
const n = rec.readUInt16LE(4);
const script = rec.slice(6, 6 + n * 2).toString('utf16le').replace(/\s+/g, ' ').trim();
outTxt.push(' [수식: ' + script + ']');
} catch (e) { outTxt.push(' [수식 읽기 실패]'); }
}
}
}
fs.writeFileSync(outFile, outTxt.join('\n'), 'utf8');
console.error('paragraphs', outTxt.length, 'chars', outTxt.join('\n').length);
@@ -0,0 +1,33 @@
# execute_rhino_code (IronPython 2.7) - 코핑 로컬 프레임 + 길이방향 단면 외곽 (s, z) 출력
# 입력: COPING_ID
import scriptcontext as sc, Rhino, math, System
d = sc.doc
tol = d.ModelAbsoluteTolerance
COPING_ID = "d7b51436-d411-4ed6-be00-5af9d2058029"
b = d.Objects.FindId(System.Guid(COPING_ID)).Geometry
# 1) 길이방향 각도: 수평 edge 방향 히스토그램
angs = {}
for e in b.Edges:
v = e.PointAtEnd - e.PointAtStart
if abs(v.Z) < 1 and v.Length > 1000:
a = round(math.degrees(math.atan2(v.Y, v.X)) % 180.0, 3)
angs[a] = angs.get(a, 0) + v.Length
out = []
for k in sorted(angs.keys()):
out.append("ang %s total len %d" % (k, round(angs[k])))
# 길이방향 = bbox 범위와 맞는 쪽 선택 (대호지교 P1: 19.105)
ANG = 19.105
a = math.radians(ANG)
u = Rhino.Geometry.Vector3d(math.cos(a), math.sin(a), 0)
c = b.GetBoundingBox(True).Center
# 2) 단면
pl = Rhino.Geometry.Plane(c, u, Rhino.Geometry.Vector3d.ZAxis)
ok, crvs, pts = Rhino.Geometry.Intersect.Intersection.BrepPlane(b, pl, tol)
for sg in crvs[0].DuplicateSegments():
p0 = sg.PointAtStart; p1 = sg.PointAtEnd
out.append("%.1f,%.1f -> %.1f,%.1f" % ((p0 - c) * u, p0.Z, (p1 - c) * u, p1.Z))
sc.sticky["frame"] = (ANG, c)
result = "\n".join(out)
print(result)
@@ -0,0 +1,59 @@
# execute_rhino_code (IronPython 2.7) - 형식 VII (C8/C10) 생성 예: 단면 오프셋 120 교점 + 고정길이 + KDS 필렛
# 단면(대호지교 P1, s>0 쪽): 끝면 s=12100 (z9238~10738), 끝헌치 (12100,9238)->(8100,7738),
# 바닥 z=7738, 내측헌치 (4900,7738)->(3400,8238)
import scriptcontext as sc, Rhino, math, System
from Rhino.Geometry import Point3d, Vector3d, Polyline, Curve, Brep, PipeCapMode
d = sc.doc
tol = d.ModelAbsoluteTolerance; atol = d.ModelAngleToleranceRadians
COV = 120.0; r = 12.7; Rc = 76.2 + 12.7 # H25: 3db + db/2
A_LEG = 630.0; D_LEN = 2331.0 # 재료표 a, d
# --- 2D 오프셋선 교점 ---
sE = 12100.0 - COV # 끝면 오프셋
dx = -4000.0; dz = -1500.0; Ln = math.sqrt(dx*dx + dz*dz)
nx = -1500.0/Ln; nz = 4000.0/Ln # 안쪽 법선
p0s = 12100.0 + COV*nx; p0z = 9238.0 + COV*nz
t = (sE - p0s)/dx; Pab = (sE, p0z + t*dz)
zf = 7738.0 + COV
t = (zf - p0z)/dz; Pbc = (p0s + t*dx, zf)
dx2 = -1500.0; dz2 = 500.0; L2 = math.sqrt(dx2*dx2 + dz2*dz2)
q0s = 4900.0 + COV*500.0/L2; q0z = 7738.0 + COV*1500.0/L2
t = (zf - q0z)/dz2; Pcd = (q0s + t*dx2, zf)
D = (Pcd[0] + D_LEN*dx2/L2, zf + D_LEN*dz2/L2)
A = (sE, Pab[1] + A_LEG)
pts2 = [A, Pab, Pbc, Pcd, D] # 결과: b=4167.1, c=3158.8 (도면과 일치)
# --- 3D 생성 / 블록 교체 ---
ang = math.radians(19.105)
u = Vector3d(math.cos(ang), math.sin(ang), 0); wv = Vector3d(-math.sin(ang), math.cos(ang), 0)
c = Point3d(480714926, 153833329.5, 9331)
W_LIST = [-1630, -1375, -1125, -875, -625, -375, -125, 125, 375, 625, 875, 1125, 1375, 1630] # 1CYCLE (도면)
out = []
for nm, sg in [("C8", 1.0), ("C10", -1.0)]:
idf = d.InstanceDefinitions.Find(nm)
inst = None
for o in d.Objects.FindByLayer(d.Layers[d.Layers.FindByFullPath("P1_두부_주철근_1CYCLE::" + nm + "_H25", -1)]):
if o.ObjectType == Rhino.DocObjects.ObjectType.InstanceReference: inst = o
ok, inv = inst.Geometry.Xform.TryGetInverse()
olds = list(idf.GetObjects())
# 백업
bg = []; ba = []
for o in olds:
bg.append(o.Geometry.Duplicate()); ba.append(o.Attributes.Duplicate())
if d.InstanceDefinitions.Find(nm + "_backup") is None:
d.InstanceDefinitions.Add(nm + "_backup", "backup", Point3d.Origin, bg, ba)
newg = []; newa = []
i = 0
for w in W_LIST:
wpts = []
for p in pts2:
wpts.append(c + u*(sg*p[0]) + wv*w + Vector3d(0, 0, p[1] - c.Z))
fc = Curve.CreateFilletCornersCurve(Polyline(wpts).ToPolylineCurve(), Rc, tol, atol)
bp = Brep.CreatePipe(fc, r, False, PipeCapMode.NONE, False, tol, atol)[0]
bp.Transform(inv)
newg.append(bp); newa.append(olds[min(i, len(olds)-1)].Attributes.Duplicate())
i += 1
out.append(nm + " modify=" + str(d.InstanceDefinitions.ModifyGeometry(idf.Index, newg, newa)))
d.Views.Redraw()
result = "\n".join(out)
print(result)
+72
View File
@@ -0,0 +1,72 @@
# execute_rhino_code (IronPython 2.7) - 검토: 파이프 Brep -> 중심선 복원, 치수/각도/길이/피복
# 마크별로 나눠 실행 (타임아웃 방지). 컴프리헨션/lambda 금지 (exec 스코프 문제).
import scriptcontext as sc, Rhino, math, System
from Rhino.Geometry import Point3d, Vector3d, Line
d = sc.doc
cop = d.Objects.FindId(System.Guid("d7b51436-d411-4ed6-be00-5af9d2058029")).Geometry
LAYER = "P1_두부_주철근_1CYCLE::C8_H25"; r = 12.7
circ = 2*math.pi*r
out = []
o = None
for ob in d.Objects.FindByLayer(d.Layers[d.Layers.FindByFullPath(LAYER, -1)]):
o = ob
xf = o.Geometry.Xform
for leaf in o.InstanceDefinition.GetObjects():
g = leaf.Geometry.Duplicate(); g.Transform(xf)
Ltot = 0.0; segs = []
for f in g.Faces:
du = f.Domain(0); dv = f.Domain(1)
c1 = f.IsoCurve(1, du.Mid); c0 = f.IsoCurve(0, dv.Mid)
if c1.IsClosed and abs(c1.GetLength() - circ) < 2:
ra = f.IsoCurve(0, dv.ParameterAt(0.0)); rb = f.IsoCurve(0, dv.ParameterAt(0.5))
elif c0.IsClosed and abs(c0.GetLength() - circ) < 2:
ra = f.IsoCurve(1, du.ParameterAt(0.0)); rb = f.IsoCurve(1, du.ParameterAt(0.5))
else:
continue # 캡 면
Ltot += (ra.GetLength() + rb.GetLength())/2.0 # 중심선 전개길이
if ra.IsLinear(0.01):
segs.append(((ra.PointAtStart + rb.PointAtStart)/2.0, (ra.PointAtEnd + rb.PointAtEnd)/2.0))
# 체인 정렬
chain = [segs.pop(0)]
while len(segs) > 0:
best = None; i = 0
while i < len(segs):
a = segs[i][0]; b = segs[i][1]
for cand in [(a, False, True), (b, True, True), (b, False, False), (a, True, False)]:
if cand[2]: ref = chain[-1][1]
else: ref = chain[0][0]
dd = cand[0].DistanceTo(ref)
if best is None or dd < best[0]: best = (dd, i, cand[1], cand[2])
i += 1
s_ = segs.pop(best[1])
if best[2]: s_ = (s_[1], s_[0])
if best[3]: chain.append(s_)
else: chain.insert(0, s_)
# 가상 교점 -> 재료표식 치수/각도
pts = [chain[0][0]]; k = 0
while k < len(chain) - 1:
l1 = Line(chain[k][0], chain[k][1]); l2 = Line(chain[k+1][0], chain[k+1][1])
ok, ta, tb = Rhino.Geometry.Intersect.Intersection.LineLine(l1, l2, 0.001, False)
pts.append((l1.PointAt(ta) + l2.PointAt(tb))/2.0); k += 1
pts.append(chain[-1][1])
dims = []; angs = []; k = 0
while k < len(pts) - 1:
dims.append(round(pts[k].DistanceTo(pts[k+1]), 1)); k += 1
k = 1
while k < len(pts) - 1:
angs.append(round(math.degrees(Vector3d.VectorAngle(pts[k-1] - pts[k], pts[k+1] - pts[k])), 1)); k += 1
# 피복: 표면 격자 샘플 -> ClosestPoint (메시 정점 사용 금지)
lm = 1e9
for f in g.Faces:
du = f.Domain(0); dv = f.Domain(1); i = 0
while i <= 60:
j = 0
while j <= 24:
p = f.PointAt(du.ParameterAt(i/60.0), dv.ParameterAt(j/24.0))
dd = p.DistanceTo(cop.ClosestPoint(p))
if dd < lm: lm = dd
j += 1
i += 1
out.append("dims=%s sum=%.1f ang=%s Ldev=%.1f centerCover=%.2f" % (str(dims), sum(dims), str(angs), Ltot, lm + r))
result = "\n".join(out)
print(result)
@@ -0,0 +1,83 @@
# execute_rhino_code (IronPython 2.7) - 재료표 DXF(R12, 나중에 CP949 변환) 작성기
# rows: (마크, 형식, 직경 or '"', 개수, [A,B,C,D,E,R,X1,X2,J], 길이m, 비고, 차이여부[, 총길이m(V형 직접지정)])
# 저장 후 bash: iconv -f UTF-8 -t CP949 matl_utf8.dxf > "YYMMDD_철근재료표 작업중.dxf"
import System
UW = {"H16":1.560, "H19":2.250, "H22":3.040, "H25":3.980, "H29":5.040}
PCT = {"H16":0.03, "H19":0.03, "H22":0.06, "H25":0.06, "H29":0.06}
groups = [
("H29", [("C9", "I", "H29", 14, ["8.539", "", "", "", "", "", "", "", ""], 8.539, "재료표 동일", False)]),
# ("H25", [...]), ("H22", [...]), ("H19", [...])
]
X = [0,1200,2400,3600,4800,6800,8800,10800,12800,14800,16800,18800,20800,22800,24800,26400,28000,29600,31200,40000]
RH = 480.0; HH = 960.0
ents = []
def L(x1, y1, x2, y2, lay="TABL-LIN", ents=ents):
ents.append("0\nLINE\n8\n%s\n10\n%.3f\n20\n%.3f\n30\n0.0\n11\n%.3f\n21\n%.3f\n31\n0.0" % (lay, x1, y1, x2, y2))
def T(x, y, s, h=200.0, lay="TABL-TEX", ents=ents):
ents.append("0\nTEXT\n8\n%s\n10\n%.3f\n20\n%.3f\n30\n0.0\n40\n%.1f\n1\n%s\n7\nKOR\n72\n1\n11\n%.3f\n21\n%.3f\n31\n0.0\n73\n2" % (lay, x, y, h, s, x, y))
def mid(i, X=X):
return (X[i] + X[i+1])/2.0
T(X[-1]/2.0, 1500, "철 근 재 료 표(두부보) - Rhino 모델 실측 기준 (작업중)", 560, "TITLE")
T(1400, 300, "(SD400)")
heads = ["번 호","형 식","직 경","개 수","A","B","C","D","E","R","X1(º)","X2(º)","J"]
i = 0
while i < 13:
T(mid(i), -HH/2, heads[i]); i += 1
top2 = [("길 이","(M)"),("총길이","(M)"),("단위중량","(KG/M)"),("총중량","(TON)"),("총중량(할증)","(TON)")]
i = 0
while i < 5:
T(mid(13+i), -RH/2, top2[i][0]); T(mid(13+i), -RH*1.5, top2[i][1]); i += 1
T(mid(18), -HH/2, "비 고 (원 재료표 대비)")
L(X[0], 0, X[-1], 0); L(X[0], -HH, X[-1], -HH)
for xx in X: L(xx, 0, xx, -HH)
y = -HH
gl = 0.0; gw = 0.0; gwh = 0.0; gwl = 0.0; gwlh = 0.0
for dia, rows in groups:
tlen = 0.0
for rw in rows:
yc = y - RH/2
T(mid(0), yc, rw[0]); T(mid(1), yc, rw[1]); T(mid(2), yc, rw[2]); T(mid(3), yc, str(rw[3]))
k = 0
while k < 9:
if rw[4][k] != "": T(mid(4+k), yc, rw[4][k])
k += 1
if len(rw) > 8: tl = rw[8]
else: tl = round(rw[3]*rw[5], 3)
tlen += tl
T(mid(13), yc, "%.3f" % rw[5]); T(mid(14), yc, "%.3f" % tl)
if rw[7]: T(mid(18), yc, rw[6], 200.0, "DIFF")
else: T(mid(18), yc, rw[6])
L(X[0], y-RH, X[-1], y-RH)
for xx in X: L(xx, y, xx, y-RH)
y -= RH
w = tlen*UW[dia]/1000.0; wh = w*(1+PCT[dia]); wl = w*1.068; wlh = wl*(1+PCT[dia])
pc = "(%d%%)" % int(round(PCT[dia]*100))
T(mid(9), y-RH/2, "소 계"); T(mid(14), y-RH/2, "%.3f" % tlen); T(mid(15), y-RH/2, "%.3f" % UW[dia])
T(mid(16), y-RH/2, "%.3f" % w); T(mid(17), y-RH/2, "%.3f%s" % (wh, pc))
L(X[0], y-RH, X[-1], y-RH)
for xx in [X[0]] + X[13:]: L(xx, y, xx, y-RH)
y -= RH
T(mid(9), y-RH/2, "소 계(겹이음 할증 6.80%)"); T(mid(16), y-RH/2, "%.3f" % wl); T(mid(17), y-RH/2, "%.3f%s" % (wlh, pc))
L(X[0], y-RH, X[-1], y-RH)
for xx in [X[0]] + X[13:]: L(xx, y, xx, y-RH)
y -= RH
gl += tlen; gw += round(w, 3); gwh += round(wh, 3); gwl += round(wl, 3); gwlh += round(wlh, 3)
T(mid(9), y-RH/2, "총 계"); T(mid(14), y-RH/2, "%.3f" % gl); T(mid(16), y-RH/2, "%.3f" % gw); T(mid(17), y-RH/2, "%.3f" % gwh)
L(X[0], y-RH, X[-1], y-RH)
for xx in [X[0]] + X[13:]: L(xx, y, xx, y-RH)
y -= RH
T(mid(9), y-RH/2, "총 계(겹이음 할증 6.80%)"); T(mid(16), y-RH/2, "%.3f" % gwl); T(mid(17), y-RH/2, "%.3f" % gwlh)
L(X[0], y-RH, X[-1], y-RH)
for xx in [X[0]] + X[13:]: L(xx, y, xx, y-RH)
hdr = "0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1009\n9\n$DWGCODEPAGE\n3\nANSI_949\n0\nENDSEC\n"
tbl = "0\nSECTION\n2\nTABLES\n0\nTABLE\n2\nLTYPE\n70\n1\n0\nLTYPE\n2\nCONTINUOUS\n70\n0\n3\nSolid line\n72\n65\n73\n0\n40\n0.0\n0\nENDTAB\n"
lays = [("0",7),("TITLE",2),("TABL-LIN",8),("TABL-TEX",7),("DIFF",1),("NOTE",3)]
tbl += "0\nTABLE\n2\nLAYER\n70\n%d\n" % len(lays)
for lay in lays:
tbl += "0\nLAYER\n2\n%s\n70\n0\n62\n%d\n6\nCONTINUOUS\n" % (lay[0], lay[1])
tbl += "0\nENDTAB\n0\nTABLE\n2\nSTYLE\n70\n1\n0\nSTYLE\n2\nKOR\n70\n0\n40\n0.0\n41\n1.0\n50\n0.0\n71\n0\n42\n200.0\n3\nmalgun.ttf\n4\n\n0\nENDTAB\n0\nENDSEC\n"
body = "0\nSECTION\n2\nENTITIES\n" + "\n".join(ents) + "\n0\nENDSEC\n0\nEOF\n"
p = "C:\\temp\\matl_utf8.dxf" # 스크래치 경로로 변경해서 사용
System.IO.File.WriteAllText(p, (hdr + tbl + body).replace("\n", "\r\n"), System.Text.UTF8Encoding(False))
result = "ents=%d total=%.3fm %.3ft" % (len(ents), gl, gw)
print(result)
@@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
# 05_realign_base_pieces.py (대호지교 P1, 2026-10-06 검증)
# 목적: 수직전단철근 박스(C12/C14/C16/C18 등)에서 바닥(⊔) 조각이 뚜껑(⊓) 평면에서 s 방향으로
# 어긋난 것을 되돌린다. 피복(헌치 경사면에 수직인 거리)은 유지하려고 경사 접선 방향으로 움직이고,
# 바닥 상단을 뚜껑 윗변 중심에서 다시 자른다(겹침 510 유지).
# 실행: rhino_mcp execute_rhino_code (IronPython 2.7). MARK 하나씩 실행 (소켓 타임아웃 방지).
# 주의: lambda/리스트컴프리헨션 금지(exec 스코프), f-string 금지. 결과는 JSON 파일로 쓰고 따로 읽는다.
# 구조 가정: 마크 idef -> 스테이션 idef -> 박스 idef(리프 Brep: 뚜껑, 바닥, [연장 조각...])
import Rhino, json, io, os, math
import scriptcontext as sc
doc = sc.doc
MARK = "C12" # C12 / C14 / C16 / C18 ...
OX, OY = 480714926.0, 153833329.5 # 코핑 로컬 원점 (코핑 bbox 중심)
ANG = math.atan2(4255.0, 12284.0) # 코핑 축 각도 (두 기둥 중심 연결선)
# 헌치 경사 dz/ds (그 스테이션에서 콘크리트 밑면의 기울기): 끝단 헌치 ±0.375, 안쪽 헌치 ±1/3
SLOPES = {"C12": -0.375, "C18": 0.375, "C14": 1.0 / 3.0, "C16": -1.0 / 3.0}
RBAR = 11.1 # 철근 반지름 (H22)
TOL = 0.5 # 이보다 작은 어긋남은 건너뜀
ca = math.cos(ANG)
sa = math.sin(ANG)
k = SLOPES[MARK]
folder = os.path.dirname(doc.Path)
out = os.path.join(folder, "fix_result_" + MARK + ".json")
inst = None
for o in doc.Objects:
g = o.Geometry
if isinstance(g, Rhino.Geometry.InstanceReferenceGeometry):
if doc.Layers[o.Attributes.LayerIndex].FullPath.endswith(MARK + "_H22"):
inst = g
mxf = inst.Xform
midf = doc.InstanceDefinitions.FindId(inst.ParentIdefId)
# 박스 idef 이름 -> 대표 인스턴스 월드 변환
boxT = {}
for ob in midf.GetObjects():
og = ob.Geometry
if not isinstance(og, Rhino.Geometry.InstanceReferenceGeometry):
continue
sidf = doc.InstanceDefinitions.FindId(og.ParentIdefId)
sxf = mxf * og.Xform
for b in sidf.GetObjects():
bg = b.Geometry
if isinstance(bg, Rhino.Geometry.InstanceReferenceGeometry):
bn = doc.InstanceDefinitions.FindId(bg.ParentIdefId).Name
if bn not in boxT:
boxT[bn] = sxf * bg.Xform
report = []
names = list(boxT.keys())
names.sort()
for bn in names:
T = boxT[bn]
ok, inv = T.TryGetInverse()
bidf = doc.InstanceDefinitions.Find(bn)
objs = bidf.GetObjects()
geoms = []
attrs = []
info = [] # [s, area, bbox minZ, bbox maxZ] (월드)
for ob in objs:
bg = ob.Geometry
amp = Rhino.Geometry.AreaMassProperties.Compute(bg)
c = amp.Centroid
c.Transform(T)
s = (c.X - OX) * ca + (c.Y - OY) * sa
w = bg.Duplicate()
w.Transform(T)
bbox = w.GetBoundingBox(True)
info.append([s, bg.GetArea(), bbox.Min.Z, bbox.Max.Z])
geoms.append(bg.Duplicate())
attrs.append(ob.Attributes.Duplicate()) # ColorFromParent 보존
# 면적 큰 2개 = 뚜껑/바닥 (연장 조각은 작다). 뚜껑 = bbox 최저 z가 더 높은 쪽
i1 = -1
i2 = -1
for i in range(len(info)):
if i1 < 0 or info[i][1] > info[i1][1]:
i2 = i1
i1 = i
elif i2 < 0 or info[i][1] > info[i2][1]:
i2 = i
if info[i1][2] > info[i2][2]:
lid = i1
base = i2
else:
lid = i2
base = i1
dev = info[base][0] - info[lid][0]
row = {"box": bn, "n": len(info), "dev_before": round(dev, 3)}
if abs(dev) <= TOL:
row["action"] = "skip"
report.append(row)
continue
bkname = bn + "_bk1006b" # 백업 idef
if doc.InstanceDefinitions.Find(bkname) is None:
bl = []
al = []
for ob in objs:
bl.append(ob.Geometry.Duplicate())
al.append(ob.Attributes.Duplicate())
doc.InstanceDefinitions.Add(bkname, "backup before s-realign", Rhino.Geometry.Point3d.Origin, bl, al)
ds = -dev
dz = k * ds # 경사 접선 방향 이동 -> 수직 피복 유지
v = Rhino.Geometry.Vector3d(ca * ds, sa * ds, dz)
v.Transform(inv) # 월드 이동벡터 -> idef 로컬
ztop = info[lid][3] - RBAR # 뚜껑 윗변 중심 z (월드)
pl = Rhino.Geometry.Plane(Rhino.Geometry.Point3d(0.0, 0.0, ztop), Rhino.Geometry.Vector3d.ZAxis)
pl.Transform(inv)
newgeoms = []
for i in range(len(geoms)):
if abs(info[i][0] - info[lid][0]) > TOL: # 뚜껑 평면에서 벗어난 조각만 이동
m = geoms[i]
m.Translate(v)
chk = m.Duplicate()
chk.Transform(T)
if chk.GetBoundingBox(True).Max.Z > ztop + 0.05:
tr = m.Trim(pl, 0.01) # +Z 쪽 제거
if tr is not None and len(tr) > 0:
m = tr[0]
newgeoms.append(m)
else:
newgeoms.append(geoms[i])
doc.InstanceDefinitions.ModifyGeometry(bidf.Index, newgeoms, attrs) # 3-인자: 속성 보존
row["action"] = "fixed"
row["dz"] = round(dz, 3)
report.append(row)
doc.Views.Redraw()
txt = json.dumps(report)
f = io.open(out, "w", encoding="utf-8")
try:
f.write(txt)
except TypeError:
f.write(txt.decode("utf-8"))
f.close()
@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
# 06_flip_hooks.py (대호지교 P1, 2026-10-06 검증)
# 목적: 수평전단철근(VIII: 직선 + 양끝 135° 수평 갈고리) 중 폭방향 특정 위치(|t|≈TARGET_T)의 철근만
# 자기 축을 기준으로 거울 반전해서 갈고리 방향을 반대로 뒤집는다 (길이 불변).
# 실행: execute_rhino_code, MARKS 한 번에 돌려도 되지만 개수가 EXPECTED와 다르면 수정하지 않고 보고만 한다.
# 구조 가정: 마크 idef(C20/C22/C24)의 리프 Brep = 철근 1본, 인스턴스 변환은 보통 단위행렬(리프가 월드 좌표).
import Rhino, json, io, os, math
import scriptcontext as sc
doc = sc.doc
OX, OY = 480714926.0, 153833329.5
ANG = math.atan2(4255.0, 12284.0)
TARGET_T = 650.0 # 대상 철근의 |t| (centroid 기준, 갈고리 때문에 축에서 ~2mm 벗어남)
T_BAND = 40.0
EXPECTED = {"C20": 8, "C22": 10, "C24": 12} # 위치 2곳 x 단 수 x 구역 수 (불일치 시 중단)
ca = math.cos(ANG)
sa = math.sin(ANG)
folder = os.path.dirname(doc.Path)
res = {}
plan = {}
es = Rhino.DocObjects.ObjectEnumeratorSettings()
es.HiddenObjects = True # 꺼진 레이어 객체도 열거 (기본값은 제외함!)
es.NormalObjects = True
es.LockedObjects = True
for mk in EXPECTED:
inst = None
for o in doc.Objects.GetObjectList(es):
g = o.Geometry
if isinstance(g, Rhino.Geometry.InstanceReferenceGeometry):
if doc.Layers[o.Attributes.LayerIndex].FullPath.endswith(mk + "_H16"):
inst = g
T = inst.Xform
ok, inv = T.TryGetInverse()
idf = doc.InstanceDefinitions.FindId(inst.ParentIdefId)
objs = idf.GetObjects()
sel = []
for idx in range(len(objs)):
w = objs[idx].Geometry.Duplicate()
w.Transform(T)
c = Rhino.Geometry.AreaMassProperties.Compute(w).Centroid
t = -(c.X - OX) * sa + (c.Y - OY) * ca
if abs(abs(t) - TARGET_T) < T_BAND:
sel.append(idx)
plan[mk] = [T, inv, idf, objs, sel]
res[mk] = {"selected": len(sel), "expected": EXPECTED[mk]}
good = True
for mk in EXPECTED:
if res[mk]["selected"] != EXPECTED[mk]:
good = False
res["all_counts_match"] = good
if good:
for mk in EXPECTED:
T = plan[mk][0]
inv = plan[mk][1]
idf = plan[mk][2]
objs = plan[mk][3]
sel = plan[mk][4]
bk = mk + "_bk1006c"
if doc.InstanceDefinitions.Find(bk) is None:
bl = []
al = []
for ob in objs:
bl.append(ob.Geometry.Duplicate())
al.append(ob.Attributes.Duplicate())
doc.InstanceDefinitions.Add(bk, "backup before hook flip", Rhino.Geometry.Point3d.Origin, bl, al)
newg = []
attrs = []
for idx in range(len(objs)):
attrs.append(objs[idx].Attributes.Duplicate())
bg = objs[idx].Geometry
if idx not in sel:
newg.append(bg.Duplicate())
continue
w = bg.Duplicate()
w.Transform(T)
cen = w.GetBoundingBox(True).Center
# 철근 중간 s에서 s-수직 평면으로 자르면 직선부 단면 원 -> 그 중심이 철근 축 위의 점
plane = Rhino.Geometry.Plane(cen, Rhino.Geometry.Vector3d(ca, sa, 0.0))
curves = Rhino.Geometry.Intersect.Intersection.BrepPlane(w, plane, 0.01)[1]
axis_pt = Rhino.Geometry.AreaMassProperties.Compute(curves[0]).Centroid
nrm = Rhino.Geometry.Vector3d(-sa, ca, 0.0) # 폭방향 t
w.Transform(Rhino.Geometry.Transform.Mirror(axis_pt, nrm))
w.Flip() # 거울 반전 뒤 법선 복구
w.Transform(inv)
newg.append(w)
doc.InstanceDefinitions.ModifyGeometry(idf.Index, newg, attrs)
doc.Views.Redraw()
f = io.open(os.path.join(folder, "flip_result.json"), "w", encoding="utf-8")
txt = json.dumps(res)
try:
f.write(txt)
except TypeError:
f.write(txt.decode("utf-8"))
f.close()
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# 07_ctc_extract.py (대호지교 P1, 2026-10-06 검증)
# 목적: 철근 마크별 리프 Brep 중심점을 코핑 로컬 좌표 (s=길이방향, t=폭방향, z)로 뽑아 JSON 저장.
# 이후 PowerShell/awk로 고유값 정렬 -> 차이 = 도면 @간격(CTC)과 대조.
# - 주철근: 고유 t 의 차이 (폭방향 간격)
# - 스터럽(C12~C18): 고유 s 의 차이 (길이방향 간격) - 주의: 뚜껑/바닥 조각은 s가 서로 다를 수 있으니 클러스터 시작값만 쓰지 말 것
# - C25 등 ⊓: 고유 s (길이방향) 와 z (단 간격)
# 실행: 마크 하나(MARKS 1~2개)씩 돌릴 것. 전체를 한 번에 하면 AreaMassProperties 때문에 소켓 타임아웃.
# 타임아웃이 나도 스크립트는 Rhino 안에서 계속 도므로, 결과 파일이 생길 때까지 기다린다(백그라운드 until 루프).
import Rhino, json, io, os, math
import scriptcontext as sc
doc = sc.doc
MARKS = ["C1", "C2", "C3"]
OX, OY = 480714926.0, 153833329.5
ANG = math.atan2(4255.0, 12284.0)
ca = math.cos(ANG)
sa = math.sin(ANG)
folder = os.path.dirname(doc.Path)
out = os.path.join(folder, "ctc_points_" + "_".join(MARKS) + ".json")
es = Rhino.DocObjects.ObjectEnumeratorSettings()
es.HiddenObjects = True # 꺼진 레이어 객체 포함
es.NormalObjects = True
es.LockedObjects = True
res = {}
for o in doc.Objects.GetObjectList(es):
g = o.Geometry
ly = doc.Layers[o.Attributes.LayerIndex].FullPath
if "CYCLE" not in ly:
continue
mk = ly.split("::")[-1].split("_")[0]
if mk not in MARKS:
continue
pts = res.get(mk, [])
leaves = []
if isinstance(g, Rhino.Geometry.InstanceReferenceGeometry):
stack = [[doc.InstanceDefinitions.FindId(g.ParentIdefId), g.Xform]]
while len(stack) > 0:
item = stack.pop()
for ob in item[0].GetObjects():
og = ob.Geometry
if isinstance(og, Rhino.Geometry.InstanceReferenceGeometry):
stack.append([doc.InstanceDefinitions.FindId(og.ParentIdefId), item[1] * og.Xform])
elif isinstance(og, Rhino.Geometry.Brep):
c = Rhino.Geometry.AreaMassProperties.Compute(og).Centroid
c.Transform(item[1]) # 중첩 변환: 월드 = 부모 * 자식 * 점
leaves.append(c)
elif isinstance(g, Rhino.Geometry.Brep): # C26처럼 인스턴스가 아닌 낱개 Brep
leaves.append(Rhino.Geometry.AreaMassProperties.Compute(g).Centroid)
for c in leaves:
dx = c.X - OX
dy = c.Y - OY
pts.append([round(dx * ca + dy * sa, 1), round(-dx * sa + dy * ca, 1), round(c.Z, 1)])
res[mk] = pts
txt = json.dumps(res)
f = io.open(out, "w", encoding="utf-8")
try:
f.write(txt)
except TypeError:
f.write(txt.decode("utf-8"))
f.close()
+195
View File
@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
# 08_clash_check.py (대호지교 P1, 2026-10-06) -- READ ONLY: 모델을 바꾸지 않고 CSV/JSON만 쓴다.
# 중심선 간섭 검사: 각 철근(리프 Brep)의 파이프면에서 중심선(원주 반대편 두 isocurve의 중점)을 뽑아 선분화하고,
# RTree 로 후보를 찾아 선분-선분 최단거리 < (rA + rB) - 0.5 이면 간섭(관입 = rA + rB - 거리).
# 같은 박스(뚜껑+바닥+연장 조각)끼리는 겹침이음 모델링이므로 제외한다.
# 실행: execute_rhino_code (IronPython 2.7). 소켓 타임아웃이 나도 Rhino 안에서 계속 도므로 결과 파일이 생길 때까지 기다린다.
# 주의: 함수/lambda/컴프리헨션 금지(exec 스코프). 콜백만 기본인자로 변수를 묶어 정의한다.
import Rhino, json, io, os, math, System
import scriptcontext as sc
doc = sc.doc
folder = os.path.dirname(doc.Path)
OX, OY = 480714926.0, 153833329.5
ANG = math.atan2(4255.0, 12284.0)
ca = math.cos(ANG)
sa = math.sin(ANG)
RAD = {"C9": 14.3, "C26": 9.55}
for m in ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C10", "C11"]:
RAD[m] = 12.7
for m in ["C12", "C13", "C14", "C15", "C16", "C17", "C18"]:
RAD[m] = 11.1
for m in ["C19", "C20", "C21", "C22", "C23", "C24", "C25"]:
RAD[m] = 7.95
MIN_PEN = 0.5
es = Rhino.DocObjects.ObjectEnumeratorSettings()
es.HiddenObjects = True # 꺼진 레이어도 포함
es.NormalObjects = True
es.LockedObjects = True
cache = {} # 리프 id -> 로컬 중심선 폴리라인 목록 [[Point3d,...], ...]
segs = [] # [mark, barIdx, group, p0, p1]
bars = [] # [mark, label]
tree = Rhino.Geometry.RTree()
group_counter = [0]
# ---- collect centerline segments of every rebar mark
for o in doc.Objects.GetObjectList(es):
g = o.Geometry
ly = doc.Layers[o.Attributes.LayerIndex].FullPath
if "CYCLE" not in ly or not ly.startswith("P1_"): # P2/P10/P12 복사본(숨김 레이어)은 제외
continue
mk = ly.split("::")[-1].split("_")[0]
if mk not in RAD:
continue
r = RAD[mk]
circ = 2.0 * math.pi * r
items = [] # [leaf id, brep, xform, group]
if isinstance(g, Rhino.Geometry.InstanceReferenceGeometry):
stack = [[doc.InstanceDefinitions.FindId(g.ParentIdefId), g.Xform, 0, -1]]
while len(stack) > 0:
it = stack.pop()
idf = it[0]
for ob in idf.GetObjects():
og = ob.Geometry
if isinstance(og, Rhino.Geometry.InstanceReferenceGeometry):
grp = it[3]
if it[2] + 1 == 2: # 박스 인스턴스 = 한 그룹
group_counter[0] += 1
grp = group_counter[0]
stack.append([doc.InstanceDefinitions.FindId(og.ParentIdefId), it[1] * og.Xform, it[2] + 1, grp])
elif isinstance(og, Rhino.Geometry.Brep):
grp = it[3]
if grp < 0:
group_counter[0] += 1
grp = group_counter[0]
items.append([ob.Id, og, it[1], grp])
elif isinstance(g, Rhino.Geometry.Brep):
group_counter[0] += 1
items.append([o.Id, g, Rhino.Geometry.Transform.Identity, group_counter[0]])
for it in items:
key = str(it[0])
if key not in cache:
lst = []
for f in it[1].Faces:
du = f.Domain(0)
dv = f.Domain(1)
c1 = f.IsoCurve(1, du.Mid)
c0 = f.IsoCurve(0, dv.Mid)
ra = None
rb = None
along_u = True
if c1 is not None and c1.IsClosed and abs(c1.GetLength() - circ) < 3.0:
ra = f.IsoCurve(0, dv.ParameterAt(0.0))
rb = f.IsoCurve(0, dv.ParameterAt(0.5))
elif c0 is not None and c0.IsClosed and abs(c0.GetLength() - circ) < 3.0:
along_u = False
ra = f.IsoCurve(1, du.ParameterAt(0.0))
rb = f.IsoCurve(1, du.ParameterAt(0.5))
else:
continue
pc = ra.ToPolyline(0, 0, 0.05, 0, 0, 0.3, 0, 1.0e6, True)
pts = []
for i in range(pc.PointCount):
p = pc.Point(i)
okc, t = ra.ClosestPoint(p)
# 중심 = 그 위치 단면 원의 중심 (v0.5 점은 NURBS 매개변수가 균등하지 않아 반대편이 아닐 수 있음)
if along_u:
circ_c = f.IsoCurve(1, t)
else:
circ_c = f.IsoCurve(0, t)
okr, cir = circ_c.TryGetCircle(0.1)
if okr:
pts.append(cir.Center)
else:
q = rb.PointAt(t)
pts.append(Rhino.Geometry.Point3d((p.X + q.X) / 2.0, (p.Y + q.Y) / 2.0, (p.Z + q.Z) / 2.0))
lst.append(pts)
cache[key] = lst
bidx = len(bars)
bars.append([mk, key[:8]])
for pl in cache[key]:
pts = []
for p in pl:
q = Rhino.Geometry.Point3d(p)
q.Transform(it[2])
pts.append(q)
for i in range(len(pts) - 1):
if pts[i].DistanceTo(pts[i + 1]) < 1.0e-6:
continue
sidx = len(segs)
segs.append([mk, bidx, it[3], pts[i], pts[i + 1]])
bb = Rhino.Geometry.BoundingBox([pts[i], pts[i + 1]]) # 순서 무관 (min/max 자동) - 두 점 생성자는 순서가 뒤바뀌면 무효 상자가 됨
bb.Inflate(r + 1.0)
tree.Insert(bb, sidx)
# ---- search overlapping segments
pairs = {}
hits = []
cur = [0]
def cb(sender, e, hits=hits, cur=cur):
if e.Id > cur[0]:
hits.append(e.Id)
handler = System.EventHandler[Rhino.Geometry.RTreeEventArgs](cb)
for i in range(len(segs)):
sA = segs[i]
ra_ = RAD[sA[0]]
lineA = Rhino.Geometry.Line(sA[3], sA[4])
bb = Rhino.Geometry.BoundingBox([sA[3], sA[4]])
bb.Inflate(ra_ + 1.0)
del hits[:]
cur[0] = i
tree.Search(bb, handler)
for j in hits:
sB = segs[j]
if sB[1] == sA[1] or sB[2] == sA[2]:
continue
rb_ = RAD[sB[0]]
d = lineA.MinimumDistanceTo(Rhino.Geometry.Line(sB[3], sB[4]))
pen = ra_ + rb_ - d
if pen > MIN_PEN:
pk = (sA[1], sB[1])
old = pairs.get(pk)
if old is None or pen > old[0]:
mx = (sA[3].X + sA[4].X + sB[3].X + sB[4].X) / 4.0
my = (sA[3].Y + sA[4].Y + sB[3].Y + sB[4].Y) / 4.0
mz = (sA[3].Z + sA[4].Z + sB[3].Z + sB[4].Z) / 4.0
pairs[pk] = [pen, mx, my, mz]
# ---- write CSV (UTF-8 BOM) + summary
rows = []
for pk in pairs:
v = pairs[pk]
a = bars[pk[0]]
b = bars[pk[1]]
dx = v[1] - OX
dy = v[2] - OY
rows.append([a[0], a[1], b[0], b[1], round(v[0], 1), round(dx * ca + dy * sa, 0), round(-dx * sa + dy * ca, 0), round(v[3], 0)])
rows.sort()
f = io.open(os.path.join(folder, "261006_간섭재검사_목록.csv"), "w", encoding="utf-8")
f.write(u"마크A,철근A,마크B,철근B,관입(mm),s,w,z\n")
for r_ in rows:
f.write(u"%s,%s,%s,%s,%s,%s,%s,%s\n" % tuple(r_))
f.close()
summ = {}
for r_ in rows:
k = r_[0] + "|" + r_[2]
if r_[0] > r_[2]:
k = r_[2] + "|" + r_[0]
s_ = summ.get(k, [0, 0.0])
s_[0] += 1
if r_[4] > s_[1]:
s_[1] = r_[4]
summ[k] = s_
res = {"bars": len(bars), "segments": len(segs), "pairs": len(rows), "by_markpair": summ}
f = io.open(os.path.join(folder, "261006_간섭재검사_요약.json"), "w", encoding="utf-8")
txt = json.dumps(res, ensure_ascii=False)
try:
f.write(txt)
except TypeError:
f.write(txt.decode("utf-8"))
f.close()