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

+29
View File
@@ -0,0 +1,29 @@
# Code 폴더
**코드·스크립트 산출물**을 저장하는 폴더입니다. ([F3])
> 본 문서는 `.claude/taskexecutionagent.md` **v0.3.01**의 [F3]·[V13]에 종속됩니다.
## 규칙
- 소스 코드, 스크립트, 설정 파일은 모두 이 폴더에 **해당 언어의 표준 확장자**로 저장합니다.
- 코드에 대한 문서화·설명은 `.md`로 작성하여 `DOC/`에 둡니다.
- 언어·모듈·기능별로 하위 폴더를 만들어 분리합니다.
- **코드 파일(`.py`·`.js`·`.html` 등)과 바이너리(`.pptx`·`.dxf` 등)를 `DOC/` 하위에 두지 않습니다.**
## 스크립트 보관 의무 ([F3])
**산출물 생성·검증·변환에 사용한 스크립트는 결과 채택 여부와 무관하게 반드시 보관합니다.**
- 대상: 패치 스크립트, 검증 스크립트, 파서, 렌더러, 대조·집계 도구 등 **일회성으로 작성한 것 포함**
- **검증 스크립트가 보관되지 않은 경우 해당 [Step 3] 검증 결과는 무효**로 간주하며, `PASS` 판정을 인용할 수 없습니다.
## 권장 하위 구조
```
Code/
├── patch/ # 일회성 패치·변환 스크립트 (재실행 시 앵커 불일치로 안전 중단되도록 작성)
├── verify/ # 회귀 검증 스크립트 (반복 실행 가능해야 함)
├── deploy/ # 배포·빌드 스크립트
└── <언어·모듈별>/
```
## 스크립트 작성 시
- 원본을 수정하는 스크립트는 **앵커가 1회 매칭되지 않으면 즉시 중단**하도록 작성합니다. 이중 적용을 구조적으로 막습니다. ([F13])
- 검증 스크립트는 **판정 방법과 모집단을 결과에 함께 출력**합니다. 전수와 표본을 구분 표기합니다. ([Step 3])
- 환경적 함정(느린 마운트, 인코딩, 폰트 누락, 경로 특성)은 `context-migration`에 남겨 다음 세션이 재발견하지 않게 합니다.
+61
View File
@@ -0,0 +1,61 @@
"""Add the column rebar (G2: P1 main, P2 hoops, P3 ties) to the app pier 'Ramp-A교P1'.
params = drawn / chain values (판독/g2_values.json, DOC 판독서 v0.1.02 §11) and user decisions K1-K4 (2026-10-13);
expected = schedule L / N / r (D4 재료표(기둥), D3 철근 상세) and the D2 chain anchor, typed in by hand.
Backs up params.json / expected.json to data/Ramp-A교P1/backup/2026-10-13_g2/ first. Refuses if G2 is already in scope.
"""
import json
import shutil
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
APP = HERE.parents[1] / "Code" / "교각모델링앱"
D = APP / "data" / "Ramp-A교P1"
V = json.loads((HERE / "판독" / "g2_values.json").read_text(encoding="utf-8"))
P = json.loads((D / "params.json").read_text(encoding="utf-8"))
X = json.loads((D / "expected.json").read_text(encoding="utf-8"))
if "G2" in P["scope"]:
sys.exit("G2 already in scope")
bk = D / "backup" / "2026-10-13_g2"
bk.mkdir(parents=True, exist_ok=True)
for f in ("params.json", "expected.json"):
shutil.copy2(D / f, bk / f)
A = 45492.0 # chain anchor (D2 '띠철근외측' / '크로스타이' common tick)
mf, ms = V["main_front"], V["main_section"]
P["scope"] = ["G1", "G2", "G5"]
P["builders"]["G2"] = "column_rebar_straight.py"
P["rebar_marks"] = {"P1": {"dia": "H35", "shape": "I"}, "P2": {"dia": "H16", "shape": "hoop-lap"},
"P3": {"dia": "H16", "shape": "tie-90/135"}}
P["rebar_marks_src"] = "D4 철근 재료표(기둥) 기호·직경, D3 철근 상세 형상. 말뚝 철근망(P1-1·P1-2·P2-1·P3-1·P3-2)은 범위 밖"
P["display"]["rebar_palette"]["35"] = [148, 103, 189]
P["display"]["rebar_palette_src_H35"] = "H35 (148,103,189) Agent 제안 2026-10-13 — R8 등재 확정 대기"
P["column_rebar"] = {
"form": "straight",
"main": {"mark": "P1", "count": ms["count"], "start_deg": 0.0, "radius": ms["radius"],
"z0": mf["bottom_EL"], "z1": mf["top_EL"], "coupler_length": 40.0},
"hoops": {"mark": "P2", "radius": 901.0, "levels": [43542.0 + 150 * k for k in range(107)],
"a": 2829.0, "lap": 650.0, "hook": 200.0, "join_deg": [90, 0]},
"ties": {"mark": "P3", "levels": [43692.0 + 300 * k for k in range(54)], "straight": 1832.0, "leg": 200.0,
"hook_tail": 150.0, "y_lift": 16.0},
"design_lengths": {"P1": mf["top_EL"] - mf["bottom_EL"], "P2": 2829.0 + 650.0 + 2 * 200.0,
"P3": 200.0 + 1832.0 + 141.0 + 100.0},
"src": {"main": "D2 단면 C-C 기호 54개 r 875 (0°부터 6.667°), 정면 작도 EL 43.492~59.752; 하단 커플러 40 = L 16,300 - 작도 16,260 (K1)",
"hoops": "재료표 r 901, a 2,829, b 200, j 650x2; 단 = 사슬 격자 43.542 + 150k, 107단 (K2); 이음 90°/0° 교번 (K4 [추론])",
"ties": "재료표 a 200, b 1,832, c 141, d 100; 단 = 43.692 + 300k, 54단, X·Y 1단 2개, Y +16 (K3 [추론]); 다리·갈고리 좌우 교번 (K4 [추론]); 갈고리 꼬리 작도 150 [추론]",
"design_lengths": "P1 작도 길이(+커플러 40 = L), P2 a+j+2b 한 조각(두 조각 = L 7,758), P3 a+b+c+d = 2,273 (L 2,274 반올림)"},
}
X["checkers"]["G2"] = "g2_straight.py"
X["negative"]["G2"] = [{"mark": "P1", "d": [5, 0, 0]}, {"mark": "P2", "d": [0, 0, 5]}]
X["rebar_palette"]["35"] = [148, 103, 189]
X["G2"] = {"schedule_L": {"P1": 16300, "P2": 7758, "P3": 2274}, "schedule_n": {"P1": 54, "P2": 107, "P3": 108},
"dia": {"P1": 35, "P2": 16, "P3": 16}, "chain_anchor": A,
"main": {"mark": "P1", "radius": 875, "divisions": 54, "start_deg": 0.0, "below_anchor": 2000, "coupler": 40},
"hoops": {"mark": "P2", "radius": 901, "pitch": 150, "below": 13},
"ties": {"mark": "P3", "pitch": 300, "below": 6, "per_level": 2, "y_lift": 16},
"src": "D4 재료표(기둥) L·N, D3 상세 r 901; D2 단면 '125, 875, 875, 125' DIVISION 54; D2 사슬 '2,000', H4 14@150, H5 78@150, "
"H1 7@300, H2 39@300 (기점 EL 45.492); K1 커플러 40, K2 hoop 13단 아래, K3 Y +16 (사용자 결정 2026-10-13)"}
(D / "params.json").write_text(json.dumps(P, ensure_ascii=False, indent=1), encoding="utf-8")
(D / "expected.json").write_text(json.dumps(X, ensure_ascii=False, indent=1), encoding="utf-8")
print("G2 added; backup", bk)
+75
View File
@@ -0,0 +1,75 @@
"""Add the shaft cage (G3: P1-1, P1-2, P2-1, P3-1, P3-2, anti-uplift plate) to the app pier 'Ramp-A교P1'.
params = drawn / table values (판독/g3_values.json, 판독서 v0.1.04 §12) and user decisions S1-S6 (2026-10-13);
expected = D4 schedule L / N, 구조물 치수표 chains from the shaft top, section radii, typed in by hand.
Backs up params.json / expected.json to data/Ramp-A교P1/backup/2026-10-13_g3/ first. Refuses if G3 is in scope.
"""
import json
import math
import shutil
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
D = HERE.parents[1] / "Code" / "교각모델링앱" / "data" / "Ramp-A교P1"
V = json.loads((HERE / "판독" / "g3_values.json").read_text(encoding="utf-8"))
P = json.loads((D / "params.json").read_text(encoding="utf-8"))
X = json.loads((D / "expected.json").read_text(encoding="utf-8"))
if "G3" in P["scope"]:
sys.exit("G3 already in scope")
bk = D / "backup" / "2026-10-13_g3"
bk.mkdir(parents=True, exist_ok=True)
for f in ("params.json", "expected.json"):
shutil.copy2(D / f, bk / f)
TOP = 45452.0 # shaft top (D1)
hoops = [TOP - 110 - 150 * k for k in range(14)]
hoops += [hoops[-1] - 300 * k for k in range(1, 35)]
ties = [TOP - 260 - 300 * k for k in range(7)]
ties += [ties[-1] - 600 * k for k in range(1, 18)]
assert len(hoops) == 48 and len(ties) == 24 and hoops[0] - hoops[-1] == 8750 + 3400
zb = 32752.0 + 100 # U bottom (centreline), shaft bottom + 100
leg_top = zb + math.sqrt(581 ** 2 - 135 ** 2)
jj = V["J-J"]
P["scope"] = ["G1", "G2", "G3", "G5"]
P["builders"]["G3"] = "shaft_rebar.py"
P["rebar_marks"].update({"P1-1": {"dia": "H35", "shape": "I"}, "P1-2": {"dia": "H22", "shape": "I"},
"P2-1": {"dia": "H16", "shape": "hoop-lap"}, "P3-1": {"dia": "H16", "shape": "tie-90/135"},
"P3-2": {"dia": "H16", "shape": "U"}})
P["rebar_marks_src"] = "D4 철근 재료표(기둥) 기호·직경, D3 철근 상세 형상 (기둥 P1~P3 + 말뚝 P1-1~P3-2)"
P["shaft_rebar"] = {
"form": "shaft_cage",
"main": {"count": jj["bar_count"], "start_deg": jj["first_deg"], "radius": 999.5, "coupler_length": 0.0,
"pieces": [{"mark": "P1-2", "z0": hoops[-1], "z1": hoops[-1] + 3400},
{"mark": "P1-1", "z0": hoops[-1] + 3400, "z1": hoops[0]}]},
"hoops": {"mark": "P2-1", "radius": 1025.0, "levels": sorted(hoops), "a": 3220.0, "lap": 880.0, "hook": 200.0,
"join_deg": [90, 0]},
"ties": {"mark": "P3-1", "levels": sorted(ties), "straight": 2137.0, "leg": 200.0, "hook_tail": 150.0, "y_lift": 16.0},
"u_bars": {"mark": "P3-2", "count": 30, "z_bottom": zb, "half_bottom": 890.0, "half_top": 1025.0,
"leg_top_z": round(leg_top, 1), "top_z": round(leg_top + 200, 1), "bend_r": 135.0},
"plate": {"size": 1250.0, "thickness": 4.0, "z_top": zb - 8, "colour": [150, 150, 150]},
"design_lengths": {"P1-1": 8750.0, "P1-2": 3400.0, "P2-1": 3220.0 + 880.0 + 2 * 200.0,
"P3-1": 200.0 + 2137.0 + 165.0 + 150.0, "P3-2": 1780.0 + 2 * 581.0 + 2 * 200.0},
"src": {"main": "D4 단면 G-G·J-J 61개 1.475°부터 5.902°; r 999.5 (S2); P1-1 45.342~36.592, P1-2 36.592~33.192 (첫~끝 띠철근 단, 커플러 0, S6)",
"hoops": "구조물 치수표 H4 13@150, H3 34@300, 사슬 '110' (48단); r 1,025 (S1, 재료표 r 929는 오기); 겹이음 880 교번 (S5 [추론])",
"ties": "H2 6@300, H1 17@600, 사슬 '260' (24단 × X·Y); 재료표 a 200 b 2,137 c 165 d 150; Y +16, 교번 (S5 [추론])",
"u_bars": "재료표 r 135, a 1,780, b 581, c 200; 바닥 = 말뚝 바닥 +100; 중심 통과 30면, 주철근 0~29번 각도 (S3 [추론])",
"plate": "D4 PLATE 1,250×1,250×4 (T=4mm); U형 바닥 아래, 중심 (S4 [추론])",
"not_modelled": "SPACER 4mm 간격재 (공식 {H-2.500-(0.500x2)}x2개소만 기록, 사용자 결정)"},
}
X["checkers"]["G3"] = "g3_shaft.py"
X["negative"]["G3"] = [{"mark": "P1-1", "d": [5, 0, 0]}, {"mark": "P2-1", "d": [0, 0, 5]}]
X["G3"] = {"schedule_L": {"P1-1": 8750, "P1-2": 3400, "P2-1": 9000, "P3-1": 2652, "P3-2": 3342},
"schedule_n": {"P1-1": 61, "P1-2": 61, "P2-1": 48, "P3-1": 48, "P3-2": 30},
"dia": {"P1-1": 35, "P1-2": 22, "P2-1": 16, "P3-1": 16, "P3-2": 16}, "shaft_top": TOP,
"main": {"pieces": ["P1-2", "P1-1"], "radius": 999.5, "divisions": 61, "start_deg": 1.475},
"hoops": {"mark": "P2-1", "radius": 1025, "chain": {"first": 110, "groups": [[13, 150], [34, 300]]}},
"ties": {"mark": "P3-1", "chain": {"first": 260, "groups": [[6, 300], [17, 600]]}, "y_lift": 16},
"u_bars": {"mark": "P3-2", "bottom_above": 100},
"plate": {"size": 1250, "thickness": 4},
"src": "D4 재료표(기둥)·PLATE 재료표; 구조물 치수표 H 12,700, H1 17@600, H2 6@300, H3 34@300, H4 13@150, H5·H6 340, "
"사슬 '260'·'110'·'100'; 단면 원 r 1,025, DIVISION 61 (첫 1.475°); S1~S6 사용자 결정 2026-10-13"}
(D / "params.json").write_text(json.dumps(P, ensure_ascii=False, indent=1), encoding="utf-8")
(D / "expected.json").write_text(json.dumps(X, ensure_ascii=False, indent=1), encoding="utf-8")
print("G3 added; backup", bk)
+66
View File
@@ -0,0 +1,66 @@
"""Add the coping rebar (G4: C1~C16, S1~S4) to the app pier 'Ramp-A교P1' (generic builder rebar_polylines.py).
params = 판독/g4_bars.json (extract_g4.py: drawn values + decisions T1-T6, 2026-10-14)
expected = D3 schedule L / N, D2 / D3 drawn positions and section dimensions, typed in by hand.
Backs up params.json / expected.json to data/Ramp-A교P1/backup/2026-10-14_g4/ first. Refuses if G4 is in scope.
"""
import json
import shutil
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
D = HERE.parents[1] / "Code" / "교각모델링앱" / "data" / "Ramp-A교P1"
B = json.loads((HERE / "판독" / "g4_bars.json").read_text(encoding="utf-8"))
P = json.loads((D / "params.json").read_text(encoding="utf-8"))
X = json.loads((D / "expected.json").read_text(encoding="utf-8"))
if "G4" in P["scope"]:
sys.exit("G4 already in scope")
bk = D / "backup" / "2026-10-14_g4"
bk.mkdir(parents=True, exist_ok=True)
for f in ("params.json", "expected.json"):
shutil.copy2(D / f, bk / f)
SCHED = {"C1": (8617, 9, "H29"), "C2": (7177, 9, "H29"), "C4": (8617, 8, "H29"), "C5": (7177, 8, "H29"),
"C3": (7230, 9, "H25"), "C6": (9003, 9, "H25"), "S3": (1250, 44, "H25"), "S4": (1250, 44, "H25"),
"C7": (7480, 20, "H19"), "C8": (6071, 40, "H19"), "C9": (9251, 15, "H19"), "C10": (7426, 30, "H19"),
"C11": (8110, 20, "H19"), "C12": (5333, 40, "H19"), "C13": (11309, 18, "H19"), "C14": (18057, 12, "H19"),
"C15": (2700, 51, "H19"), "C16": (4000, 17, "H19"), "S1": (2700, 22, "H16"), "S2": (2600, 24, "H16")}
SHAPE = {"C1": "top-legs", "C2": "top-legs", "C3": "top-legs", "C4": "top-legs", "C5": "top-legs", "C6": "soffit-R",
"C7": "stirrup", "C8": "stirrup-chamfer", "C9": "stirrup", "C10": "stirrup-chamfer", "C11": "stirrup",
"C12": "stirrup-chamfer", "C13": "loop", "C14": "loop", "C15": "U", "C16": "U", "S1": "U", "S2": "U",
"S3": "I", "S4": "I"}
P["scope"] = ["G1", "G2", "G3", "G4", "G5"]
P["builders"]["G4"] = "rebar_polylines.py"
P["rebar_marks"].update({m: {"dia": d, "shape": SHAPE[m]} for m, (_, _, d) in SCHED.items()})
P["coping_rebar"] = {"form": "polylines", "host": "CP", "group": "코핑", "bars": B["bars"],
"src": "Code/RampA판독/extract_g4.py -> 판독/g4_bars.json; 판독서 v0.1.06 §13, 결정 T1~T6 (2026-10-14)"}
X["checkers"]["G4"] = "g4_ramp.py"
X["negative"]["G4"] = [{"mark": "C1", "d": [0, 5, 0]}, {"mark": "C7", "d": [0, 0, 5]}]
row = lambda s, p, n: {"start": s, "pitch": p, "n": n}
X["G4"] = {
"schedule": {m: [L, n] for m, (L, n, _) in SCHED.items()},
"dia": {m: int(d[1:]) for m, (_, _, d) in SCHED.items()},
"fixed_length": ["C1", "C2", "C3", "C4", "C5", "C6", "C15", "C16", "S1", "S2", "S3", "S4"],
"layers": {"C1": {"z": 59852, "y": row(-1000, 250, 9), "x_top": [-3425.6, 3492.1]},
"C4": {"z": 59852, "y": row(-875, 250, 8), "x_top": [-3425.6, 3492.1]},
"C2": {"z": 59752, "y": row(-1000, 250, 9), "x_top": [-3455.6, 3515.0]},
"C5": {"z": 59752, "y": row(-875, 250, 8), "x_top": [-3455.6, 3515.0]},
"C3": {"z": 59652, "y": row(-1000, 250, 9), "x_top": [-3485.6, 3537.8]},
"C16": {"z": 60167, "y": row(-1000, 125, 17), "x_top": [100, 3400]}},
"stirrups": {"x": row(-3375, 125, 55), "inner_dx": 20, "top_cover": 77.5, "bottom_cover": 77.5,
"outer_y": [-875, 750],
"zones": {"left": ["C7", "C8", -3375, -1000], "centre": ["C9", "C10", -875, 875],
"right": ["C11", "C12", 1000, 3375]}},
"loops": {"per_level": 3, "levels": {"C14": [59827, 59577, 59327, 59077],
"C13": [58827, 58577, 58327, 58077, 57827, 57577]}},
"c6": {"z_bottom": 57552, "y": [-906.5, -750, -500, -250, 0, 250, 500, 750, 906.5],
"y_note": "T1 @250 9본; 하면 직선부(EL 57.552)에서는 바깥 2본이 측면 테이퍼 피복 100.2로 ±906.5"},
"c15": {"a": 2000},
"seats": {"1": {"S1": 11, "S2": 12, "S3": 22, "S4": 22}, "2": {"S1": 11, "S2": 12, "S3": 22, "S4": 22}},
"src": "D3 재료표·철근 상세; D2 정면 작도(상면 3단 EL·x 끝, 스터럽 55곳, C13·C14 단, C16); 단면 A-A·B-B(행 @125/@250, "
"스터럽 y·피복 77.5); 교좌면 E-E·F-F; 결정 T1~T6 (2026-10-14)",
}
(D / "params.json").write_text(json.dumps(P, ensure_ascii=False, indent=1), encoding="utf-8")
(D / "expected.json").write_text(json.dumps(X, ensure_ascii=False, indent=1), encoding="utf-8")
print("G4 added:", len(B["bars"]), "bars; backup", bk)
+20
View File
@@ -0,0 +1,20 @@
"""Ramp-A P1: convert copies of RAG/Ramp-A교 P1 교각(1..4).dwg to DXF (Code/RampA판독/dxf) and index texts.
Uses the app reader (sources are copied before conversion and never touched). Labels D1..D4 by file order."""
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "Code" / "교각모델링앱"))
from engine import reader # noqa: E402
OUT = Path(__file__).resolve().parent / "dxf"
dwgs = sorted((ROOT / "RAG").glob("Ramp-A교 P1 교각(*).dwg"))
audit = reader.convert(dwgs, OUT)
labels = [f"D{i}" for i in range(1, len(audit) + 1)]
for a, lab in zip(audit, labels):
a["label"] = lab
(OUT / "audit.json").write_text(json.dumps(audit, ensure_ascii=False, indent=1), encoding="utf-8")
texts = reader.build_index([OUT / a["dxf"] for a in audit], labels, OUT / "texts.json")
print(json.dumps(audit, ensure_ascii=False, indent=1))
print("texts", len(texts))
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+34
View File
@@ -0,0 +1,34 @@
[
{
"source": "Ramp-A교 P1 교각(1).dwg",
"source_sha256": "8a7d7d1baac8a882130389776ff4d2c41c1c91306642be907a31bdb599adc479",
"dxf": "Ramp-A교 P1 교각(1).dxf",
"dxf_sha256": "b2bd3f6ad4a0d985472af97eaa1b842f2e8d4937cf7f20eeabe38c3761266bad",
"engine": "ODA File Converter 27.1.0",
"label": "D1"
},
{
"source": "Ramp-A교 P1 교각(2).dwg",
"source_sha256": "0549a0f9165f5105192e45a86bd8b4603e38f89e880a2f0e09d0aae2d651cabf",
"dxf": "Ramp-A교 P1 교각(2).dxf",
"dxf_sha256": "4bcd46ade02bcf46ba7f6358238a285e98f346bfb54fb530300b0b5b63db8b3d",
"engine": "ODA File Converter 27.1.0",
"label": "D2"
},
{
"source": "Ramp-A교 P1 교각(3).dwg",
"source_sha256": "7e28c5f9919ef054b6328aa8bfc4a12cba08550de4351606b614c3e7643d2b8a",
"dxf": "Ramp-A교 P1 교각(3).dxf",
"dxf_sha256": "e4e426156d031f4c88f96103daae422acaf5287a7473e53e3acfc66644d81a47",
"engine": "ODA File Converter 27.1.0",
"label": "D3"
},
{
"source": "Ramp-A교 P1 교각(4).dwg",
"source_sha256": "0919ec2f363c68495acd11f6c4aa2cda66dd0cce225df7df1115a5c58e57f78e",
"dxf": "Ramp-A교 P1 교각(4).dxf",
"dxf_sha256": "50d7b2c77fc64c7f77e6e6b7c4bc760385f86bab21019407defb32790f103c5d",
"engine": "ODA File Converter 27.1.0",
"label": "D4"
}
]
+1
View File
@@ -0,0 +1 @@
[{"dwg": "D1", "layer": "CS-DIMT", "x": 88916.7, "y": -471214.1, "h": 300.0, "text": "1,250", "handle": "BD"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 87166.7, "y": -471214.1, "h": 300.0, "text": "2,250", "handle": "CD"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 84916.7, "y": -471214.1, "h": 300.0, "text": "2,250", "handle": "D3"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 83166.7, "y": -471214.1, "h": 300.0, "text": "1,250", "handle": "D9"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 86041.7, "y": -470214.1, "h": 300.0, "text": "7,000", "handle": "E1"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 105655.6, "y": -471154.0, "h": 300.0, "text": "1,100", "handle": "E7"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 105105.6, "y": -470154.0, "h": 300.0, "text": "2,200", "handle": "EF"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 104555.6, "y": -471154.0, "h": 300.0, "text": "1,100", "handle": "F5"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 78714.0, "y": -474663.2, "h": 300.0, "text": "1,000", "handle": "FD"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 78714.0, "y": -475913.2, "h": 300.0, "text": "1,500", "handle": "105"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 92811.2, "y": -474663.2, "h": 300.0, "text": "1,000", "handle": "10B"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 92811.2, "y": -475913.2, "h": 300.0, "text": "1,500", "handle": "113"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 93340.5, "y": -474005.7, "h": 300.0, "text": "315", "handle": "119"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 77714.0, "y": -487763.2, "h": 300.0, "text": "27,200", "handle": "121"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 83641.7, "y": -477794.6, "h": 300.0, "text": "2,800", "handle": "127"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 86041.7, "y": -478794.6, "h": 300.0, "text": "7,600", "handle": "12D"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 81582.6, "y": -471243.0, "h": 300.0, "text": "300", "handle": "133"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 88441.7, "y": -477794.6, "h": 300.0, "text": "2,800", "handle": "13B"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 105105.6, "y": -462050.8, "h": 300.0, "text": "2,000", "handle": "141"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 78742.9, "y": -488413.2, "h": 300.0, "text": "500", "handle": "147"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 78714.0, "y": -482413.2, "h": 300.0, "text": "11,500", "handle": "14F"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 78714.0, "y": -497613.2, "h": 300.0, "text": "7,500", "handle": "155"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 78714.0, "y": -491263.2, "h": 300.0, "text": "5,200", "handle": "15B"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 101823.6, "y": -465444.9, "h": 300.0, "text": "2,000", "handle": "161"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 84829.0, "y": -503213.0, "h": 300.0, "text": "75", "handle": "167"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 86041.7, "y": -504184.1, "h": 300.0, "text": "2,500", "handle": "16F"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 86041.5, "y": -503184.1, "h": 300.0, "text": "2,350", "handle": "175"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 87254.0, "y": -503213.0, "h": 300.0, "text": "75", "handle": "17B"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 90500.8, "y": -471243.0, "h": 300.0, "text": "300", "handle": "183"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 86041.7, "y": -477794.6, "h": 300.0, "text": "2,000", "handle": "18B"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 86041.7, "y": -485966.1, "h": 300.0, "text": "2,000", "handle": "191"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 84916.7, "y": -485995.0, "h": 300.0, "text": "250", "handle": "197"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 87166.7, "y": -485995.0, "h": 300.0, "text": "250", "handle": "19F"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 86041.7, "y": -484966.1, "h": 300.0, "text": "2,500", "handle": "1A7"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 86041.7, "y": -490363.6, "h": 300.0, "text": "2,500", "handle": "1AD"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 97777.8, "y": -474663.2, "h": 300.0, "text": "1,000", "handle": "1B3"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 97777.8, "y": -475913.2, "h": 300.0, "text": "1,500", "handle": "1BB"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 96777.8, "y": -487763.2, "h": 300.0, "text": "27,200", "handle": "1C1"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 97806.7, "y": -488413.2, "h": 300.0, "text": "500", "handle": "1C7"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 97777.8, "y": -482413.2, "h": 300.0, "text": "11,500", "handle": "1CF"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 97777.8, "y": -497613.2, "h": 300.0, "text": "7,500", "handle": "1D5"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 97777.8, "y": -491263.2, "h": 300.0, "text": "5,200", "handle": "1DB"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 103892.9, "y": -503213.0, "h": 300.0, "text": "75", "handle": "1E1"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 105105.6, "y": -504184.1, "h": 300.0, "text": "2,500", "handle": "1E9"}, {"dwg": "D1", "layer": "CS-DIMT", "x": 105105.4, "y": -503184.1, "h": 300.0, "text": "2,350", "handle": "1EF"}, {"dwg": "D1", "layer": Line truncated
+100
View File
@@ -0,0 +1,100 @@
"""Reading aids for the Ramp-A P1 drawings (read only): render a region, dump entities of a region.
usage:
python dxftools.py render <D1..D4> x0 y0 x1 y1 <name> [px] -> 판독/<name>.png
python dxftools.py dump <D1..D4> x0 y0 x1 y1 [layer-regex] -> prints LINE/LWPOLYLINE/ARC/CIRCLE/TEXT
DXF source: Code/RampA판독/dxf (converted from copies of RAG/Ramp-A교 P1 교각(1..4).dwg).
"""
import re
import sys
from pathlib import Path
import ezdxf
from ezdxf import bbox
HERE = Path(__file__).resolve().parent
DXF = HERE / "dxf"
_CACHE = {}
def doc(label):
if label not in _CACHE:
path = sorted(DXF.glob("*.dxf"))[int(label[1]) - 1]
_CACHE[label] = ezdxf.readfile(path)
return _CACHE[label]
def in_region(e, x0, y0, x1, y1):
try:
b = bbox.extents([e], fast=True)
except Exception:
return False
return b.has_data and b.extmax.x >= x0 and b.extmin.x <= x1 and b.extmax.y >= y0 and b.extmin.y <= y1
def entities(label, x0, y0, x1, y1, layer=None):
rx = re.compile(layer) if layer else None
for e in doc(label).modelspace():
if rx and not rx.search(e.dxf.layer):
continue
if in_region(e, x0, y0, x1, y1):
yield e
def describe(e):
t, L, h = e.dxftype(), e.dxf.layer, e.dxf.handle
r = lambda v: round(v, 1)
if t == "LINE":
s, f = e.dxf.start, e.dxf.end
return f"{h} {L} LINE ({r(s.x)},{r(s.y)})-({r(f.x)},{r(f.y)}) len {r(s.distance(f))}"
if t == "LWPOLYLINE":
pts = [(r(p[0]), r(p[1])) for p in e.get_points()]
return f"{h} {L} PLINE closed={e.closed} {pts}"
if t == "ARC":
c = e.dxf.center
return f"{h} {L} ARC c({r(c.x)},{r(c.y)}) r {r(e.dxf.radius)} {r(e.dxf.start_angle)}..{r(e.dxf.end_angle)}"
if t == "CIRCLE":
c = e.dxf.center
return f"{h} {L} CIRCLE c({r(c.x)},{r(c.y)}) r {r(e.dxf.radius)}"
if t in ("TEXT", "MTEXT"):
p = e.dxf.insert
s = e.plain_text() if t == "MTEXT" else e.dxf.text
return f"{h} {L} {t} ({r(p.x)},{r(p.y)}) '{s.strip()}'"
if t == "INSERT":
p = e.dxf.insert
return f"{h} {L} INSERT {e.dxf.name} ({r(p.x)},{r(p.y)})"
return f"{h} {L} {t}"
def render(label, x0, y0, x1, y1, name, px=2400):
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from ezdxf.addons.drawing import Frontend, RenderContext
from ezdxf.addons.drawing.config import BackgroundPolicy, ColorPolicy, Configuration
from ezdxf.addons.drawing.matplotlib import MatplotlibBackend
d = doc(label)
keep = list(entities(label, x0, y0, x1, y1))
fig = plt.figure(figsize=(px / 100, px * (y1 - y0) / (x1 - x0) / 100), dpi=100)
ax = fig.add_axes([0, 0, 1, 1])
cfg = Configuration(background_policy=BackgroundPolicy.WHITE, color_policy=ColorPolicy.BLACK)
Frontend(RenderContext(d), MatplotlibBackend(ax), config=cfg).draw_entities(keep)
ax.set_xlim(x0, x1)
ax.set_ylim(y0, y1)
ax.set_aspect("equal")
ax.axis("off")
out = HERE / "판독" / f"{name}.png"
out.parent.mkdir(exist_ok=True)
fig.savefig(out, facecolor="white")
plt.close(fig)
return out
if __name__ == "__main__":
cmd, label = sys.argv[1], sys.argv[2]
x0, y0, x1, y1 = map(float, sys.argv[3:7])
if cmd == "render":
print(render(label, x0, y0, x1, y1, sys.argv[7], int(sys.argv[8]) if len(sys.argv) > 8 else 2400))
else:
for e in entities(label, x0, y0, x1, y1, sys.argv[7] if len(sys.argv) > 7 else None):
print(describe(e))
+94
View File
@@ -0,0 +1,94 @@
"""Ramp-A P1 shape reading (G1 + G5 positions) from D1 by handle -> 판독/g1_values.json.
Datums (D1 front view): pier CL x = 86041.7, z(mm) = drawing y + 534115.2 (EL 32.752 at pile bottom y -501363.2,
EL 59.952 / 60.267 at coping top y -474163.2 / -473848.2). Plan view: CL x = 86041.7, coping CL y = -465444.9.
Model axes: X along the coping (교축직각), Y across (교축), origin = pier centre, Z = EL x 1000.
"""
import json
import math
from pathlib import Path
from dxftools import doc
D = doc("D1")
X0, ZOFF = 86041.7, 534115.2
PX0, PY0 = 86041.7, -465444.9
def ent(h):
return D.entitydb[h]
def z(y):
return round(y + ZOFF, 1)
def line(h):
e = ent(h)
return (e.dxf.start.x, e.dxf.start.y), (e.dxf.end.x, e.dxf.end.y)
out = {}
# front view
(a, b) = line("857"); (c, d) = line("7D1")
out["coping"] = {
"top_left": {"x0": round(line("856")[0][0] - X0, 1), "x1": 0.0, "z": z(line("856")[0][1]), "src": "D1 856"},
"top_right": {"x0": 0.0, "x1": round(line("854")[1][0] - X0, 1), "z": z(line("854")[0][1]), "src": "D1 854"},
"end_left_front": {"bottom": [round(a[0] - X0, 1), z(a[1])], "top": [round(b[0] - X0, 1), z(b[1])], "src": "D1 857"},
"end_right_front": {"bottom": [round(c[0] - X0, 1), z(c[1])], "top": [round(d[0] - X0, 1), z(d[1])], "src": "D1 7D1"},
}
for h, k in (("501", "soffit_left"), ("4FF", "soffit_right")):
e = ent(h)
cx, cz, r = e.dxf.center.x - X0, z(e.dxf.center.y), e.dxf.radius
pts = [(round(cx + r * math.cos(math.radians(t)), 1), round(cz + r * math.sin(math.radians(t)), 1))
for t in (e.dxf.start_angle, e.dxf.end_angle)]
out["coping"][k] = {"centre": [round(cx, 1), cz], "radius": r, "ends": pts, "src": f"D1 ARC {h}"}
(p, q) = line("55A")
out["coping"]["column_top_z"] = z(p[1])
# side view (CL x 105105.6)
SX0 = 105105.6
(s0, s1) = line("504")
out["coping"]["side"] = {"half_width_top": round(SX0 - ent("520").dxf.start.x, 1),
"taper": {"from": [round(s1[0] - SX0, 1), z(s1[1])], "to": [round(s0[0] - SX0, 1), z(s0[1])]},
"src": "D1 측면도 520/521 (2,200), 503/504 (2,200 -> 2,000)"}
# plan view ends: polyline 51A / 519 with bulge
for h, k in (("51A", "plan_end_left"), ("519", "plan_end_right")):
pts = ent(h).get_points("xyb")
x_top, x_bot, bulge = pts[0][0] - PX0, pts[1][0] - PX0, pts[1][2]
chord = abs(pts[2][1] - pts[1][1])
sag = abs(bulge) * chord / 2
out["coping"][k] = {"top_edge_x": round(x_top, 1), "corner_bottom_x": round(x_bot, 1), "half_width": round(chord / 2, 1),
"sagitta": round(sag, 1), "radius": round((chord ** 2 / 4 + sag ** 2) / (2 * sag), 1),
"src": f"D1 평면 {h} bulge {bulge:.3f}"}
# column / pile (front)
out["column"] = {"d": round(line("610")[0][0] - line("60F")[0][0], 1), "z0": z(line("60F")[0][1]),
"z1": z(line("60F")[1][1]), "src": "D1 60F/610"}
out["pile_upper"] = {"d": round(line("87F")[0][0] - line("881")[0][0], 1), "z0": z(line("880")[0][1]),
"z1": z(line("881")[1][1]), "src": "D1 880-87F / 882-881, 89F"}
out["pile_lower"] = {"d": round(line("875")[0][0] - line("873")[0][0], 1), "z0": z(line("873")[1][1]),
"z1": z(line("873")[0][1]), "src": "D1 873/875, 87E"}
out["ground_z"] = z(line("89E")[0][1])
# plan: pedestals and pads (corner polylines / lines), rotation from the long (along Y) edge
def rect(pts, src):
cx = sum(p[0] for p in pts) / 4 - PX0
cy = sum(p[1] for p in pts) / 4 - PY0
e = [(pts[(i + 1) % 4][0] - pts[i][0], pts[(i + 1) % 4][1] - pts[i][1]) for i in range(4)]
L = [math.hypot(*v) for v in e]
along = max(range(4), key=lambda i: abs(e[i][1])) # edge most parallel to plan Y
vx, vy = e[along] if e[along][1] > 0 else (-e[along][0], -e[along][1])
rot = math.degrees(math.atan2(vx, vy)) # + = clockwise from +Y (bearings.py convention)
return {"centre": [round(cx, 1), round(cy, 1)], "along": round(L[along], 1), "across": round(L[(along + 1) % 4], 1),
"rotation_deg": round(rot, 3), "src": src}
out["plan"] = {}
for no, ped, pads in ((1, ("49D", "49E", "49F", "4A0"), ("486", "487", "488")),
(2, ("4A5", "4A6", "4A7", "4A8"), ("489", "48A", "48B"))):
pts = [line(h)[0] for h in ped]
out["plan"][no] = {"pedestal": rect(pts, "D1 평면 " + "/".join(ped)),
"pads": [rect([p[:2] for p in ent(h).get_points("xy")], f"D1 평면 {h}") for h in pads]}
p = Path(__file__).resolve().parent / "판독" / "g1_values.json"
p.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
print(json.dumps(out, ensure_ascii=False, indent=1))
+45
View File
@@ -0,0 +1,45 @@
"""Ramp-A P1 column rebar reading (G2: P1 main, P2 hoops, P3 ties) from D2 -> 판독/g2_values.json.
D2 front view: column CL x 12741.4; shaft top (EL 45.452) at y -700829.1, coping bottom (EL 57.452) at y -692829.1;
the view is broken between y -696879.1 and -696779.1 (4,000 hidden): EL = 45452 + (y + 700829.1) [+ 4000 above].
Left dimension chains (ticks at x 8216 '띠철근외측' and x 7716 '크로스타이') give the hoop / tie zones.
Section C-C (centre 1045.1, -698159.3): main bars = INSERT symbols on r 875.
"""
import json
import math
from collections import defaultdict
from pathlib import Path
from dxftools import entities
el = lambda y: round(45452 + (y + 700829.1) + (4000 if y > -696829 else 0), 1)
out = {}
ticks = defaultdict(set)
for e in entities("D2", 6800, -706000, 9200, -688500):
if e.dxftype() == "INSERT":
ticks[round(e.dxf.insert.x)].add(round(e.dxf.insert.y, 1))
out["chain_hoop_EL"] = [el(y) for y in sorted(ticks[8216])]
out["chain_tie_EL"] = [el(y) for y in sorted(ticks[7716])]
# main bars in the front view (verticals of the column group)
v = [e for e in entities("D2", 11800, -703000, 13700, -690500, "CS-RBAR-MAJR") if e.dxftype() == "LINE"
and abs(e.dxf.start.x - e.dxf.end.x) < 0.5 and abs(e.dxf.start.y - e.dxf.end.y) > 3000]
out["main_front"] = {"x_min_rel": round(min(e.dxf.start.x for e in v) - 12741.4, 1),
"x_max_rel": round(max(e.dxf.start.x for e in v) - 12741.4, 1),
"bottom_EL": el(min(min(e.dxf.start.y, e.dxf.end.y) for e in v)),
"top_EL": el(max(max(e.dxf.start.y, e.dxf.end.y) for e in v))}
# hoops drawn (front, EREC) and side view hoops / ties
hy = sorted({round(e.dxf.start.y, 1) for e in entities("D2", 11000, -703000, 14500, -690900, "CS-RBAR-EREC")
if e.dxftype() == "LINE" and abs(e.dxf.start.y - e.dxf.end.y) < 0.5 and abs(e.dxf.start.x - e.dxf.end.x) > 1500})
out["hoops_front_drawn"] = {"count": len(hy), "first_EL": el(hy[0]), "last_EL": el(hy[-1])}
# section C-C main bars
cx, cy = 1045.1, -698159.3
angs = sorted(round(math.degrees(math.atan2(e.dxf.insert.y - cy, e.dxf.insert.x - cx)) % 360, 3)
for e in entities("D2", -500, -700000, 2600, -696300) if e.dxftype() == "INSERT"
and abs(math.hypot(e.dxf.insert.x - cx, e.dxf.insert.y - cy) - 875) < 3)
out["main_section"] = {"count": len(angs), "radius": 875.0, "first_deg": angs[0], "step_deg": round(angs[1] - angs[0], 3)}
ties = [e for e in entities("D2", -1500, -698500, 4500, -692000, "CS-RBAR") if e.dxftype() == "LINE"]
out["ties_section"] = [{"handle": e.dxf.handle, "len": round(e.dxf.start.distance(e.dxf.end), 1),
"dir": "Y" if abs(e.dxf.start.x - e.dxf.end.x) < 0.5 else "X"} for e in ties]
p = Path(__file__).resolve().parent / "판독" / "g2_values.json"
p.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
print(json.dumps(out, ensure_ascii=False))
+41
View File
@@ -0,0 +1,41 @@
"""Ramp-A P1 shaft cage reading (G3: P1-1, P1-2, P2-1, P3-1, P3-2, PLATE) from D4 -> 판독/g3_values.json.
D4 elevation is schematic (upper D2,500 part drawn 12,700 long, rock line 3,900 above the bottom vs D1 7,500), so
levels come from the 구조물 치수표 (H 12,700; H1 17@600, H2 6@300, H3 34@300, H4 13@150, H5 340, H6 340) and the
chain texts ('260' / '110' at the top, '100' at the bottom). Sections G-G / J-J give radii and bar angles.
"""
import json
import math
from collections import Counter
from pathlib import Path
from dxftools import entities
out = {}
for name, (x0, y0, x1, y1) in {"G-G": (90000, -697000, 96000, -688000), "J-J": (98000, -706000, 104500, -697000)}.items():
big = [e for e in entities("D4", x0, y0, x1, y1) if e.dxftype() == "CIRCLE" and e.dxf.radius > 1000]
c = big[0].dxf.center
radii = sorted({round(e.dxf.radius, 1) for e in big if e.dxf.center.distance(c) < 1})
ins = [e for e in entities("D4", x0, y0, x1, y1) if e.dxftype() == "INSERT"]
rr = Counter(round(math.hypot(e.dxf.insert.x - c.x, e.dxf.insert.y - c.y), 1) for e in ins)
r0 = rr.most_common(1)[0][0]
a = sorted(round(math.degrees(math.atan2(e.dxf.insert.y - c.y, e.dxf.insert.x - c.x)) % 360, 3)
for e in ins if abs(math.hypot(e.dxf.insert.x - c.x, e.dxf.insert.y - c.y) - r0) < 1)
lines = [round(e.dxf.start.distance(e.dxf.end), 1) for e in entities("D4", x0, y0, x1, y1, "CS-RBAR")
if e.dxftype() == "LINE" and e.dxf.start.distance(e.dxf.end) > 1500]
out[name] = {"concrete_and_hoop_radii": radii, "bar_symbol_radius": r0, "bar_count": len(a), "first_deg": a[0],
"step_deg": round((a[-1] - a[0]) / (len(a) - 1), 4) if name == "J-J" else None, "tie_lines": lines}
H0 = 45452.0 # shaft top (D1)
hoops = [H0 - 110 - 150 * k for k in range(14)]
hoops += [hoops[-1] - 300 * k for k in range(1, 35)]
ties = [H0 - 260 - 300 * k for k in range(7)]
ties += [ties[-1] - 600 * k for k in range(1, 18)]
out["hoop_levels_from_table"] = {"count": len(hoops), "top": hoops[0], "bottom": hoops[-1],
"rule": "shaft top -110, 13@150, 34@300 (+H6 340 +100 = shaft bottom)"}
out["tie_levels_from_table"] = {"count": len(ties), "top": ties[0], "bottom": ties[-1],
"rule": "shaft top -260, 6@300, 17@600 (+H5 340 +100)"}
out["chain_check"] = {"hoops": 110 + 1950 + 10200 + 340 + 100, "ties": 260 + 1800 + 10200 + 340 + 100, "H": 12700}
out["main_span"] = {"top": hoops[0], "bottom": hoops[-1], "length": hoops[0] - hoops[-1], "P1-1 + P1-2": 8750 + 3400}
p = Path(__file__).resolve().parent / "판독" / "g3_values.json"
p.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
print(json.dumps(out, ensure_ascii=False))
+198
View File
@@ -0,0 +1,198 @@
"""Ramp-A P1 coping rebar (G4: C1~C16, S1~S4) -> centreline polylines for the generic builder rebar_polylines.py.
Inputs: drawn values read from D2 / D3 (판독서 v0.1.06 §13, values typed below with their source) and the user
decisions T1-T6 (2026-10-14). Output: 판독/g4_bars.json {"bars": [...], "summary": {...}} used by add_g4.py.
Model axes: X along the coping, Y across, Z = EL x 1000; pier centre at the origin.
"""
import json
import math
from pathlib import Path
HERE = Path(__file__).resolve().parent
APP = HERE.parents[1] / "Code" / "교각모델링앱"
P = json.loads((APP / "data" / "Ramp-A교P1" / "params.json").read_text(encoding="utf-8"))
CP = P["coping"]
TL, TR = CP["z_top_left"], CP["z_top_right"] # 59952 / 60267
ZB, ZT = CP["z_bottom"], CP["z_tip"] # 57452 / 58952
SCX, SCZ = abs(CP["soffit"]["centre_x"]), CP["soffit"]["centre_z"]
bars = []
def rb(dia): # min bend radius of the centreline (as the other builders)
return ((2 * dia if dia <= 16 else 3.5 * dia) + dia) / 2
def add(mark, occ, pts, dia, L, src, r=None):
bars.append({"mark": mark, "occ": occ, "pts": [[round(v, 2) for v in p] for p in pts],
"r": rb(dia) if r is None else r, "design_length": round(L, 1), "src": src})
def top(x):
return TL if x <= 0 else TR
def arc_z(x, R):
"""z of the soffit-parallel arc of radius R (centre (+-SCX, SCZ)) at x; flat zone at the column handled by caller."""
d = SCX - abs(x)
return SCZ + math.sqrt(R * R - d * d)
def face_y(z):
"""Half width of the coping side at z (2,200 above the tip level, taper to 2,000 at the column top)."""
return 1100.0 if z >= ZT else 1000.0 + 100.0 * (z - ZB) / (ZT - ZB)
# ---- top bars C1/C4 (1단), C2/C5 (2단), C3 (3단): D2 정면 작도 end points, 단면 A-A/B-B rows
ROW17 = [-1000 + 125 * k for k in range(17)]
ROW9 = [-1000 + 250 * k for k in range(9)]
TOPS = { # mark: (z, [left leg end], x_left, x_right, [right leg end], L, dia)
1: (59852.0, (-3672.0, 59030.4), -3425.6, 3492.1, (3679.3, 59031.7), 8617, 29),
2: (59752.0, (-3485.6, 59652.0), -3455.6, 3515.0, (3537.8, 59652.0), 7177, 29),
3: (59652.0, (-3515.6, 59552.0), -3485.6, 3537.8, (3560.6, 59552.0), 7230, 25),
}
for layer, (z, le, xl, xr, re_, L, dia) in TOPS.items():
ys = ROW9 if layer == 3 else ROW17
for k, y in enumerate(ys):
mark = {1: ("C1", "C4"), 2: ("C2", "C5"), 3: ("C3", "C3")}[layer][k % 2]
add(mark, f"y{y:+.0f}", [(le[0], y, le[1]), (xl, y, z), (xr, y, z), (re_[0], y, re_[1])], dia, L,
f"D2 정면 {layer}단 작도선, 단면 A-A/B-B {'@250' if layer == 3 else '@125 교번'}")
# ---- C6 soffit bars: leg 400 along the end face, R 5,100 arc, bottom 1,914 at EL 57.552 (D2 ARC 478/479); T1: 9 @250;
# the bars near the faces keep inside the side taper (y <= face - 100.2) [T1 추론]
for k in range(9):
y0 = -1000 + 250 * k
pts = [(-3557.1, y0, 59413.5)]
a0, a1 = math.radians(79.1), math.radians(43.8)
for i in range(21):
t = a0 + (a1 - a0) * i / 20
pts.append((-SCX + 5100 * math.cos(t), y0, SCZ + 5100 * math.sin(t)))
for i in range(21):
t = math.radians(180 - 43.8) + (math.radians(180 - 79.1) - math.radians(180 - 43.8)) * i / 20
pts.append((SCX + 5100 * math.cos(t), y0, SCZ + 5100 * math.sin(t)))
pts.append((3590.3, y0, 59421.7))
pts = [(x, math.copysign(min(abs(y), face_y(z) - 100.2), y) if y else 0.0, z) for x, y, z in pts]
add("C6", f"y{y0:+.0f}", pts, 25, 9003, "D2 정면 ARC 478/479 R5,100, 하면 1,914 EL 57.552; @250 9본 (T1 [추론])", r=0)
# ---- stirrups at x = -3375 ... 3375 @125 (D2 정면 55 작도선): outer C7 / C9 / C11, inner pair C8 / C10 / C12
XS = [-3375 + 125 * i for i in range(55)]
def bottom(x):
"""Stirrup bottom (centreline): 77.5 above the soffit, i.e. on R 5,077.5, flat 57,529.5 over the column."""
return ZB + 77.5 if abs(x) < 1000 else arc_z(x, 5077.5)
Z_C1 = TL - 996.7 # inner outer-leg vertical bottom (단면 A-A/B-B)
for x in XS:
zt, zb = top(x) - 77.5, bottom(x)
b = zt - zb
outer, inner = ("C7", "C8") if x < -875 else (("C9", "C10") if x <= 875 else ("C11", "C12"))
add(outer, f"x{x:+.0f}", [(x, -62.5, zt), (x, 750, zt), (x, 750, zb), (x, -875, zb), (x, -875, zt), (x, -62.5, zt)],
19, 2 * 1673 + 2 * b + 2 * 450, f"D2 정면 x {x}; 단면 B-B y -875~750; b {b:.1f}")
yb = face_y(zb) - 100.2
for s in (1, -1):
xi = x + 20 # beside the outer stirrup (T6 [추론])
p = [(xi, s * 687.5, zt), (xi, s * 1000, zt), (xi, s * 1000, Z_C1), (xi, s * yb, zb), (xi, s * 375, zb),
(xi, s * 375, zt), (xi, s * 687.5, zt)]
c2 = math.hypot(1000 - yb, Z_C1 - zb)
add(inner, f"x{x:+.0f}{'R' if s > 0 else 'L'}", p, 19, 673 + b + (yb - 375) + c2 + (zt - Z_C1) + 2 * 450,
f"단면 A-A/B-B 안쪽 모따기 스터럽, x {x}+20 [T6 추론]; c1 {zt - Z_C1:.0f} (T4)")
# ---- side loops C14 (4 levels) / C13 (6 levels): D2 정면 수평선 ends; 3 concentric loops per level (T2)
LEVELS = [ # z, x_left, x_right (D2 정면)
(59827.0, -3433.1, 3497.9), (59577.0, -3508.1, 3554.9), (59327.0, -3583.1, 3611.9), (59077.0, -3658.1, 3669.0),
(58827.0, -2927.0, 2927.1), (58577.0, -2343.2, 2343.2), (58327.0, -1903.0, 1903.1), (58077.0, -1544.6, 1544.7),
(57827.0, -1241.9, 1242.0), (57577.0, -981.2, 981.3)]
for i, (z, xl, xr) in enumerate(LEVELS):
mark = "C14" if i < 4 else "C13"
yo = min(977.5, face_y(z) - 122.7) # outer dots follow the taper (단면 B-B -968.9 ...)
for k, (yn, yp) in enumerate(((-yo, yo), (-852.5, 727.5), (-397.5, 397.5))):
a, b_ = xl + 25 * k, xr - 25 * k
p = [(0, yp, z), (b_, yp, z), (b_, yn, z), (a, yn, z), (a, yp, z), (0, yp, z)]
L = 2 * (b_ - a + 48) + 2 * (yp - yn) + 2 * 350 + (290 if mark == "C14" else 0)
add(mark, f"z{z:.0f}-{k}", p, 19, L, f"D2 정면 수평선 EL {z / 1000:.3f}; 동심 고리 {k} (T2 [추론])")
# ---- C16: raised right top, x 100~3,400 at EL 60.167, legs 350, y @125 (17)
for y in ROW17:
add("C16", f"y{y:+.0f}", [(100, y, 60167 - 350), (100, y, 60167), (3400, y, 60167), (3400, y, 60167 - 350)], 19,
4000, "D2 정면 C16 EL 60.167, x 100~3,400, 다리 350")
# ---- C15: transverse U on top, midway between stirrups, |x| <= 3,187.5 except 62.5 (T3 [추론]) -> 51
for x in [-3312.5 + 125 * i for i in range(54)]:
if abs(x) > 3187.5 or x == 62.5:
continue
z = top(x) - 77.5 - 19
add("C15", f"x{x:+.1f}", [(x, -1000, z - 350), (x, -1000, z), (x, 1000, z), (x, 1000, z - 350)], 19, 2700,
"상면 횡방향 U형, 스터럽 사이 중간 위치 (T3 [추론])")
# ---- seat bars per bearing (T5): local axes u (along, rotated), v (across); pedestal top = seat_z + h1
for bng in P["bearing_seats"]["bearings"]:
no, (cx, cy), rot = bng["no"], bng["centre"], math.radians(bng["rotation_deg"])
u = (math.sin(rot), math.cos(rot))
v = (math.cos(rot), -math.sin(rot))
zp = bng["seat_z"] + bng["h1"]
g = lambda su, sv, z: (cx + su * u[0] + sv * v[0], cy + su * u[1] + sv * v[1], z)
# S1: straight a 1,300 along u, legs 700, 11 positions along v (50|100|8@125|100|50 = 1,300)
for j, sv in enumerate([-600] + [-500 + 125 * k for k in range(9)] + [600]):
z = zp - 50
add("S1", f"B{no}-{j:02d}", [g(-650, sv, z - 700), g(-650, sv, z), g(650, sv, z), g(650, sv, z - 700)], 16, 2700,
f"교좌면 보강 E-E, 받침 {no} [T5 추론]")
# S2: straight a 1,200 along v, legs 700, 12 positions along u (9@125 + 2 edge [추론])
for j, su in enumerate([-625] + [-562.5 + 125 * k for k in range(10)] + [625]):
z = zp - 50 - 16
add("S2", f"B{no}-{j:02d}", [g(su, -600, z - 700), g(su, -600, z), g(su, 600, z), g(su, 600, z - 700)], 16, 2600,
f"교좌면 보강 E-E, 받침 {no} [T5 추론]")
# S3 (along u) / S4 (along v): 2 layers x 11, 10@125 = 1,250, straight 1,250, 345 / 445 below the pedestal top
for layer, dz in enumerate((345, 445)):
for j, s_ in enumerate([-625 + 125 * k for k in range(11)]):
add("S3", f"B{no}-{layer}{j:02d}", [g(-625, s_, zp - dz), g(625, s_, zp - dz)], 25, 1250,
f"교좌면 보강 F-F, 받침 {no}, {layer + 1}층 [T5 추론]", r=0)
add("S4", f"B{no}-{layer}{j:02d}", [g(s_, -625, zp - dz - 25), g(s_, 625, zp - dz - 25)], 25, 1250,
f"교좌면 보강 F-F, 받침 {no}, {layer + 1}층 [T5 추론]", r=0)
# ---- plan-corner clipping [추론, 울산 R500 모서리 단축과 같은 방식]: the end face is convex in plan (corner 3,650 at the
# tip level vs 3,800 at y = 0), so points nearer than cover 40 (+ bar radius) to it are pulled inwards in x
PE = CP["plan_end"]
RP = (PE["half_width"] ** 2 + (PE["tip_x"] - PE["corner_x"]) ** 2) / (2 * (PE["tip_x"] - PE["corner_x"]))
def face_x(y, z, sx):
xa = PE["tip_x"] - (RP - math.sqrt(RP * RP - min(abs(y), PE["half_width"]) ** 2))
zt = TL if sx < 0 else TR
return xa + (z - ZT) / (zt - ZT) * (CP["top_half"] - xa)
DIA = {"C1": 29, "C2": 29, "C3": 25, "C4": 29, "C5": 29, "C6": 25, "C13": 19, "C14": 19}
clipped = {}
for b in bars:
m = b["mark"]
if m not in DIA:
continue
r = DIA[m] / 2
pts = b["pts"]
if m in ("C13", "C14"): # keep the loop rectangular: one limit per short side
ymax = max(abs(p[1]) for p in pts)
for sx in (-1, 1):
lim = min(face_x(ymax + r, p[2], sx) for p in pts) - (r + 40)
if any(p[0] * sx > lim for p in pts):
clipped[m] = clipped.get(m, 0) + 1
for p in pts:
if p[0] * sx > 0 and abs(p[0]) > 1:
p[0] = round(sx * min(abs(p[0]), lim), 2)
continue
hit = False
for p in pts:
sx = 1 if p[0] > 0 else -1
lim = face_x(abs(p[1]) + r, p[2], sx) - (r + 40)
if abs(p[0]) > lim:
p[0] = round(sx * lim, 2)
hit = True
clipped[m] = clipped.get(m, 0) + hit
if hit:
b["src"] += "; 평면 모서리 단축 [추론]"
count = {}
for b in bars:
count[b["mark"]] = count.get(b["mark"], 0) + 1
out = {"bars": bars, "summary": {"bars": len(bars), "per_mark": count, "corner_clipped": clipped}}
(HERE / "판독" / "g4_bars.json").write_text(json.dumps(out, ensure_ascii=False), encoding="utf-8")
print(json.dumps(out["summary"], ensure_ascii=False))
+136
View File
@@ -0,0 +1,136 @@
"""Create the app pier 'Ramp-A교P1' (form "single shaft", scope G1 + G5) through the web client:
new pier (Ulsan template as a starting copy), upload copies of the four RAG DWGs, then write params / expected
from the reading (판독/g1_values.json, D1 tables; DOC/02_분석/분석_Ramp-A교P1교각도면판독_2026-10-13_v0.1.00.md).
params = drawn values (handles), user decisions R1-R4 (2026-10-13).
expected = dimension / EL texts and the D1 tables, typed in by hand; two drawn-only inputs marked [작도].
usage: python make_pier.py (refuses if the pier already exists)
"""
import json
import math
import sys
from io import BytesIO
from pathlib import Path
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
APP = ROOT / "Code" / "교각모델링앱"
sys.path.insert(0, str(APP / "web"))
sys.path.insert(0, str(APP))
import app as webapp # noqa: E402
import store # noqa: E402
NAME = "Ramp-A교P1"
V = json.loads((HERE / "판독" / "g1_values.json").read_text(encoding="utf-8"))
cp = V["coping"]
def circle_centre(a, b, r):
"""Centre (lower side) of the radius-r circle through a and b."""
(ax, az), (bx, bz) = a, b
mx, mz = (ax + bx) / 2, (az + bz) / 2
d = math.hypot(bx - ax, bz - az)
h = math.sqrt(r * r - d * d / 4)
ux, uz = (bz - az) / d, -(bx - ax) / d
return min(((mx + h * ux, mz + h * uz), (mx - h * ux, mz - h * uz)), key=lambda c: c[1])
ZB, ZT = V["column"]["z1"], cp["end_left_front"]["bottom"][1]
cx, cz = circle_centre((-1000.0, ZB), (-3800.0, ZT), 5000.0)
bear = {1: dict(el_x=60112, seat_z=cp["top_left"]["z"]), 2: dict(el_x=60427, seat_z=cp["top_right"]["z"])}
params = {
"_schema": "pier-T/1",
"pier_id": "P1",
"_meta": {"structure": "Ramp-A교 P1 교각 (T형 계열: 단주 + Y형 코핑 + 단일 현장타설말뚝)",
"status": "형상(G1)·받침(G5) 판독 2026-10-13, 결정 R1~R4 사용자 확정. 철근 G2~G4 미판독(범위 밖)",
"units": "mm; Z = EL x 1000",
"frame": "X 교축직각(정면도 좌→우), Y 교축(평면도 아래→위), 원점 = 교각 중심",
"sources": {"D1": "RAG/Ramp-A교 P1 교각(1).dwg 일반도", "D2": "(2) 코핑·기둥 배근(1)",
"D3": "(3) 주철근 조립도·재료표", "D4": "(4) 기둥 재료표·떠오름 방지 PLATE"},
"reading": "DOC/02_분석/분석_Ramp-A교P1교각도면판독_2026-10-13_v0.1.00.md, Code/RampA판독/판독/g1_values.json"},
"scope": ["G1", "G5"],
"builders": {"G1": "concrete_shaft.py"},
"pile": {"segments": [{"d": V["pile_lower"]["d"], "z0": V["pile_lower"]["z0"], "z1": V["pile_lower"]["z1"]},
{"d": V["pile_upper"]["d"], "z0": V["pile_upper"]["z0"], "z1": V["pile_upper"]["z1"]}],
"kind": "현장타설말뚝 (암반 근입 Ø2,350 + 상부 Ø2,500)",
"src": f"D1 정면 {V['pile_lower']['src']}; {V['pile_upper']['src']}; 상부 Ø2,500은 '75, 2,350, 75' 표기(케이싱 포함 가능) — 작도선대로 콘크리트"},
"column": {"shape": "circular", "diameter": V["column"]["d"], "z0": V["column"]["z0"], "z1": V["column"]["z1"],
"src": f"D1 정면 {V['column']['src']}"},
"coping": {"form": "y_shaft", "z_bottom": ZB, "z_tip": ZT, "half_bottom": 1000.0,
"soffit": {"centre_x": round(cx, 4), "centre_z": round(cz, 4), "radius": 5000.0},
"z_top_left": cp["top_left"]["z"], "z_top_right": cp["top_right"]["z"], "top_half": 3500.0,
"plan_end": {"corner_x": abs(cp["plan_end_left"]["corner_bottom_x"]), "tip_x": 3800.0,
"half_width": cp["plan_end_left"]["half_width"]},
"side": {"half_width_top": cp["side"]["half_width_top"], "half_width_bottom": 1000.0},
"src": {"front": "D1 정면 856/854/855 상면, 857/7D1 끝면, ARC 501/4FF R5,000 (중심은 기둥면·끝 하단 두 점과 R로 재계산)",
"side": cp["side"]["src"], "plan_end": f"{cp['plan_end_left']['src']} / {cp['plan_end_right']['src']}",
"decisions": "R2 직사각 단면(정면 ∩ 측면), R3 우측 상단 3,500(평면값; 정면 작도 3,495.1)",
"not_modelled": "R1 장식 홈(상세 B, 30x30 2줄)"}},
"materials": {"fck": {"pile": "35MPa", "column": "40MPa", "coping": "40MPa", "seat": "40MPa"},
"src": "D1 NOTE '기둥 (40MPa), 코핑 (40MPa), 현장타설말뚝 (35MPa)'; 대좌 40 [추론]"},
"bearing_seats": {
"_note": "pedestal = C3 (along) x C6 (across) box per bearing (h1), rotated as drawn; pad (h2) = frustum (A+200)x(B+200) -> (A+100)x(B+100)",
"pedestal": {"size_x": 1300.0, "size_y": 1400.0}, "pad_margin_bottom": 200, "pad_margin_top": 100,
"bearings": [dict(no=no, type="7000 kN", centre=V["plan"][str(no)]["pedestal"]["centre"],
rotation_deg=V["plan"][str(no)]["pedestal"]["rotation_deg"], h1=110, h2=50, A=900, B=800,
**bear[no]) for no in (1, 2)],
"src": "D1 평면 49D~4A0 / 4A5~4A8 (대좌), 486~48B (패드·받침), ELEVATION TABLE h1·h2·A·B·C3·C6"},
"sole_plates": {"table": [{"no": no, "S": -5.526, "L": 1000, "W": 900, "T": 50, "T1": 78, "T2": 22} for no in (1, 2)],
"h3": 190, "bearing_size": "AB", "bearing_shrink": 100,
"colours": {"bearing": [0, 0, 0], "sole_plate": [150, 150, 150]},
"sole_plate_src": "D1 솔플레이트 상세 TABLE (S, L, W, T, T1, T2); L 교축 방향; S < 0 -> 두꺼운 쪽 -Y (R4, 운곡교 규칙 [추론])",
"src": "교량받침 = A x B (900 x 800), h3 190 (ELEVATION TABLE); 솔플레이트 T = h4 50"},
"rebar_marks": {},
"rebar_marks_src": "철근 G2~G4 다음 Task",
}
expected = {
"_schema": "pier-T-expected/1",
"_note": "독립 기대값: D1 치수 문자·EL 문자·표에서 옮긴 값. params와 별도. [작도] = 치수 문자 없음(작도값)",
"pier_id": "P1",
"tol_mm": 1.0,
"checkers": {"G1": "g1_shaft.py"},
"negative": {"G1": [{"id": "BS02", "d": [3, 0, 0]}, {"id": "BP01", "d": [0, 0, 2]}],
"G5": [{"id": "BB02", "d": [5, 0, 0]}, {"id": "SP01", "d": [0, 0, 5]}]},
"planned_fail": [],
"G1": {
"pile": {"z_bottom": 32752, "segments": [{"d": 2350, "length": 7500}, {"d": 2500, "length": 5200}],
"src": "'EL=32.752', '75, 2,350, 75', '2,500', '7,500', '5,200'"},
"column": {"d": 2000, "z0": 45452, "length": 12000, "src": "'2,000', 'EL=45.452', '11,500' + '500'"},
"coping": {"top_length": 7000, "bottom_length": 7600, "column_face_half": 1000, "soffit_rise": 1500,
"soffit_radius": 5000, "width_y": 2200, "side_bottom_width": 2000, "z_top_left": 59952,
"z_top_right": 60267, "top_step": 315, "plan_end_corner_x": 3650,
"src": "'7,000', '7,600' (2,800+2,000+2,800), '1,500', '1,000', 'R=5,000', '2,200' (1,100+1,100), "
"'EL=59.952', 'EL=60.267', '315'; plan_end_corner_x [작도] D1 평면 51A"},
"total_height": 27200,
"seats": {"table": {"1": [60112, 110, 50, 900, 800], "2": [60427, 110, 50, 900, 800]},
"pos": {"1": [-2250, 0], "2": [2250, 0]}, "pedestal_along": 1400, "pedestal_across": 1300,
"rotation_deg": {"1": -5.55, "2": -9.194}, "pad_margin": [200, 100],
"src": "ELEVATION TABLE; 평면 '2,250' x2; C3 1,400, C6 1,300; rotation [작도] D1 평면"},
},
"G5": {"el_x": {"1": 60112, "2": 60427}, "S": {"1": -5.526, "2": -5.526}, "W": {"1": 900, "2": 900},
"L": 1000, "T": 50, "h3": 190, "bearing_size": {"1": [900, 800], "2": [900, 800]}, "bearing_shrink": 100,
"thick_end": "-Y", "colours": {"bearing": [0, 0, 0], "sole_plate": [150, 150, 150]},
"src": "ELEVATION TABLE (EL.'X', h3, A, B, h4 = T); 솔플레이트 상세 TABLE (S, L, W, T1 78 / T2 22 -> 두꺼운 쪽 -Y, R4)"},
}
if __name__ == "__main__":
if (store.DATA / NAME).exists():
sys.exit(f"pier exists: {NAME}")
c = webapp.app.test_client()
r = c.post("/pier/new", data={"name": NAME, "type": "T형", "template": "tpl:산외교울산P1형"})
assert r.status_code == 302, r.status_code
dwgs = sorted((ROOT / "RAG").glob("Ramp-A교 P1 교각(*).dwg"))
r = c.post(f"/pier/{NAME}/upload", data={"dwg": [(BytesIO(p.read_bytes()), p.name) for p in dwgs]},
content_type="multipart/form-data")
assert r.status_code == 302, r.status_code
p = store.Pier(NAME)
params["display"] = p.load("params")["display"] # Ulsan display (layers incl. 말뚝, palette)
expected["rebar_palette"] = p.load("expected")["rebar_palette"]
p.save("params", params)
p.save("expected", expected)
meta = store.rjson(p.dir / "pier.json")
meta["template"] = "신규 판독 (Ramp-A교 P1 판독서 v0.1.00)"
store.wjson(p.dir / "pier.json", meta)
print("created", p.dir, "texts", len(p.texts()))
+17
View File
@@ -0,0 +1,17 @@
"""Run gates G1 then G5 of the app pier 'Ramp-A교P1' with the engine (no confirmation / approval: those are the
user's review records in the app). Results: data/Ramp-A교P1/runs/current/G*_gate.json, G*.3dm.
usage: python run_gates.py [G1|G5 ...]"""
import json
import sys
from pathlib import Path
APP = Path(__file__).resolve().parents[2] / "Code" / "교각모델링앱"
sys.path.insert(0, str(APP))
from engine.gates import run_gate # noqa: E402
D = APP / "data" / "Ramp-A교P1"
for g in sys.argv[1:] or ["G1", "G5"]:
rep = run_gate(g, D / "params.json", D / "expected.json", D / "runs" / "current")
print(json.dumps({k: rep[k] for k in ("gate", "verdict", "summary", "unplanned_fails", "reload", "secs")}, ensure_ascii=False))
if rep["verdict"] != "PASS":
break
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

@@ -0,0 +1,178 @@
4FF CS-CONC-MAJR ARC c(90680.5,-480092.3) r 5000.0 99.7..136.7
500 CS-CONC-MAJR ARC c(90680.5,-480092.3) r 5000.0 99.7..136.7
501 CS-CONC-MAJR ARC c(81402.9,-480092.3) r 5000.0 43.3..80.3
502 CS-CONC-MAJR ARC c(81402.9,-480092.3) r 5000.0 43.3..80.3
74A CS-CONC-MAJR ARC c(84773.7,-494123.1) r 94.4 298.1..61.9
74B CS-CONC-MAJR ARC c(84773.7,-495289.7) r 94.4 349.8..61.9
74C CS-CONC-MAJR ARC c(84773.7,-494956.4) r 94.4 298.1..61.9
74D CS-CONC-MAJR ARC c(84773.7,-495123.1) r 94.4 298.1..61.9
74E CS-CONC-MAJR ARC c(84773.7,-494789.7) r 94.4 298.1..61.9
74F CS-CONC-MAJR ARC c(84773.7,-493789.7) r 94.4 298.1..308.9
750 CS-CONC-MAJR ARC c(84773.7,-493956.4) r 94.4 298.1..61.9
751 CS-CONC-MAJR ARC c(84773.7,-494289.7) r 94.4 298.1..61.9
752 CS-CONC-MAJR ARC c(84773.7,-494456.4) r 94.4 298.1..61.9
753 CS-CONC-MAJR ARC c(84773.7,-494623.1) r 94.4 298.1..61.9
754 CS-CONC-MAJR ARC c(87309.7,-494123.1) r 94.4 118.1..241.9
755 CS-CONC-MAJR ARC c(87309.7,-495289.7) r 94.4 118.1..190.2
756 CS-CONC-MAJR ARC c(87309.7,-494956.4) r 94.4 118.1..241.9
757 CS-CONC-MAJR ARC c(87309.7,-495123.1) r 94.4 118.1..241.9
758 CS-CONC-MAJR ARC c(87309.7,-494789.7) r 94.4 118.1..241.9
759 CS-CONC-MAJR ARC c(87309.7,-493789.7) r 94.4 231.1..241.9
75A CS-CONC-MAJR ARC c(87309.7,-493956.4) r 94.4 118.1..241.9
75B CS-CONC-MAJR ARC c(87309.7,-494289.7) r 94.4 118.1..241.9
75C CS-CONC-MAJR ARC c(87309.7,-494456.4) r 94.4 118.1..241.9
75D CS-CONC-MAJR ARC c(87309.7,-494623.1) r 94.4 118.1..241.9
75E CS-CONC-MAJR ARC c(84773.7,-496123.1) r 94.4 298.1..61.9
75F CS-CONC-MAJR ARC c(84773.7,-497289.7) r 94.4 349.8..61.9
760 CS-CONC-MAJR ARC c(84773.7,-496956.4) r 94.4 298.1..61.9
761 CS-CONC-MAJR ARC c(84773.7,-497123.1) r 94.4 298.1..61.9
762 CS-CONC-MAJR ARC c(84773.7,-496789.7) r 94.4 298.1..61.9
763 CS-CONC-MAJR ARC c(84773.7,-495289.7) r 94.4 298.1..0.0
764 CS-CONC-MAJR ARC c(84773.7,-495456.4) r 94.4 298.1..61.9
765 CS-CONC-MAJR ARC c(84773.7,-495623.1) r 94.4 298.1..61.9
766 CS-CONC-MAJR ARC c(84773.7,-495789.7) r 94.4 298.1..61.9
767 CS-CONC-MAJR ARC c(84773.7,-495956.4) r 94.4 298.1..61.9
768 CS-CONC-MAJR ARC c(84773.7,-496289.7) r 94.4 298.1..61.9
769 CS-CONC-MAJR ARC c(84773.7,-496456.4) r 94.4 298.1..61.9
76A CS-CONC-MAJR ARC c(84773.7,-496623.1) r 94.4 298.1..61.9
76B CS-CONC-MAJR ARC c(87309.7,-496123.1) r 94.4 118.1..241.9
76C CS-CONC-MAJR ARC c(87309.7,-497289.7) r 94.4 118.1..190.2
76D CS-CONC-MAJR ARC c(87309.7,-496956.4) r 94.4 118.1..241.9
76E CS-CONC-MAJR ARC c(87309.7,-497123.1) r 94.4 118.1..241.9
76F CS-CONC-MAJR ARC c(87309.7,-496789.7) r 94.4 118.1..241.9
770 CS-CONC-MAJR ARC c(87309.7,-495289.7) r 94.4 180.0..241.9
771 CS-CONC-MAJR ARC c(87309.7,-495456.4) r 94.4 118.1..241.9
772 CS-CONC-MAJR ARC c(87309.7,-495623.1) r 94.4 118.1..241.9
773 CS-CONC-MAJR ARC c(87309.7,-495789.7) r 94.4 118.1..241.9
774 CS-CONC-MAJR ARC c(87309.7,-495956.4) r 94.4 118.1..241.9
775 CS-CONC-MAJR ARC c(87309.7,-496289.7) r 94.4 118.1..241.9
776 CS-CONC-MAJR ARC c(87309.7,-496456.4) r 94.4 118.1..241.9
777 CS-CONC-MAJR ARC c(87309.7,-496623.1) r 94.4 118.1..241.9
778 CS-CONC-MAJR ARC c(84772.3,-498163.2) r 94.4 298.1..61.9
779 CS-CONC-MAJR ARC c(84772.3,-499329.9) r 94.4 349.8..61.9
77A CS-CONC-MAJR ARC c(84772.3,-498996.5) r 94.4 298.1..61.9
77B CS-CONC-MAJR ARC c(84772.3,-499163.2) r 94.4 298.1..61.9
77C CS-CONC-MAJR ARC c(84772.3,-498829.9) r 94.4 298.1..61.9
77D CS-CONC-MAJR ARC c(84772.3,-497329.9) r 94.4 298.1..0.0
77E CS-CONC-MAJR ARC c(84772.3,-497496.5) r 94.4 298.1..61.9
77F CS-CONC-MAJR ARC c(84772.3,-497663.2) r 94.4 298.1..61.9
780 CS-CONC-MAJR ARC c(84772.3,-497829.9) r 94.4 298.1..61.9
781 CS-CONC-MAJR ARC c(84772.3,-497996.5) r 94.4 298.1..61.9
782 CS-CONC-MAJR ARC c(84772.3,-498329.9) r 94.4 298.1..61.9
783 CS-CONC-MAJR ARC c(84772.3,-498496.5) r 94.4 298.1..61.9
784 CS-CONC-MAJR ARC c(84772.3,-498663.2) r 94.4 298.1..61.9
785 CS-CONC-MAJR ARC c(87308.2,-498163.2) r 94.4 118.1..241.9
786 CS-CONC-MAJR ARC c(87308.2,-499329.9) r 94.4 118.1..190.2
787 CS-CONC-MAJR ARC c(87308.2,-498996.5) r 94.4 118.1..241.9
788 CS-CONC-MAJR ARC c(87308.2,-499163.2) r 94.4 118.1..241.9
789 CS-CONC-MAJR ARC c(87308.2,-498829.9) r 94.4 118.1..241.9
78A CS-CONC-MAJR ARC c(87308.2,-497329.9) r 94.4 180.0..241.9
78B CS-CONC-MAJR ARC c(87308.2,-497496.5) r 94.4 118.1..241.9
78C CS-CONC-MAJR ARC c(87308.2,-497663.2) r 94.4 118.1..241.9
78D CS-CONC-MAJR ARC c(87308.2,-497829.9) r 94.4 118.1..241.9
78E CS-CONC-MAJR ARC c(87308.2,-497996.5) r 94.4 118.1..241.9
78F CS-CONC-MAJR ARC c(87308.2,-498329.9) r 94.4 118.1..241.9
790 CS-CONC-MAJR ARC c(87308.2,-498496.5) r 94.4 118.1..241.9
791 CS-CONC-MAJR ARC c(87308.2,-498663.2) r 94.4 118.1..241.9
858 CS-CONC-MAJR ARC c(84773.7,-500179.9) r 94.4 298.1..61.9
859 CS-CONC-MAJR ARC c(84773.7,-501346.5) r 94.4 349.8..61.9
85A CS-CONC-MAJR ARC c(84773.7,-501013.2) r 94.4 298.1..61.9
85B CS-CONC-MAJR ARC c(84773.7,-501179.9) r 94.4 298.1..61.9
85C CS-CONC-MAJR ARC c(84773.7,-500846.5) r 94.4 298.1..61.9
85D CS-CONC-MAJR ARC c(84773.7,-499346.5) r 94.4 298.1..0.0
85E CS-CONC-MAJR ARC c(84773.7,-499513.2) r 94.4 298.1..61.9
85F CS-CONC-MAJR ARC c(84773.7,-499679.9) r 94.4 298.1..61.9
860 CS-CONC-MAJR ARC c(84773.7,-499846.5) r 94.4 298.1..61.9
861 CS-CONC-MAJR ARC c(84773.7,-500013.2) r 94.4 298.1..61.9
862 CS-CONC-MAJR ARC c(84773.7,-500346.5) r 94.4 298.1..61.9
863 CS-CONC-MAJR ARC c(84773.7,-500513.2) r 94.4 298.1..61.9
864 CS-CONC-MAJR ARC c(84773.7,-500679.9) r 94.4 298.1..61.9
865 CS-CONC-MAJR ARC c(87309.7,-500179.9) r 94.4 118.1..241.9
866 CS-CONC-MAJR ARC c(87309.7,-501346.5) r 94.4 118.1..190.2
867 CS-CONC-MAJR ARC c(87309.7,-501013.2) r 94.4 118.1..241.9
868 CS-CONC-MAJR ARC c(87309.7,-501179.9) r 94.4 118.1..241.9
869 CS-CONC-MAJR ARC c(87309.7,-500846.5) r 94.4 118.1..241.9
86A CS-CONC-MAJR ARC c(87309.7,-499346.5) r 94.4 180.0..241.9
86B CS-CONC-MAJR ARC c(87309.7,-499513.2) r 94.4 118.1..241.9
86C CS-CONC-MAJR ARC c(87309.7,-499679.9) r 94.4 118.1..241.9
86D CS-CONC-MAJR ARC c(87309.7,-499846.5) r 94.4 118.1..241.9
86E CS-CONC-MAJR ARC c(87309.7,-500013.2) r 94.4 118.1..241.9
86F CS-CONC-MAJR ARC c(87309.7,-500346.5) r 94.4 118.1..241.9
870 CS-CONC-MAJR ARC c(87309.7,-500513.2) r 94.4 118.1..241.9
871 CS-CONC-MAJR ARC c(87309.7,-500679.9) r 94.4 118.1..241.9
55A CS-CONC-MAJR LINE (85041.7,-476663.2)-(87041.7,-476663.2) len 2000.0
60F CS-CONC-MAJR LINE (85041.7,-488663.2)-(85041.7,-476663.2) len 12000.0
610 CS-CONC-MAJR LINE (87041.7,-488663.2)-(87041.7,-476663.2) len 12000.0
7D1 CS-CONC-MAJR LINE (89841.7,-475163.2)-(89536.8,-473848.2) len 1349.9
81E CS-CONC-MAJR LINE (87641.7,-473848.2)-(88941.7,-473848.2) len 1300.0
81F CS-CONC-MAJR LINE (88666.7,-473638.2)-(87916.7,-473638.2) len 750.0
820 CS-CONC-MAJR LINE (88666.7,-473588.2)-(88666.7,-473638.2) len 50.0
821 CS-CONC-MAJR LINE (87916.7,-473588.2)-(88666.7,-473588.2) len 750.0
822 CS-CONC-MAJR LINE (87916.7,-473638.2)-(87916.7,-473588.2) len 50.0
823 CS-CONC-MAJR LINE (88291.7,-473688.2)-(88291.7,-473638.2) len 50.0
824 CS-CONC-MAJR LINE (88291.7,-473688.2)-(88291.7,-473638.2) len 50.0
825 CS-CONC-MAJR LINE (88141.7,-473688.2)-(88141.7,-473638.2) len 50.0
826 CS-CONC-MAJR LINE (88441.7,-473688.2)-(88441.7,-473638.2) len 50.0
827 CS-CONC-MAJR LINE (88091.7,-473688.2)-(88091.7,-473638.2) len 50.0
828 CS-CONC-MAJR LINE (88491.7,-473688.2)-(88491.7,-473638.2) len 50.0
829 CS-CONC-MAJR LINE (88062.6,-473688.2)-(88062.6,-473638.2) len 50.0
82A CS-CONC-MAJR LINE (88520.8,-473688.2)-(88520.8,-473638.2) len 50.0
82B CS-CONC-MAJR LINE (88046.8,-473688.2)-(88046.8,-473638.2) len 50.0
82C CS-CONC-MAJR LINE (88536.6,-473688.2)-(88536.6,-473638.2) len 50.0
82D CS-CONC-MAJR LINE (88541.7,-473688.2)-(88041.7,-473688.2) len 500.0
82E CS-CONC-MAJR LINE (88541.7,-473638.2)-(88541.7,-473688.2) len 50.0
82F CS-CONC-MAJR LINE (88041.7,-473638.2)-(88541.7,-473638.2) len 500.0
830 CS-CONC-MAJR LINE (88041.7,-473688.2)-(88041.7,-473638.2) len 50.0
831 CS-CONC-MAJR LINE (88641.7,-473738.2)-(87941.7,-473738.2) len 700.0
832 CS-CONC-MAJR LINE (88591.7,-473688.2)-(88641.7,-473738.2) len 70.7
833 CS-CONC-MAJR LINE (87991.7,-473688.2)-(88591.7,-473688.2) len 600.0
834 CS-CONC-MAJR LINE (87941.7,-473738.2)-(87991.7,-473688.2) len 70.7
835 CS-CONC-MAJR LINE (88941.7,-473738.2)-(88941.7,-473848.2) len 110.0
836 CS-CONC-MAJR LINE (87641.7,-473738.2)-(88941.7,-473738.2) len 1300.0
837 CS-CONC-MAJR LINE (87641.7,-473848.2)-(87641.7,-473738.2) len 110.0
838 CS-CONC-MAJR LINE (83141.7,-474163.2)-(84441.7,-474163.2) len 1300.0
839 CS-CONC-MAJR LINE (84166.7,-473953.2)-(83416.7,-473953.2) len 750.0
83A CS-CONC-MAJR LINE (84166.7,-473903.2)-(84166.7,-473953.2) len 50.0
83B CS-CONC-MAJR LINE (83416.7,-473903.2)-(84166.7,-473903.2) len 750.0
83C CS-CONC-MAJR LINE (83416.7,-473953.2)-(83416.7,-473903.2) len 50.0
83D CS-CONC-MAJR LINE (83791.7,-474003.2)-(83791.7,-473953.2) len 50.0
83E CS-CONC-MAJR LINE (83791.7,-474003.2)-(83791.7,-473953.2) len 50.0
83F CS-CONC-MAJR LINE (83641.7,-474003.2)-(83641.7,-473953.2) len 50.0
840 CS-CONC-MAJR LINE (83941.7,-474003.2)-(83941.7,-473953.2) len 50.0
841 CS-CONC-MAJR LINE (83591.7,-474003.2)-(83591.7,-473953.2) len 50.0
842 CS-CONC-MAJR LINE (83991.7,-474003.2)-(83991.7,-473953.2) len 50.0
843 CS-CONC-MAJR LINE (83562.6,-474003.2)-(83562.6,-473953.2) len 50.0
844 CS-CONC-MAJR LINE (84020.8,-474003.2)-(84020.8,-473953.2) len 50.0
845 CS-CONC-MAJR LINE (83546.8,-474003.2)-(83546.8,-473953.2) len 50.0
846 CS-CONC-MAJR LINE (84036.6,-474003.2)-(84036.6,-473953.2) len 50.0
847 CS-CONC-MAJR LINE (84041.7,-474003.2)-(83541.7,-474003.2) len 500.0
848 CS-CONC-MAJR LINE (84041.7,-473953.2)-(84041.7,-474003.2) len 50.0
849 CS-CONC-MAJR LINE (83541.7,-473953.2)-(84041.7,-473953.2) len 500.0
84A CS-CONC-MAJR LINE (83541.7,-474003.2)-(83541.7,-473953.2) len 50.0
84B CS-CONC-MAJR LINE (84141.7,-474053.2)-(83441.7,-474053.2) len 700.0
84C CS-CONC-MAJR LINE (84091.7,-474003.2)-(84141.7,-474053.2) len 70.7
84D CS-CONC-MAJR LINE (83491.7,-474003.2)-(84091.7,-474003.2) len 600.0
84E CS-CONC-MAJR LINE (83441.7,-474053.2)-(83491.7,-474003.2) len 70.7
84F CS-CONC-MAJR LINE (84441.7,-474053.2)-(84441.7,-474163.2) len 110.0
850 CS-CONC-MAJR LINE (83141.7,-474053.2)-(84441.7,-474053.2) len 1300.0
851 CS-CONC-MAJR LINE (83141.7,-474163.2)-(83141.7,-474053.2) len 110.0
854 CS-CONC-MAJR LINE (86041.7,-473848.2)-(89536.8,-473848.2) len 3495.1
855 CS-CONC-MAJR LINE (86041.7,-473848.2)-(86041.7,-474163.2) len 315.0
856 CS-CONC-MAJR LINE (82541.7,-474163.2)-(86041.7,-474163.2) len 3500.0
857 CS-CONC-MAJR LINE (82241.7,-475163.2)-(82541.7,-474163.2) len 1044.0
872 CS-CONC-MAJR LINE (84866.7,-493863.2)-(87216.7,-493863.2) len 2350.0
873 CS-CONC-MAJR LINE (84866.7,-493863.2)-(84866.7,-501363.2) len 7500.0
874 CS-CONC-MAJR LINE (84791.7,-493863.2)-(84866.7,-493863.2) len 75.0
875 CS-CONC-MAJR LINE (87216.7,-493863.2)-(87216.7,-501363.2) len 7500.0
876 CS-CONC-MAJR LINE (87291.7,-493863.2)-(87216.7,-493863.2) len 75.0
87E CS-CONC-MAJR LINE (84866.7,-501363.2)-(87216.7,-501363.2) len 2350.0
87F CS-CONC-MAJR LINE (87291.7,-493218.3)-(87291.7,-488663.2) len 4555.1
880 CS-CONC-MAJR LINE (87291.7,-493863.2)-(87291.7,-493218.3) len 644.9
881 CS-CONC-MAJR LINE (84791.7,-493218.3)-(84791.7,-488663.2) len 4555.1
882 CS-CONC-MAJR LINE (84791.7,-493863.2)-(84791.7,-493218.3) len 644.9
883 CS-CNTL LINE (86041.7,-493218.3)-(86041.7,-501363.2) len 8144.9
884 CS-CNTL LINE (86041.7,-488663.2)-(86041.7,-493218.3) len 4555.1
885 CS-CNTL LINE (86041.7,-473158.2)-(86041.7,-488663.2) len 15505.0
89E CC-GRND LINE (81441.7,-488163.2)-(90611.1,-488163.2) len 9169.4
89F CS-CONC-MAJR LINE (84791.7,-488663.2)-(87291.7,-488663.2) len 2500.0
7DC CS-CONC-MAJR PLINE closed=False [(np.float64(84591.7), np.float64(-474163.2)), (np.float64(85855.3), np.float64(-476150.9)), (np.float64(86228.1), np.float64(-476150.9)), (np.float64(87662.4), np.float64(-473848.2))]
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

+214
View File
@@ -0,0 +1,214 @@
{
"coping": {
"top_left": {
"x0": -3500.0,
"x1": 0.0,
"z": 59952.0,
"src": "D1 856"
},
"top_right": {
"x0": 0.0,
"x1": 3495.1,
"z": 60267.0,
"src": "D1 854"
},
"end_left_front": {
"bottom": [
-3800.0,
58952.0
],
"top": [
-3500.0,
59952.0
],
"src": "D1 857"
},
"end_right_front": {
"bottom": [
3800.0,
58952.0
],
"top": [
3495.1,
60267.0
],
"src": "D1 7D1"
},
"soffit_left": {
"centre": [
-4638.8,
54022.9
],
"radius": 5000.0,
"ends": [
[
-1000.0,
57452.0
],
[
-3800.0,
58952.0
]
],
"src": "D1 ARC 501"
},
"soffit_right": {
"centre": [
4638.8,
54022.9
],
"radius": 5000.0,
"ends": [
[
3800.0,
58952.0
],
[
1000.0,
57452.0
]
],
"src": "D1 ARC 4FF"
},
"column_top_z": 57452.0,
"side": {
"half_width_top": 1100.0,
"taper": {
"from": [
-1100.0,
58952.0
],
"to": [
-1000.0,
57452.0
]
},
"src": "D1 측면도 520/521 (2,200), 503/504 (2,200 -> 2,000)"
},
"plan_end_left": {
"top_edge_x": -3500.0,
"corner_bottom_x": -3650.0,
"half_width": 1100.0,
"sagitta": 150.0,
"radius": 4108.3,
"src": "D1 평면 51A bulge 0.136"
},
"plan_end_right": {
"top_edge_x": 3500.0,
"corner_bottom_x": 3650.0,
"half_width": 1100.0,
"sagitta": 150.0,
"radius": 4108.3,
"src": "D1 평면 519 bulge -0.136"
}
},
"column": {
"d": 2000.0,
"z0": 45452.0,
"z1": 57452.0,
"src": "D1 60F/610"
},
"pile_upper": {
"d": 2500.0,
"z0": 40252.0,
"z1": 45452.0,
"src": "D1 880-87F / 882-881, 89F"
},
"pile_lower": {
"d": 2350.0,
"z0": 32752.0,
"z1": 40252.0,
"src": "D1 873/875, 87E"
},
"ground_z": 45952.0,
"plan": {
"1": {
"pedestal": {
"centre": [
-2250.0,
0.0
],
"along": 1400.0,
"across": 1300.0,
"rotation_deg": -5.55,
"src": "D1 평면 49D/49E/49F/4A0"
},
"pads": [
{
"centre": [
-2250.0,
0.0
],
"along": 900.0,
"across": 800.0,
"rotation_deg": -5.55,
"src": "D1 평면 486"
},
{
"centre": [
-2250.0,
0.0
],
"along": 1000.0,
"across": 900.0,
"rotation_deg": -5.55,
"src": "D1 평면 487"
},
{
"centre": [
-2250.0,
0.0
],
"along": 1100.0,
"across": 1000.0,
"rotation_deg": -5.55,
"src": "D1 평면 488"
}
]
},
"2": {
"pedestal": {
"centre": [
2250.0,
0.0
],
"along": 1400.0,
"across": 1300.0,
"rotation_deg": -9.194,
"src": "D1 평면 4A5/4A6/4A7/4A8"
},
"pads": [
{
"centre": [
2250.0,
0.0
],
"along": 900.0,
"across": 800.0,
"rotation_deg": -9.194,
"src": "D1 평면 489"
},
{
"centre": [
2250.0,
0.0
],
"along": 1000.0,
"across": 900.0,
"rotation_deg": -9.194,
"src": "D1 평면 48A"
},
{
"centre": [
2250.0,
0.0
],
"along": 1100.0,
"across": 1000.0,
"rotation_deg": -9.194,
"src": "D1 평면 48B"
}
]
}
}
}
+46
View File
@@ -0,0 +1,46 @@
{
"chain_hoop_EL": [
43492.0,
45492.0,
47592.0,
59292.0,
59852.0,
59952.0
],
"chain_tie_EL": [
45492.0,
47592.0,
59292.0,
59852.0,
59952.0
],
"main_front": {
"x_min_rel": -875.0,
"x_max_rel": 875.0,
"bottom_EL": 43492.0,
"top_EL": 59752.0
},
"hoops_front_drawn": {
"count": 76,
"first_EL": 43542.0,
"last_EL": 59092.0
},
"main_section": {
"count": 54,
"radius": 875.0,
"first_deg": 0.001,
"step_deg": 6.667
},
"ties_section": [
{
"handle": "36C",
"len": 1795.0,
"dir": "Y"
},
{
"handle": "36D",
"len": 1795.0,
"dir": "X"
}
]
}
+53
View File
@@ -0,0 +1,53 @@
{
"G-G": {
"concrete_and_hoop_radii": [
1025.0,
1250.0
],
"bar_symbol_radius": 1002.5,
"bar_count": 60,
"first_deg": 1.475,
"step_deg": null,
"tie_lines": [
2050.0,
2050.0
]
},
"J-J": {
"concrete_and_hoop_radii": [
1025.0,
1175.0
],
"bar_symbol_radius": 1002.5,
"bar_count": 61,
"first_deg": 1.475,
"step_deg": 5.9017,
"tie_lines": [
2050.0,
2050.0
]
},
"hoop_levels_from_table": {
"count": 48,
"top": 45342.0,
"bottom": 33192.0,
"rule": "shaft top -110, 13@150, 34@300 (+H6 340 +100 = shaft bottom)"
},
"tie_levels_from_table": {
"count": 24,
"top": 45192.0,
"bottom": 33192.0,
"rule": "shaft top -260, 6@300, 17@600 (+H5 340 +100)"
},
"chain_check": {
"hoops": 12700,
"ties": 12700,
"H": 12700
},
"main_span": {
"top": 45342.0,
"bottom": 33192.0,
"length": 12150.0,
"P1-1 + P1-2": 12150
}
}
+1
View File
@@ -0,0 +1 @@
{"bars": [{"mark": "C1", "occ": "y-1000", "pts": [[-3604.73, -1000, 59030.4], [-3425.6, -1000, 59852.0], [3492.1, -1000, 59852.0], [3607.8, -1000, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번; 평면 모서리 단축 [추론]"}, {"mark": "C4", "occ": "y-875", "pts": [[-3632.17, -875, 59030.4], [-3425.6, -875, 59852.0], [3492.1, -875, 59852.0], [3635.77, -875, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번; 평면 모서리 단축 [추론]"}, {"mark": "C1", "occ": "y-750", "pts": [[-3655.85, -750, 59030.4], [-3425.6, -750, 59852.0], [3492.1, -750, 59852.0], [3659.91, -750, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번; 평면 모서리 단축 [추론]"}, {"mark": "C4", "occ": "y-625", "pts": [[-3672.0, -625, 59030.4], [-3425.6, -625, 59852.0], [3492.1, -625, 59852.0], [3679.3, -625, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번"}, {"mark": "C1", "occ": "y-500", "pts": [[-3672.0, -500, 59030.4], [-3425.6, -500, 59852.0], [3492.1, -500, 59852.0], [3679.3, -500, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번"}, {"mark": "C4", "occ": "y-375", "pts": [[-3672.0, -375, 59030.4], [-3425.6, -375, 59852.0], [3492.1, -375, 59852.0], [3679.3, -375, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번"}, {"mark": "C1", "occ": "y-250", "pts": [[-3672.0, -250, 59030.4], [-3425.6, -250, 59852.0], [3492.1, -250, 59852.0], [3679.3, -250, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번"}, {"mark": "C4", "occ": "y-125", "pts": [[-3672.0, -125, 59030.4], [-3425.6, -125, 59852.0], [3492.1, -125, 59852.0], [3679.3, -125, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번"}, {"mark": "C1", "occ": "y+0", "pts": [[-3672.0, 0, 59030.4], [-3425.6, 0, 59852.0], [3492.1, 0, 59852.0], [3679.3, 0, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번"}, {"mark": "C4", "occ": "y+125", "pts": [[-3672.0, 125, 59030.4], [-3425.6, 125, 59852.0], [3492.1, 125, 59852.0], [3679.3, 125, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번"}, {"mark": "C1", "occ": "y+250", "pts": [[-3672.0, 250, 59030.4], [-3425.6, 250, 59852.0], [3492.1, 250, 59852.0], [3679.3, 250, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번"}, {"mark": "C4", "occ": "y+375", "pts": [[-3672.0, 375, 59030.4], [-3425.6, 375, 59852.0], [3492.1, 375, 59852.0], [3679.3, 375, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번"}, {"mark": "C1", "occ": "y+500", "pts": [[-3672.0, 500, 59030.4], [-3425.6, 500, 59852.0], [3492.1, 500, 59852.0], [3679.3, 500, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번"}, {"mark": "C4", "occ": "y+625", "pts": [[-3672.0, 625, 59030.4], [-3425.6, 625, 59852.0], [3492.1, 625, 59852.0], [3679.3, 625, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번"}, {"mark": "C1", "occ": "y+750", "pts": [[-3655.85, 750, 59030.4], [-3425.6, 750, 59852.0], [3492.1, 750, 59852.0], [3659.91, 750, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번; 평면 모서리 단축 [추론]"}, {"mark": "C4", "occ": "y+875", "pts": [[-3632.17, 875, 59030.4], [-3425.6, 875, 59852.0], [3492.1, 875, 59852.0], [3635.77, 875, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번; 평면 모서리 단축 [추론]"}, {"mark": "C1", "occ": "y+1000", "pts": [[-3604.73, 1000, 59030.4], [-3425.6, 1000, 59852.0], [3492.1, 1000, 59852.0], [3607.8, 1000, 59031.7]], "r": 65.25, "design_length": 8617, "src": "D2 정면 1단 작도선, 단면 A-A/B-B @125 교번; 평면 모서리 단축 [추론]"}, {"mark": "C2", "occ": "y-1000", "pts": [[-3485.6, -1000, 59652.0], [-3455.6, -1000, 59752.0], [3513.16, -1000, 59752.0], [3526.3, -1000, 59652.0]], "r": 65.25, "design_length": 7177, "src": "D2 정면 2단 작도선, 단면 A-A/B-B @125 교번; 평면 모서리 단축 [추론]"}, {"mark": "C5", "occ": "y-875", "pts": [[-3485.6, -875, 59652.0], [-3455.6, -875, 59752.0], [3515.0, -875, 59752.0], [3537.8, -875, 59652.0]], "r": 65.25, "design_length": 7177, "src": "D2 정면 2단 작도선, 단면 A-A/B-B @125 교번"}, {"mark": "C2", "occ": "y-750", "pts": [[-3485.6, -750, 59652.0], [-3455.6, -750, 59752.0], [3515.0, -750, 59752.0], [3537.8, -750, 59652.0]], "r": 65.25, "design_length": 7177, "src": "D2 정Line truncated
View File
Whitespace-only changes.
+3
View File
@@ -0,0 +1,3 @@
__pycache__/
.pytest_cache/
.venv/
+38
View File
@@ -0,0 +1,38 @@
# 작업 규칙
## 언어
- 코드와 변수명은 영어. 응답, 문서, 코드 주석은 한국어 존댓말. 일본어·중국어 금지.
- 쉬운 문장으로 설명. 나는 교각과 코딩을 배우는 중임.
## 문서
- 설계 규칙(피복, 철근 형상, R값, 레이어 이름 등)은 docs/ 폴더 md 파일에 정리.
- 코드와 문서가 충돌하면 먼저 알려줄 것.
- 작업이 끝나면 docs/ 와 README 업데이트 여부를 물어볼 것.
## 개발 방식
- 기능 추가 전에 테스트를 먼저 만들 것. 계산(좌표, 길이, 간격, 철근 수량)은 Rhino 없이 테스트 가능하게 분리.
- 버그는 재현 테스트가 실패하는 것을 먼저 확인한 뒤 수정.
- 같은 방법으로 2번 실패하면 다른 가설을 세워 시도.
- 철근 코드는 일회성으로 실행하지 말고 scripts/ 폴더에 .py 파일로 저장하고 재사용.
## Git
- 브랜치를 임의로 만들지 말 것. 현재 브랜치에 완료된 작업만 커밋.
- 커밋 메시지에 docs/tasks.md 의 작업 번호를 넣을 것.
- 할 일은 docs/tasks.md 에 번호, 내용, 예상 시간, 상태로 기록.
## 경로 안내
- 작업에서 쓰는 모든 경로는 docs/paths.md 에 기록하고, 새 경로가 생기면 바로 추가할 것.
- 파일을 만들거나 수정하거나 읽을 때마다 응답 끝에 "이번에 쓴 경로"를 전체 경로로 알려줄 것. 사용자가 경로를 자주 헷갈려 함.
## 저장 공간
- C 드라이브 Python(약 156MB)은 그대로 둠. 용량이 큰 패키지(numpy, 3D 라이브러리 등)가 필요하면 설치 전에 알리고, D 드라이브(`D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\.venv`)에 가상환경으로 설치할 것.
- 용량이 큰 파일(렌더링, 백업, DXF, Rhino 파일)은 항상 D 드라이브에 저장.
## 폴더 구조
- `docs/` 설계 규칙과 할 일 (`design_rules.md`, `tasks.md`)
- `scripts/` 재사용 철근 코드. 계산 모듈(Rhino 없이 실행)과 Rhino 실행 스크립트를 분리
- `tests/` 계산 모듈 테스트
## 참고 (기존 프로젝트)
- 기존 코드와 방법: `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\rebar_pipeline\` (README 먼저 읽을 것)
- 설계기준: KDS 24 14 21 (상세는 docs/design_rules.md)
+11
View File
@@ -0,0 +1,11 @@
# 교각 철근 프로그램
교각 두부보 등의 철근을 Rhino에 자동으로 배치하는 프로그램입니다.
- `CLAUDE.md` 작업 규칙
- `docs/design_rules.md` 설계 규칙 (KDS 24 14 21 기준)
- `docs/tasks.md` 할 일 목록
- `scripts/` 재사용 코드 (계산 모듈은 Rhino 없이 실행 가능하게 분리)
- `tests/` 계산 모듈 테스트
기존 작업 자료는 `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\rebar_pipeline\` 에 있습니다.
+46
View File
@@ -0,0 +1,46 @@
# 설계 규칙
기준 문서: KDS 24 14 21 : 2025 콘크리트교 설계기준(한계상태설계법).
조항 추출본: `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\rebar_pipeline\kds\KDS_24_14_21.txt` (조항 번호로 검색).
충돌 시 이 기준이 우선입니다. 기준과 다르게 배근된 철근은 철근재료표 비고에 별도 색(자홍)으로 적습니다.
## 1. 철근 구부림 최소 내면 지름 (KDS 24 14 21 표 4.5-1)
- D16 이하: 2db
- D19 이상: 3.5db
- 참고: 기존 프로젝트는 건축기준(KDS 14 20 50) 반지름 R = 3db(D10~D25), 4db(D29~D35), 5db(D38 이상)를 써 왔고, 이 값은 위 최소값보다 크므로 만족합니다.
- R값은 구부림 각도와 무관합니다. 각도에 따라 달라지는 것은 갈고리 뒤 연장 길이입니다.
## 2. 갈고리
- 표준갈고리 90°~150° 구부림 뒤 연장 길이는 5db 이상 (KDS 24 14 21).
- 기존 프로젝트 적용값: 90°=12db, 135°=6db, 180°=4db(또는 60mm 이상).
## 3. 철근 간격 (4.5.3)
- 수평 순간격: max(1.5db, 1.5Gmax, 40mm) 이상
- 다단 연직 순간격: max(25mm, db) 이상, 같은 연직면
## 4. 피복 (4.4.4)
- 최소피복 = max(부착 db, 환경 표 4.4-4) + 설계편차 10mm
- 피복이 70mm를 넘으면 표피철근(0.005Act,ext) 필요
- 기존 프로젝트 두부보 상면 피복: 120mm
## 5. 전단철근 (4.6.2)
- 종방향 간격 ≤ 0.75d(1+cotα), 폭방향 간격 ≤ 0.75d, 600mm
- ρv,min = 0.08√fck/fy
- 단면 내 50% 이상 폐쇄형 또는 U형
## 6. 겹침이음 (4.5.5)
- 겹친 두 철근의 횡방향 순거리 ≤ 4db, 50mm
## 7. 공칭 철근직경 (KS D 3504)
H13=12.7, H16=15.9, H19=19.1, H22=22.2, H25=25.4, H29=28.6 (mm)
## 8. 코핑(두부보) 배근 공통 규칙
- 단차나 경사가 있는 코핑 상단을 지나는 주철근은 단순 직선으로 둡니다. 기준면의 가장 낮은 지점에서 피복을 재고, 그 높이에 평평하게 유지합니다.
- 일반도와 구조도는 항상 함께 대조합니다. 형상이 서로 다를 수 있습니다.
## 9. 레이어 이름 규칙 (기존 프로젝트 방식)
- `P1_두부_주철근_1CYCLE::C1_H25` 처럼 "교각_부위_종류_사이클::마크_규격"
- 철근은 블록(InstanceReference)으로 배치
## 확인 필요
- 새 프로그램에서 레이어 이름과 철근 형상 코드(I~VIII)를 어떻게 일반화할지 정해야 합니다.
+42
View File
@@ -0,0 +1,42 @@
# 다음 작업 안내 (2026-10-07 저장)
새 세션을 시작하면 이 문서와 `docs/tasks.md`, `docs/paths.md`를 먼저 읽고 이어서 하세요.
## 지금까지 (커밋 7개, 테스트 119개 통과)
| 작업 | 내용 | 파일 | 커밋 |
|---|---|---|---|
| T-001 | Python 3.12.10, pytest 설치 | (C 드라이브 설치) | - |
| T-002 | 구부림 반지름, 갈고리 길이 | `scripts/rebar_calc.py` | 65e02b6 |
| T-003 | 철근 간격·수량·중량 | `scripts/rebar_layout.py` | 5072ff3 |
| T-004 | 기존 스크립트 재사용 계획 | `docs/reuse_plan.md` | c008c0e |
| T-005 | 월드 ↔ 코핑 로컬 좌표 | `scripts/frame.py` | d55e74e |
| T-006 | 철근 중심선 꺾임점 | `scripts/centerline.py` | 6f20a6d |
| T-007 | 철근 간섭 검사 | `scripts/clash.py` | fddf8de |
## 내일 할 일
### T-008 철근재료표 DXF 작성 (문자열 생성 부분) — 예상 3시간
- 참고할 기존 코드: `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\rebar_pipeline\rhino\04_table_writer.py` (82줄)
- 참고할 기존 도구: `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\rebar_pipeline\dxf\leftalign.ps1` (글자 왼쪽 정렬), `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\rebar_pipeline\dxf\kds_note_column.ps1` (KDS 상이 비고 열)
- **시작 전 사용자에게 확인할 것**: 새 모듈은 처음부터 CP949(R12 DXF)와 왼쪽 정렬(cell-left+200, DXF 72=0/73=2)을 지원하게 만들면 되는지. (아직 답을 받지 못함)
- 단위중량은 `scripts/rebar_layout.py`의 `UNIT_WEIGHT`를 쓰고 중복해서 만들지 않을 것.
- 설계기준과 다르게 배근된 철근은 비고 3 열에 자홍색(62=6)으로 적는 규칙이 있음 (`CLAUDE.md` 구 프로젝트 §0 참고).
- 기존 방식: UTF-8로 쓴 뒤 `iconv -f UTF-8 -t CP949` 로 변환. 파이썬은 `encoding="cp949"`로 바로 쓸 수 있음.
### T-009 Rhino 연결 코드 — 예상 4시간 (먼저 결정이 필요함)
시작하려면 사용자가 정해야 하는 것:
1. **레이어 이름 규칙**을 새 프로그램에서 어떻게 일반화할지. 기존: `P1_두부_주철근_1CYCLE::C1_H25` (교각_부위_종류_사이클::마크_규격)
2. **철근 형상 코드(I~VIII)** 를 어떻게 일반화할지.
3. 새 모듈을 Rhino 8의 IronPython 2.7에서 `import` 하는 방법 (`sys.path`에 `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\scripts` 추가가 필요할 것으로 보이나 **아직 실제 Rhino에서 확인하지 않음**).
- 주의: 기존 스크립트 규칙 — `execute_rhino_code`에서는 중첩 def, lambda, 리스트 컴프리헨션, f-string 금지. 마크 하나씩 실행(소켓 타임아웃 방지).
- 새 모듈에서 가져다 쓸 것: `frame.py`(좌표), `centerline.py`(꺾임점), `rebar_calc.py`(R값), `clash.py`(간섭).
## 알려진 가정과 빈칸
- 굵은골재 최대치수(Gmax)는 25mm로 **가정**했음 (`rebar_layout.DEFAULT_GMAX`). 설계서 값 확인 필요.
- 단위중량은 H16, H19, H22, H25, H29만 등록됨. H13 등은 값을 확인한 뒤 추가해야 함.
- `centerline.py`가 구하는 길이는 중심선 길이이고, 재료표 전개길이(꺾임 보정 포함)와 다를 수 있음.
- 새 모듈의 Rhino 8 `import` 동작은 시험하지 않음.
## 명령어 (새 터미널에서)
- 테스트: `cd D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program` 후 `python -m pytest -q`
- `python`이 안 되면: `C:\Users\User\AppData\Local\Programs\Python\Python312\python.exe -m pytest -q`
- git 사용자 이름은 `yjahn` (이 폴더에만 적용, 이메일은 가짜 주소 `yjahn@localhost`)
+43
View File
@@ -0,0 +1,43 @@
# 경로 목록 (헷갈릴 때 여기를 보세요)
작업 중 새 경로가 생기면 이 표에 추가합니다.
## 새 프로그램 (지금 작업하는 곳)
| 이름 | 경로 | 설명 |
|---|---|---|
| 프로젝트 폴더 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program` | 새 교각 철근 프로그램 (git 저장소) |
| 작업 규칙 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\CLAUDE.md` | 언어, 문서, 개발, Git 규칙 |
| 설계 규칙 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\docs\design_rules.md` | KDS 기준, R값, 피복 등 |
| 할 일 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\docs\tasks.md` | 작업 번호 T-001~ |
| 다음 작업 안내 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\docs\next_steps.md` | 내일 이어서 할 일(T-008, T-009)과 확인할 점 |
| 재사용 계획 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\docs\reuse_plan.md` | 기존 스크립트 8개 선별 결과 (T-004) |
| 사용법 (메모장) | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\사용법.txt` | 실행 방법 안내. 더블클릭하면 메모장으로 열림 |
| 재사용 코드 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\scripts\` | 철근 .py 파일 |
| 테스트 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\tests\` | 계산 모듈 테스트 |
| 계산 모듈 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\scripts\rebar_calc.py` | 구부림 반지름, 갈고리 길이 (T-002) |
| 계산 모듈 테스트 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\tests\test_rebar_calc.py` | 25개 테스트 |
| 배치 계산 모듈 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\scripts\rebar_layout.py` | 중심에서 바깥 배치, 피복 맞춤, 간격 검사, 수량·중량 (T-003) |
| 좌표 변환 모듈 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\scripts\frame.py` | 월드 좌표 ↔ 코핑 로컬 좌표 (s, t, z). 원점과 각도를 값으로 받음 (T-005) |
| 좌표 변환 테스트 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\tests\test_frame.py` | 23개 테스트 |
| 중심선 모듈 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\scripts\centerline.py` | 단면 오프셋 교점으로 철근 중심선 꺾임점 계산 (T-006) |
| 중심선 테스트 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\tests\test_centerline.py` | 22개 테스트 |
| 간섭 검사 모듈 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\scripts\clash.py` | 철근 중심선 선분 최단거리, 간섭 판정 (T-007) |
| 간섭 검사 테스트 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\tests\test_clash.py` | 23개 테스트 |
| 배치 계산 테스트 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\tests\test_rebar_layout.py` | 26개 테스트 |
| 테스트 설정 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\tests\conftest.py` | scripts 폴더를 불러오는 경로 설정 |
## 기존 프로젝트 (참고용, 수정하지 않음)
| 이름 | 경로 | 설명 |
|---|---|---|
| 기존 프로젝트 폴더 | `D:\Rhino-MCP\rhino-mcp-main` | 대호지교 P1 작업과 Rhino MCP 서버 |
| 기존 철근 스크립트 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\rebar_pipeline\rhino\` | 01~08 번호 스크립트 |
| 기존 README | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\rebar_pipeline\README.md` | 방법 설명 |
| KDS 추출 텍스트 | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\rebar_pipeline\kds\KDS_24_14_21.txt` | 조항 번호로 검색 |
| KDS 원본 | `D:\★Pier 자료\KDS 24 14 21 콘크리트교 설계기준(한계상태설계법).hwp` | 설계기준 원문 |
## 도구
| 이름 | 경로 | 설명 |
|---|---|---|
| Python 3.12 | `C:\Users\User\AppData\Local\Programs\Python\Python312\python.exe` | 새 터미널에서는 `python`으로도 실행될 수 있음 |
| 가상환경 (필요할 때만) | `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\.venv` | 큰 패키지가 필요해지면 여기에 설치 (아직 없음) |
| pytest | `python -m pytest` | 테스트 실행 (pier-program 폴더에서) |
+37
View File
@@ -0,0 +1,37 @@
# 기존 스크립트 재사용 계획 (T-004)
대상: `D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\rebar_pipeline\rhino\` 의 스크립트 8개. 원본은 수정하지 않습니다.
## 먼저 알아둘 점
- 8개 모두 **Rhino 안에서만 실행**되고, 계산과 Rhino 호출이 섞여 있습니다. 그대로는 테스트를 만들 수 없습니다.
- 대호지교 P1 전용 숫자(원점 `480714926 / 153833329.5`, 각도 `atan2(4255, 12284)`, 마크 이름, 객체 ID)가 코드 안에 박혀 있습니다.
- 기존 스크립트는 IronPython 2.7용이라 f-string, lambda, 리스트 컴프리헨션을 못 씁니다. 새 모듈(`rebar_calc.py`, `rebar_layout.py`)은 이 제한을 지켜서 썼지만, 실제 Rhino에서 불러 쓰는 것은 아직 확인하지 않았습니다.
- 그래서 방법은 따라가되, **계산 부분만 뽑아 테스트 가능한 모듈로 새로 만드는 방식**을 권합니다.
## 선별표
| 스크립트 | 하는 일 | 판단 | 이유 |
|---|---|---|---|
| 01_section_profile.py | 코핑의 길이방향 각도와 단면 외곽 (s, z) 추출 | **재사용 (우선순위 높음)** | 모든 교각에서 가장 먼저 필요합니다. 각도 히스토그램과 좌표 변환은 순수 계산으로 분리 가능합니다. |
| 02_build_shape_VII.py | 단면을 안쪽 120 오프셋한 직선의 교점으로 철근 중심선을 만들고 필렛과 파이프로 생성 | **재사용 (핵심)** | 오프셋 교점은 순수 2D 계산이라 테스트 가능합니다. 필렛과 파이프 생성은 Rhino 쪽에 둡니다. 숫자가 형식 VII에 고정돼 있어 일반화가 필요합니다. |
| 03_audit.py | 생성된 철근을 다시 중심선으로 복원해 치수, 각도, 피복 검사 | 재사용 (보통) | 검증용으로 유용합니다. 08과 중심선 복원 부분이 겹칩니다. |
| 04_table_writer.py | 철근재료표 DXF 작성 | **재사용 (우선순위 높음)** | DXF 문자열 만들기는 순수 계산이라 테스트 가능합니다. 단위중량이 `rebar_layout.UNIT_WEIGHT`와 중복되므로 한곳으로 합칩니다. |
| 05_realign_base_pieces.py | ⊔ 바닥 조각을 ⊓ 뚜껑 평면에 맞춰 재정렬 | **재사용 안 함 (참고만)** | P1에서 생긴 어긋남을 고친 일회성 보정 코드입니다. 새 프로그램이 처음부터 맞게 만들면 필요 없습니다. |
| 06_flip_hooks.py | 갈고리 방향이 반대인 철근을 거울 반전 | **재사용 안 함 (참고만)** | 도면과 달랐던 부분을 고친 일회성 코드입니다. 대신 갈고리 방향을 입력값으로 받게 설계합니다. |
| 07_ctc_extract.py | 철근 중심을 코핑 로컬 좌표로 뽑아 도면 `@` 간격과 비교 | 재사용 (보통) | 검증에 유용합니다. 좌표 변환은 01과 공통 모듈로 합칩니다. |
| 08_clash_check.py | 철근 중심선끼리 간섭 검사 | **재사용 (우선순위 높음)** | 선분 사이 최단거리 계산은 순수 수학이라 테스트가 쉽고, 어느 프로젝트에나 필요한 품질 검사입니다. |
## 공통 문제: 좌표 변환이 5곳에 복붙되어 있음
02, 05, 06, 07, 08 모두 `OX, OY, ANG`로 월드 좌표와 코핑 로컬 좌표 (s, t, z)를 바꾸는 코드를 각자 갖고 있습니다.
**가장 먼저 이 변환을 하나의 모듈(`frame.py`)로 만들고 테스트하는 것**을 권합니다. 이후 모든 스크립트가 이 모듈을 씁니다.
## 권장 순서 (tasks.md에 T-005~T-009로 추가)
1. 코핑 좌표 변환 `frame.py` (월드 ↔ 로컬)
2. 단면 오프셋 교점으로 중심선 꺾임점 계산 (02의 순수 계산 부분)
3. 선분 최단거리와 간섭 판정 (08의 순수 계산 부분)
4. 재료표 DXF 작성 (04)
5. Rhino 연결 코드 (필렛, 파이프, 블록, 레이어): 위 모듈을 불러서 실행
## 확인이 필요한 결정
- 레이어 이름 규칙과 형식 코드(I~VIII)를 새 프로그램에서 어떻게 일반화할지 (`design_rules.md`의 "확인 필요"와 같은 항목)
- 새 모듈을 Rhino 8의 IronPython 2.7에서 `import` 할 때 경로를 어떻게 지정할지 (실제 Rhino에서 확인 필요)
+13
View File
@@ -0,0 +1,13 @@
# 할 일
| 번호 | 내용 | 예상 시간 | 상태 |
|---|---|---|---|
| T-001 | Python 설치와 pytest 실행 확인 | 0.5시간 | 완료 (Python 3.12.10, pytest 9.1.1) |
| T-002 | 계산 모듈 뼈대 `scripts/rebar_calc.py` (구부림 반지름, 갈고리 연장 길이) + 테스트 | 1시간 | 완료 (테스트 25개 통과) |
| T-003 | 철근 간격·수량 계산 (중심에서 바깥으로 배치, 피복 맞춤) + 테스트 | 2시간 | 완료 (테스트 51개 통과, `scripts/rebar_layout.py`) |
| T-004 | 기존 `rebar_pipeline/rhino/` 스크립트 중 재사용할 것 선별 | 1시간 | 완료 (`docs/reuse_plan.md`: 재사용 4개, 보통 2개, 참고만 2개) |
| T-005 | 코핑 좌표 변환 `scripts/frame.py` (월드 ↔ 로컬 s, t, z) + 테스트 | 1.5시간 | 완료 (테스트 74개 통과, `scripts/frame.py`) |
| T-006 | 단면 오프셋 교점으로 철근 중심선 꺾임점 계산 + 테스트 | 3시간 | 완료 (테스트 96개 통과, `scripts/centerline.py`, 형식 VII 도면 b=4167.1, c=3158.8 재현) |
| T-007 | 선분 최단거리와 간섭 판정 + 테스트 | 2시간 | 완료 (테스트 119개 통과, `scripts/clash.py`, 무작위 검증 포함) |
| T-008 | 철근재료표 DXF 작성 (문자열 생성 부분) + 테스트 | 3시간 | 대기 |
| T-009 | Rhino 연결 코드 (필렛, 파이프, 블록, 레이어) 와 import 경로 확인 | 4시간 | 대기 (레이어·형식 코드 일반화 결정 필요) |
+123
View File
@@ -0,0 +1,123 @@
"""단면 외곽선에서 철근 중심선 꺾임점을 계산하는 모듈.
Rhino 없이 실행됩니다. 단위는 mm, 각도는 도(degree)입니다.
2D 좌표는 (s, z): s = 길이방향, z = 높이 (frame.py 의 로컬 좌표와 같은 방향).
방법 (기존 프로젝트 rebar_pipeline/README.md §3):
1. 단면 외곽의 각 면을 안쪽으로 피복만큼 옮긴 직선으로 바꿉니다.
2. 이웃한 두 직선의 교점이 철근 중심선의 꺾임점입니다.
3. 시작과 끝은 도면의 고정 길이만큼 연장합니다.
"""
import math
EPS = 1e-9
def _unit_direction(p0, p1):
dx = p1[0] - p0[0]
dz = p1[1] - p0[1]
length = math.hypot(dx, dz)
if length < EPS:
raise ValueError("길이가 0인 선분입니다: %r" % (p0,))
return (dx / length, dz / length)
def _normal(direction, side):
"""진행 방향의 왼쪽 또는 오른쪽 단위 법선."""
dx, dz = direction
if side == "left":
return (-dz, dx)
if side == "right":
return (dz, -dx)
raise ValueError("side 는 'left' 또는 'right' 여야 합니다: %r" % (side,))
def offset_line(p0, p1, distance, side):
"""선분 p0->p1 을 진행 방향의 side(left/right)로 distance 만큼 옮긴 직선.
돌려주는 값: (직선 위의 한 점, 진행 방향 단위벡터)
"""
direction = _unit_direction(p0, p1)
n = _normal(direction, side)
point = (p0[0] + n[0] * distance, p0[1] + n[1] * distance)
return point, direction
def line_intersection(line_a, line_b):
"""두 직선 (점, 방향) 의 교점. 평행하면 ValueError."""
(ax, az), (adx, adz) = line_a
(bx, bz), (bdx, bdz) = line_b
cross = adx * bdz - adz * bdx
if abs(cross) < EPS:
raise ValueError("두 직선이 평행해서 교점이 없습니다")
t = ((bx - ax) * bdz - (bz - az) * bdx) / cross
return (ax + adx * t, az + adz * t)
def centerline_points(segments, cover, side, start_len=None, end_len=None):
"""철근 중심선의 꺾임점 목록 (s, z).
segments : 외곽선 선분 목록 [(시작점, 끝점), ...]. 철근이 지나가는 순서와 방향대로 적습니다.
cover : 피복 (철근 중심에서 면까지 거리)
side : 콘크리트 안쪽이 진행 방향의 'left' 인지 'right' 인지
start_len: 첫 꺾임점에서 첫 선분 반대 방향으로 연장할 길이. None 이면 첫 선분의 시작점을 그대로 씁니다.
end_len : 마지막 꺾임점에서 마지막 선분 방향으로 연장할 길이. None 이면 마지막 선분의 끝점을 그대로 씁니다.
"""
if not segments:
raise ValueError("선분이 하나 이상 필요합니다")
lines = [offset_line(p0, p1, cover, side) for (p0, p1) in segments]
if len(lines) == 1:
if start_len is not None or end_len is not None:
raise ValueError("선분이 1개이면 고정 길이를 쓸 수 없습니다")
(p0, d) = lines[0]
length = math.hypot(segments[0][1][0] - segments[0][0][0],
segments[0][1][1] - segments[0][0][1])
return [p0, (p0[0] + d[0] * length, p0[1] + d[1] * length)]
vertices = [line_intersection(lines[i], lines[i + 1]) for i in range(len(lines) - 1)]
if start_len is None:
first = lines[0][0]
else:
d0 = lines[0][1]
first = (vertices[0][0] - d0[0] * start_len, vertices[0][1] - d0[1] * start_len)
if end_len is None:
seg_end = segments[-1][1]
n = _normal(lines[-1][1], side)
last = (seg_end[0] + n[0] * cover, seg_end[1] + n[1] * cover)
else:
dn = lines[-1][1]
last = (vertices[-1][0] + dn[0] * end_len, vertices[-1][1] + dn[1] * end_len)
return [first] + vertices + [last]
def segment_lengths(points):
"""꺾임점 목록의 각 구간 길이."""
return [math.hypot(b[0] - a[0], b[1] - a[1]) for a, b in zip(points, points[1:])]
def polyline_length(points):
"""꺾임점을 이은 전체 길이."""
return sum(segment_lengths(points))
def turn_angles(points):
"""각 안쪽 꺾임점에서 진행 방향이 바뀌는 각도(도, 0~180). 직선이면 0, 직각이면 90."""
angles = []
for i in range(1, len(points) - 1):
d1 = _unit_direction(points[i - 1], points[i])
d2 = _unit_direction(points[i], points[i + 1])
dot = max(-1.0, min(1.0, d1[0] * d2[0] + d1[1] * d2[1]))
angles.append(math.degrees(math.acos(dot)))
return angles
def distance_to_line(point, line_p0, line_p1):
"""점에서 (선분이 아니라) 무한 직선 line_p0-line_p1 까지의 수직거리."""
d = _unit_direction(line_p0, line_p1)
vx = point[0] - line_p0[0]
vz = point[1] - line_p0[1]
return abs(vx * d[1] - vz * d[0])
+164
View File
@@ -0,0 +1,164 @@
"""철근 중심선 간섭 검사 모듈.
Rhino 없이 실행됩니다. 단위는 mm. 좌표는 (x, y, z) 3D 점입니다.
계산을 정확하게 하려면 월드 좌표(약 4.8억)보다 frame.py 로 바꾼 로컬 좌표를 쓰는 것이 좋습니다.
규칙 (기존 프로젝트 rebar_pipeline/rhino/08_clash_check.py):
- 철근을 중심선 선분으로 쪼개서, 두 선분의 최단거리 d 에 대해
관입 = rA + rB - d 가 min_penetration(기본 0.5mm)보다 크면 간섭입니다.
- 같은 철근, 같은 그룹(겹침이음 모델링인 박스) 선분끼리는 제외합니다.
- 철근 쌍마다 가장 깊은 관입 하나만 기록합니다.
"""
import math
EPS = 1e-12
BBOX_MARGIN = 1.0 # 후보를 찾을 때 반지름에 더하는 여유(mm)
class Seg(object):
"""철근 중심선의 선분 하나.
p, q : 끝점 (x, y, z)
radius : 철근 반지름
bar : 철근 번호 (같은 철근끼리는 검사 제외)
group : 그룹 번호 (같은 박스의 뚜껑·바닥 조각처럼 검사 제외할 묶음)
mark : 철근 마크 이름 (보고용)
"""
def __init__(self, p, q, radius, bar, group, mark=""):
self.p = (float(p[0]), float(p[1]), float(p[2]))
self.q = (float(q[0]), float(q[1]), float(q[2]))
self.radius = float(radius)
self.bar = bar
self.group = group
self.mark = mark
def _sub(a, b):
return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
def _dot(a, b):
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
def _clamp01(x):
if x < 0.0:
return 0.0
if x > 1.0:
return 1.0
return x
def segment_distance(p1, q1, p2, q2):
"""두 선분 p1-q1, p2-q2 사이의 최단거리 (3D, 길이 0인 선분도 가능)."""
d1 = _sub(q1, p1)
d2 = _sub(q2, p2)
r = _sub(p1, p2)
a = _dot(d1, d1)
e = _dot(d2, d2)
f = _dot(d2, r)
if a <= EPS and e <= EPS:
s = 0.0
t = 0.0
elif a <= EPS:
s = 0.0
t = _clamp01(f / e)
else:
c = _dot(d1, r)
if e <= EPS:
t = 0.0
s = _clamp01(-c / a)
else:
b = _dot(d1, d2)
denom = a * e - b * b
if denom > 1e-12 * a * e:
s = _clamp01((b * f - c * e) / denom)
else:
s = 0.0
t = (b * s + f) / e
if t < 0.0:
t = 0.0
s = _clamp01(-c / a)
elif t > 1.0:
t = 1.0
s = _clamp01((b - c) / a)
c1 = (p1[0] + d1[0] * s, p1[1] + d1[1] * s, p1[2] + d1[2] * s)
c2 = (p2[0] + d2[0] * t, p2[1] + d2[1] * t, p2[2] + d2[2] * t)
diff = _sub(c1, c2)
return math.sqrt(_dot(diff, diff))
def _box(s):
"""선분을 반지름 + 여유만큼 부풀린 상자 (xmin, ymin, zmin, xmax, ymax, zmax)."""
m = s.radius + BBOX_MARGIN
return (min(s.p[0], s.q[0]) - m, min(s.p[1], s.q[1]) - m, min(s.p[2], s.q[2]) - m,
max(s.p[0], s.q[0]) + m, max(s.p[1], s.q[1]) + m, max(s.p[2], s.q[2]) + m)
def _boxes_overlap(a, b):
return (a[0] <= b[3] and b[0] <= a[3] and
a[1] <= b[4] and b[1] <= a[4] and
a[2] <= b[5] and b[2] <= a[5])
def find_clashes(segments, min_penetration=0.5):
"""간섭 목록을 돌려줍니다 (관입이 깊은 것부터).
각 항목: {"bar_a", "bar_b", "mark_a", "mark_b", "penetration", "point"}
bar_a < bar_b 로 정렬되고, point 는 간섭 선분 네 끝점의 평균입니다.
"""
boxes = [_box(s) for s in segments]
order = sorted(range(len(segments)), key=lambda i: boxes[i][0])
best = {}
for oi in range(len(order)):
i = order[oi]
a = segments[i]
for oj in range(oi + 1, len(order)):
j = order[oj]
if boxes[j][0] > boxes[i][3]:
break # x 방향으로 이미 떨어져 있음. 뒤쪽은 더 멀리 있음
b = segments[j]
if a.bar == b.bar or a.group == b.group:
continue
if not _boxes_overlap(boxes[i], boxes[j]):
continue
d = segment_distance(a.p, a.q, b.p, b.q)
pen = a.radius + b.radius - d
if pen <= min_penetration:
continue
first, second = (a, b) if a.bar <= b.bar else (b, a)
key = (first.bar, second.bar)
old = best.get(key)
if old is None or pen > old["penetration"]:
point = ((a.p[0] + a.q[0] + b.p[0] + b.q[0]) / 4.0,
(a.p[1] + a.q[1] + b.p[1] + b.q[1]) / 4.0,
(a.p[2] + a.q[2] + b.p[2] + b.q[2]) / 4.0)
best[key] = {
"bar_a": first.bar, "bar_b": second.bar,
"mark_a": first.mark, "mark_b": second.mark,
"penetration": pen, "point": point,
}
hits = list(best.values())
hits.sort(key=lambda h: -h["penetration"])
return hits
def summarize_by_mark(hits):
"""마크 쌍별 간섭 개수와 최대 관입. 키는 'C1|C8' 처럼 이름순으로 정렬된 마크 쌍."""
summary = {}
for h in hits:
names = sorted([h["mark_a"], h["mark_b"]])
key = names[0] + "|" + names[1]
item = summary.get(key)
if item is None:
summary[key] = {"count": 1, "max_penetration": h["penetration"]}
else:
item["count"] += 1
if h["penetration"] > item["max_penetration"]:
item["max_penetration"] = h["penetration"]
return summary
+75
View File
@@ -0,0 +1,75 @@
"""월드 좌표와 코핑 로컬 좌표를 바꾸는 모듈.
Rhino 없이 실행됩니다. 단위는 mm, 각도는 라디안입니다.
월드 좌표 (x, y, z) <-> 로컬 좌표 (s, t, z)
s = 길이방향 (코핑 축 방향), t = 폭방향 (s를 반시계로 90도 돌린 방향)
z = 월드 높이를 그대로 사용
원점(origin_x, origin_y)과 각도(angle)는 값으로 받으므로 언제든 바꿀 수 있습니다.
기존 프로젝트 rebar_pipeline/rhino/07_ctc_extract.py 의 OX, OY, ANG 방식과 같습니다.
"""
import math
class Frame(object):
"""코핑 로컬 좌표계. 원점(월드 x, y)과 s축 각도(월드 +X 기준, 반시계)로 정해집니다."""
def __init__(self, origin_x, origin_y, angle):
self.origin_x = float(origin_x)
self.origin_y = float(origin_y)
self.angle = float(angle)
self._cos = math.cos(self.angle)
self._sin = math.sin(self.angle)
def to_local(self, x, y, z):
"""월드 점 (x, y, z) -> 로컬 점 (s, t, z)."""
dx = x - self.origin_x
dy = y - self.origin_y
return (dx * self._cos + dy * self._sin,
-dx * self._sin + dy * self._cos,
z)
def to_world(self, s, t, z):
"""로컬 점 (s, t, z) -> 월드 점 (x, y, z)."""
return (self.origin_x + s * self._cos - t * self._sin,
self.origin_y + s * self._sin + t * self._cos,
z)
def direction_to_local(self, dx, dy, dz):
"""월드 방향 벡터 -> 로컬 방향 벡터 (원점 이동 없이 회전만)."""
return (dx * self._cos + dy * self._sin,
-dx * self._sin + dy * self._cos,
dz)
def direction_to_world(self, ds, dt, dz):
"""로컬 방향 벡터 -> 월드 방향 벡터 (원점 이동 없이 회전만)."""
return (ds * self._cos - dt * self._sin,
ds * self._sin + dt * self._cos,
dz)
def to_local_many(self, points):
"""월드 점 목록 -> 로컬 점 목록."""
return [self.to_local(x, y, z) for (x, y, z) in points]
def to_world_many(self, points):
"""로컬 점 목록 -> 월드 점 목록."""
return [self.to_world(s, t, z) for (s, t, z) in points]
def from_two_points(a, b):
"""두 점 a, b (월드 (x, y))로 프레임을 만듭니다.
원점 = 두 점의 가운데, s축 = a에서 b로 향하는 방향.
(예: 2주식 교각의 두 기둥 중심)
"""
dx = b[0] - a[0]
dy = b[1] - a[1]
if dx == 0 and dy == 0:
raise ValueError("두 점이 같아서 방향을 정할 수 없습니다: %r" % (a,))
return Frame((a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0, math.atan2(dy, dx))
def convert(point, src, dst):
"""src 프레임의 로컬 점을 dst 프레임의 로컬 점으로 바꿉니다 (월드를 거쳐서)."""
return dst.to_local(*src.to_world(*point))
+90
View File
@@ -0,0 +1,90 @@
"""철근 구부림 반지름과 갈고리 길이 계산 모듈.
Rhino 없이 실행됩니다. 단위는 모두 mm입니다.
기준값 출처: docs/design_rules.md
"""
# KS D 3504 공칭 철근 직경 (mm)
NOMINAL_DIAMETER = {
10: 9.5,
13: 12.7,
16: 15.9,
19: 19.1,
22: 22.2,
25: 25.4,
29: 28.6,
32: 31.8,
35: 34.9,
38: 38.1,
}
# 갈고리 뒤 연장 길이 (공칭 직경의 배수). 프로젝트 적용값
HOOK_EXTENSION_FACTOR = {90: 12.0, 135: 6.0, 180: 4.0}
HOOK_180_MIN_EXTENSION = 60.0 # 180도 갈고리는 4db 또는 60mm 중 큰 값
# KDS 24 14 21: 90도~150도 표준갈고리 연장 길이 최소값 (db의 배수)
KDS_HOOK_MIN_FACTOR = 5.0
def bar_number(name):
"""'H25', 'D25', 'h25' 같은 이름에서 호칭 번호(25)를 꺼냅니다."""
text = str(name).strip().upper()
if len(text) < 2 or text[0] not in ("H", "D") or not text[1:].isdigit():
raise ValueError("알 수 없는 철근 이름입니다: %r" % (name,))
return int(text[1:])
def nominal_diameter(name):
"""철근 이름(예: 'H25')의 공칭 직경을 mm로 돌려줍니다."""
number = bar_number(name)
if number not in NOMINAL_DIAMETER:
raise ValueError("지원하지 않는 철근 규격입니다: %r" % (name,))
return NOMINAL_DIAMETER[number]
def bend_radius(name, is_stirrup=False):
"""프로젝트 적용 구부림 최소 내면 반지름(mm).
D10~D25: 3db, D29~D35: 4db, D38 이상: 5db.
스터럽/띠철근이고 D16 이하이면 2db.
"""
number = bar_number(name)
db = nominal_diameter(name)
if is_stirrup and number <= 16:
return 2.0 * db
if number <= 25:
return 3.0 * db
if number <= 35:
return 4.0 * db
return 5.0 * db
def kds_min_inner_diameter(name):
"""KDS 24 14 21 표 4.5-1 최소 내면 지름(mm). D16 이하 2db, D19 이상 3.5db."""
number = bar_number(name)
db = nominal_diameter(name)
if number <= 16:
return 2.0 * db
return 3.5 * db
def satisfies_kds_bend(name, radius):
"""내면 반지름(mm)이 KDS 최소 내면 지름을 만족하는지 확인합니다."""
return 2.0 * radius >= kds_min_inner_diameter(name)
def hook_extension(name, angle):
"""프로젝트 적용 갈고리 뒤 연장 길이(mm). angle은 90, 135, 180 중 하나."""
if angle not in HOOK_EXTENSION_FACTOR:
raise ValueError("지원하지 않는 갈고리 각도입니다: %r" % (angle,))
length = HOOK_EXTENSION_FACTOR[angle] * nominal_diameter(name)
if angle == 180:
length = max(length, HOOK_180_MIN_EXTENSION)
return length
def kds_min_hook_extension(name, angle):
"""KDS 24 14 21 갈고리 연장 길이 최소값(mm). 90~150도는 5db."""
if not 90 <= angle <= 150:
raise ValueError("KDS 5db 규정은 90~150도에만 적용됩니다: %r" % (angle,))
return KDS_HOOK_MIN_FACTOR * nominal_diameter(name)
+105
View File
@@ -0,0 +1,105 @@
"""철근 배치(간격, 수량, 중량) 계산 모듈.
Rhino 없이 실행됩니다. 단위는 mm이고, 중량만 kg입니다.
기준값 출처: docs/design_rules.md, 기존 프로젝트 rebar_pipeline/README.md
"""
import rebar_calc
# 단위중량 kg/m (기존 프로젝트 재료표 값). 아래에 없는 규격은 값을 확인한 뒤 추가하세요.
UNIT_WEIGHT = {
"H16": 1.560,
"H19": 2.250,
"H22": 3.040,
"H25": 3.980,
"H29": 5.040,
}
DEFAULT_GMAX = 25.0 # 굵은골재 최대치수(mm). 가정값이므로 설계서의 값으로 바꿔 쓰세요.
EPS = 1e-9
def center_out_positions(count, spacing):
"""중심(0)에서 바깥으로 spacing 간격으로 놓은 폭방향 위치 목록(작은 값부터).
짝수 개이면 중심에 철근이 없고(±spacing/2부터), 홀수 개이면 중심(0)에 철근이 있습니다.
"""
if count < 0:
raise ValueError("철근 수는 0 이상이어야 합니다: %r" % (count,))
if spacing <= 0:
raise ValueError("간격은 0보다 커야 합니다: %r" % (spacing,))
mid = (count - 1) / 2.0
return [spacing * (i - mid) for i in range(count)]
def snapped_array(count, spacing, width, cover):
"""바깥 철근 2본은 피복에 맞추고, 나머지는 중심에서 바깥으로 배치합니다 (1CYCLE 방식).
바깥 철근 위치 = ±(width/2 - cover). 안쪽 철근은 중심에서 바깥으로 spacing 간격입니다.
안쪽 철근이 바깥 철근 위치를 넘어가면 ValueError.
"""
if count < 2:
raise ValueError("바깥 철근 2본이 필요하므로 count는 2 이상이어야 합니다: %r" % (count,))
outer = width / 2.0 - cover
inner = center_out_positions(count - 2, spacing)
if inner and max(inner) >= outer:
raise ValueError("폭 %r 안에 철근 %r본을 간격 %r로 놓을 수 없습니다" % (width, count, spacing))
return [-outer] + inner + [outer]
def min_horizontal_clear(name, gmax=DEFAULT_GMAX):
"""수평 최소 순간격 = max(1.5db, 1.5Gmax, 40mm) (KDS 24 14 21 4.5.3)."""
db = rebar_calc.nominal_diameter(name)
return max(1.5 * db, 1.5 * gmax, 40.0)
def clear_spacing(center_spacing, name):
"""중심 간격에서 철근 직경을 뺀 순간격."""
return center_spacing - rebar_calc.nominal_diameter(name)
def horizontal_spacing_ok(positions, name, gmax=DEFAULT_GMAX):
"""이웃한 철근 사이 순간격이 모두 수평 최소 순간격 이상이면 True."""
ordered = sorted(positions)
need = min_horizontal_clear(name, gmax)
for a, b in zip(ordered, ordered[1:]):
if clear_spacing(b - a, name) < need - EPS:
return False
return True
def min_vertical_clear(name):
"""다단 배근의 연직 최소 순간격 = max(25mm, db)."""
return max(25.0, rebar_calc.nominal_diameter(name))
def vertical_pitch_ok(name, pitch):
"""단 사이 중심 간격(pitch)의 순간격이 연직 최소 순간격 이상이면 True."""
return clear_spacing(pitch, name) >= min_vertical_clear(name) - EPS
def tier_elevations(top_z, cover, tiers, pitch):
"""맨 위 단은 상면에서 cover만큼 아래, 다음 단부터 pitch씩 아래로 내린 높이 목록."""
return [top_z - cover - i * pitch for i in range(tiers)]
def unit_weight(name):
"""단위중량(kg/m)."""
key = "H%d" % rebar_calc.bar_number(name)
if key not in UNIT_WEIGHT:
raise ValueError("단위중량이 등록되지 않은 규격입니다: %r" % (name,))
return UNIT_WEIGHT[key]
def bar_weight(name, length_mm, count=1):
"""철근 length_mm 길이 count본의 중량(kg)."""
return unit_weight(name) * length_mm / 1000.0 * count
def array_summary(name, length_mm, positions):
"""한 배열의 본수, 총길이(m), 중량(kg)."""
count = len(positions)
return {
"count": count,
"total_length_m": length_mm / 1000.0 * count,
"weight_kg": bar_weight(name, length_mm, count),
}
+5
View File
@@ -0,0 +1,5 @@
"""테스트에서 scripts/ 폴더의 모듈을 불러올 수 있게 경로를 추가합니다."""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
+177
View File
@@ -0,0 +1,177 @@
"""centerline 테스트 (Rhino 없이 실행됩니다).
단면 외곽선을 안쪽으로 피복만큼 옮긴 직선들의 교점으로 철근 중심선 꺾임점을 구합니다.
2D 좌표는 (s, z): s = 길이방향, z = 높이.
기준: 기존 프로젝트 rebar_pipeline/rhino/02_build_shape_VII.py (형식 VII, 도면 b=4167.1, c=3158.8)
"""
import math
import pytest
import centerline as cl
# ---- 직선 오프셋 ----
def test_offset_line_right_side_of_rightward_segment_is_below():
# +s 방향으로 가는 선분의 오른쪽은 아래(-z)
point, direction = cl.offset_line((0.0, 0.0), (100.0, 0.0), 10.0, "right")
assert point == pytest.approx((0.0, -10.0))
assert direction == pytest.approx((1.0, 0.0))
def test_offset_line_left_side_of_rightward_segment_is_above():
point, direction = cl.offset_line((0.0, 0.0), (100.0, 0.0), 10.0, "left")
assert point == pytest.approx((0.0, 10.0))
def test_offset_line_downward_segment_right_side_is_minus_s():
# 아래로 가는 선분의 오른쪽은 -s 쪽 (끝면 s=12100 에서 안쪽)
point, _ = cl.offset_line((12100.0, 10738.0), (12100.0, 9238.0), 120.0, "right")
assert point[0] == pytest.approx(11980.0)
def test_offset_line_direction_is_unit_length():
_, d = cl.offset_line((0.0, 0.0), (30.0, 40.0), 5.0, "left")
assert math.hypot(*d) == pytest.approx(1.0)
def test_offset_line_rejects_zero_length_segment():
with pytest.raises(ValueError):
cl.offset_line((1.0, 1.0), (1.0, 1.0), 10.0, "left")
def test_offset_line_rejects_bad_side():
with pytest.raises(ValueError):
cl.offset_line((0.0, 0.0), (1.0, 0.0), 10.0, "up")
# ---- 직선 교점 ----
def test_line_intersection_perpendicular_lines():
a = ((0.0, 5.0), (1.0, 0.0))
b = ((7.0, 0.0), (0.0, 1.0))
assert cl.line_intersection(a, b) == pytest.approx((7.0, 5.0))
def test_line_intersection_diagonal():
a = ((0.0, 0.0), (1.0, 1.0))
b = ((10.0, 0.0), (-1.0, 1.0))
assert cl.line_intersection(a, b) == pytest.approx((5.0, 5.0))
def test_line_intersection_parallel_raises():
a = ((0.0, 0.0), (1.0, 0.0))
b = ((0.0, 5.0), (1.0, 0.0))
with pytest.raises(ValueError):
cl.line_intersection(a, b)
# ---- 중심선 꺾임점: 단순한 경우 ----
def test_centerline_of_u_shape_inside_a_box():
# 바깥 외곽: 위에서 왼쪽 벽을 따라 내려가 바닥을 지나 오른쪽 벽을 올라가는 ㄷ자 경로
segs = [
((0.0, 100.0), (0.0, 0.0)), # 왼쪽 벽, 아래로
((0.0, 0.0), (200.0, 0.0)), # 바닥, 오른쪽으로
((200.0, 0.0), (200.0, 100.0)), # 오른쪽 벽, 위로
]
pts = cl.centerline_points(segs, 10.0, "left")
# 아래로 가는 선분의 왼쪽은 +s, 오른쪽으로 가는 선분의 왼쪽은 +z (안쪽)
assert pts[0] == pytest.approx((10.0, 100.0))
assert pts[1] == pytest.approx((10.0, 10.0))
assert pts[2] == pytest.approx((190.0, 10.0))
assert pts[3] == pytest.approx((190.0, 100.0))
def test_centerline_fixed_start_and_end_lengths():
segs = [
((0.0, 100.0), (0.0, 0.0)),
((0.0, 0.0), (200.0, 0.0)),
((200.0, 0.0), (200.0, 100.0)),
]
pts = cl.centerline_points(segs, 10.0, "left", start_len=30.0, end_len=40.0)
assert pts[0] == pytest.approx((10.0, 40.0)) # 첫 꺾임점(10,10)에서 위로 30
assert pts[-1] == pytest.approx((190.0, 50.0)) # 마지막 꺾임점(190,10)에서 위로 40
assert len(pts) == 4
def test_centerline_single_segment_is_just_offset():
pts = cl.centerline_points([((0.0, 0.0), (100.0, 0.0))], 10.0, "left")
assert pts == [pytest.approx((0.0, 10.0)), pytest.approx((100.0, 10.0))]
def test_centerline_single_segment_rejects_fixed_lengths():
with pytest.raises(ValueError):
cl.centerline_points([((0.0, 0.0), (100.0, 0.0))], 10.0, "left", start_len=5.0)
def test_centerline_parallel_neighbors_raise():
segs = [((0.0, 0.0), (100.0, 0.0)), ((100.0, 0.0), (200.0, 0.0))]
with pytest.raises(ValueError):
cl.centerline_points(segs, 10.0, "left")
def test_centerline_rejects_empty():
with pytest.raises(ValueError):
cl.centerline_points([], 10.0, "left")
# ---- 길이와 꺾임각 ----
def test_segment_lengths_and_total():
pts = [(0.0, 0.0), (3.0, 4.0), (3.0, 10.0)]
assert cl.segment_lengths(pts) == pytest.approx([5.0, 6.0])
assert cl.polyline_length(pts) == pytest.approx(11.0)
def test_turn_angles_right_angle():
pts = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]
assert cl.turn_angles(pts) == pytest.approx([90.0])
def test_turn_angles_straight_is_zero():
pts = [(0.0, 0.0), (10.0, 0.0), (20.0, 0.0)]
assert cl.turn_angles(pts) == pytest.approx([0.0])
# ---- 형식 VII (대호지교 P1 C8/C10) 도면 값 재현 ----
P1_VII_SEGMENTS = [
((12100.0, 10738.0), (12100.0, 9238.0)), # 끝면, 아래로
((12100.0, 9238.0), (8100.0, 7738.0)), # 끝헌치
((8100.0, 7738.0), (4900.0, 7738.0)), # 바닥
((4900.0, 7738.0), (3400.0, 8238.0)), # 내측헌치
]
def test_shape_vii_matches_drawing_b_and_c():
pts = cl.centerline_points(P1_VII_SEGMENTS, 120.0, "right", start_len=630.0, end_len=2331.0)
lengths = cl.segment_lengths(pts)
assert len(pts) == 5
assert lengths[0] == pytest.approx(630.0) # a
assert lengths[1] == pytest.approx(4167.1, abs=0.1) # b (도면)
assert lengths[2] == pytest.approx(3158.8, abs=0.1) # c (도면)
assert lengths[3] == pytest.approx(2331.0) # d
def test_shape_vii_key_points():
pts = cl.centerline_points(P1_VII_SEGMENTS, 120.0, "right", start_len=630.0, end_len=2331.0)
assert pts[0] == pytest.approx((11980.0, 9951.16), abs=0.01) # A
assert pts[1] == pytest.approx((11980.0, 9321.16), abs=0.01) # 끝면-끝헌치 교점
assert pts[2][1] == pytest.approx(7858.0) # 바닥 위 120 (7738+120)
assert pts[3][1] == pytest.approx(7858.0)
assert pts[3][0] == pytest.approx(4919.47, abs=0.01)
def test_shape_vii_cover_is_120_from_every_face():
# 중심선 위의 각 꺾임점은 이웃한 두 면에서 모두 120 떨어져 있어야 합니다.
pts = cl.centerline_points(P1_VII_SEGMENTS, 120.0, "right", start_len=630.0, end_len=2331.0)
for i, (p0, p1) in enumerate(P1_VII_SEGMENTS[:-1]):
vertex = pts[i + 1]
for seg in (P1_VII_SEGMENTS[i], P1_VII_SEGMENTS[i + 1]):
assert cl.distance_to_line(vertex, seg[0], seg[1]) == pytest.approx(120.0, abs=1e-6)
def test_distance_to_line_basic():
assert cl.distance_to_line((5.0, 7.0), (0.0, 0.0), (10.0, 0.0)) == pytest.approx(7.0)
+197
View File
@@ -0,0 +1,197 @@
"""clash 테스트 (Rhino 없이 실행됩니다).
철근 중심선(선분)끼리의 간섭 검사.
기준: 기존 프로젝트 rebar_pipeline/rhino/08_clash_check.py
- 간섭 = rA + rB - (두 선분 최단거리) > 0.5mm
- 같은 철근, 같은 박스(겹침이음 모델링) 선분끼리는 제외
- 철근 쌍마다 가장 깊은 관입 하나만 기록
"""
import time
import pytest
import clash as ck
def seg(p, q, radius=12.7, bar=0, group=0, mark="C1"):
return ck.Seg(p, q, radius, bar, group, mark)
# ---- 선분 사이 최단거리 (3D) ----
def test_distance_crossing_skew_segments():
# x축 선분과, z=3 높이에서 y 방향으로 가로지르는 선분 -> 3
d = ck.segment_distance((-5, 0, 0), (5, 0, 0), (0, -5, 3), (0, 5, 3))
assert d == pytest.approx(3.0)
def test_distance_parallel_segments():
d = ck.segment_distance((0, 0, 0), (10, 0, 0), (0, 4, 0), (10, 4, 0))
assert d == pytest.approx(4.0)
def test_distance_intersecting_segments_is_zero():
d = ck.segment_distance((-5, 0, 0), (5, 0, 0), (0, -5, 0), (0, 5, 0))
assert d == pytest.approx(0.0)
def test_distance_collinear_with_gap():
d = ck.segment_distance((0, 0, 0), (1, 0, 0), (3, 0, 0), (4, 0, 0))
assert d == pytest.approx(2.0)
def test_distance_closest_at_endpoint():
d = ck.segment_distance((0, 0, 0), (10, 0, 0), (12, 5, 0), (12, 10, 0))
assert d == pytest.approx((2 ** 2 + 5 ** 2) ** 0.5)
def test_distance_is_symmetric():
a = ((0, 0, 0), (10, 3, 1))
b = ((4, 8, -2), (9, -6, 5))
assert ck.segment_distance(*(a + b)) == pytest.approx(ck.segment_distance(*(b + a)))
def test_distance_point_to_segment():
# 길이 0 선분(점)과 선분
d = ck.segment_distance((5, 3, 0), (5, 3, 0), (0, 0, 0), (10, 0, 0))
assert d == pytest.approx(3.0)
def test_distance_two_points():
d = ck.segment_distance((0, 0, 0), (0, 0, 0), (3, 4, 0), (3, 4, 0))
assert d == pytest.approx(5.0)
def test_distance_matches_brute_force_on_random_segments():
# 무작위 선분 200쌍을 "촘촘히 점을 찍어 가장 가까운 쌍을 찾는 느린 방법"과 비교
import random
rng = random.Random(12345)
def pt():
return (rng.uniform(-50, 50), rng.uniform(-50, 50), rng.uniform(-50, 50))
n = 60
for _ in range(200):
p1, q1, p2, q2 = pt(), pt(), pt(), pt()
best = None
for i in range(n + 1):
u = i / float(n)
a = tuple(p1[k] + (q1[k] - p1[k]) * u for k in range(3))
for j in range(n + 1):
v = j / float(n)
b = tuple(p2[k] + (q2[k] - p2[k]) * v for k in range(3))
dist = sum((a[k] - b[k]) ** 2 for k in range(3)) ** 0.5
if best is None or dist < best:
best = dist
fast = ck.segment_distance(p1, q1, p2, q2)
# 정확한 값은 점 찍기 값보다 작거나 같고, 격자 간격만큼만 차이가 남
assert fast <= best + 1e-9
assert best - fast < 3.0
# ---- 간섭 판정 ----
def crossing_pair(gap, **kw):
"""x축 선분과, z = gap 높이에서 y 방향으로 가로지르는 선분."""
a = seg((-100, 0, 0), (100, 0, 0), bar=1, group=1, **kw)
b = seg((0, -100, gap), (0, 100, gap), bar=2, group=2, **kw)
return [a, b]
def test_clash_found_when_bars_overlap():
hits = ck.find_clashes(crossing_pair(20.0)) # 25.4 - 20 = 5.4 관입
assert len(hits) == 1
assert hits[0]["penetration"] == pytest.approx(5.4)
def test_no_clash_when_touching():
assert ck.find_clashes(crossing_pair(25.4)) == []
def test_no_clash_below_min_penetration():
# 관입 0.4mm 는 기준 0.5mm 이하라서 간섭이 아님
assert ck.find_clashes(crossing_pair(25.0)) == []
def test_clash_when_just_above_min_penetration():
assert len(ck.find_clashes(crossing_pair(24.8))) == 1 # 관입 0.6
def test_min_penetration_can_be_changed():
assert len(ck.find_clashes(crossing_pair(25.0), min_penetration=0.1)) == 1
def test_different_radii_are_added():
a = seg((-100, 0, 0), (100, 0, 0), radius=14.3, bar=1, group=1)
b = seg((0, -100, 20), (0, 100, 20), radius=9.55, bar=2, group=2)
hits = ck.find_clashes([a, b])
assert hits[0]["penetration"] == pytest.approx(14.3 + 9.55 - 20.0)
def test_same_bar_is_skipped():
a = seg((-100, 0, 0), (100, 0, 0), bar=1, group=1)
b = seg((0, -100, 10), (0, 100, 10), bar=1, group=2)
assert ck.find_clashes([a, b]) == []
def test_same_group_is_skipped():
# 같은 박스 안의 뚜껑과 바닥 조각은 겹침이음이라 간섭으로 보지 않음
a = seg((-100, 0, 0), (100, 0, 0), bar=1, group=7)
b = seg((0, -100, 10), (0, 100, 10), bar=2, group=7)
assert ck.find_clashes([a, b]) == []
def test_far_apart_bars_are_not_checked():
a = seg((0, 0, 0), (100, 0, 0), bar=1, group=1)
b = seg((0, 0, 5000), (100, 0, 5000), bar=2, group=2)
assert ck.find_clashes([a, b]) == []
def test_only_deepest_penetration_per_bar_pair_is_kept():
# 같은 두 철근이 두 곳에서 겹침 (관입 5.4 와 10.4)
a1 = seg((-100, 0, 0), (0, 0, 0), bar=1, group=1)
a2 = seg((0, 0, 0), (100, 0, 0), bar=1, group=1)
b1 = seg((-50, -100, 20), (-50, 100, 20), bar=2, group=2) # a1 과 간격 20 -> 관입 5.4
b2 = seg((50, -100, 15), (50, 100, 15), bar=2, group=2) # a2 와 간격 15 -> 관입 10.4
hits = ck.find_clashes([a1, a2, b1, b2])
assert len(hits) == 1
assert hits[0]["penetration"] == pytest.approx(10.4)
def test_bar_pair_reported_once_regardless_of_order():
a = seg((-100, 0, 0), (100, 0, 0), bar=5, group=1)
b = seg((0, -100, 10), (0, 100, 10), bar=2, group=2)
hits = ck.find_clashes([a, b])
assert len(hits) == 1
assert (hits[0]["bar_a"], hits[0]["bar_b"]) == (2, 5)
def test_hit_has_marks_and_location():
a = seg((-100, 0, 0), (100, 0, 0), bar=1, group=1, mark="C1")
b = seg((0, -100, 20), (0, 100, 20), bar=2, group=2, mark="C8")
h = ck.find_clashes([a, b])[0]
assert (h["mark_a"], h["mark_b"]) == ("C1", "C8")
# 위치 = 네 끝점의 평균
assert h["point"] == pytest.approx((0.0, 0.0, 10.0))
def test_parallel_bars_at_250_have_no_clash_and_run_fast():
segs = []
for i in range(300):
segs.append(seg((i * 250.0, 0, 0), (i * 250.0, 5000, 0), bar=i, group=i))
start = time.time()
assert ck.find_clashes(segs) == []
assert time.time() - start < 2.0
# ---- 마크 쌍별 요약 ----
def test_summarize_by_mark_pair():
hits = [
{"mark_a": "C1", "mark_b": "C8", "penetration": 3.0},
{"mark_a": "C8", "mark_b": "C1", "penetration": 7.0},
{"mark_a": "C2", "mark_b": "C2", "penetration": 1.5},
]
s = ck.summarize_by_mark(hits)
assert s["C1|C8"] == {"count": 2, "max_penetration": 7.0}
assert s["C2|C2"] == {"count": 1, "max_penetration": 1.5}
+168
View File
@@ -0,0 +1,168 @@
"""frame 테스트 (Rhino 없이 실행됩니다).
월드 좌표 (x, y, z) <-> 코핑 로컬 좌표 (s, t, z)
s = 길이방향 (코핑 축), t = 폭방향 (s를 반시계로 90도 돌린 방향), z = 월드 높이 그대로
기존 프로젝트: rebar_pipeline/rhino/07_ctc_extract.py 의 OX, OY, ANG 방식과 같습니다.
"""
import math
import pytest
import frame as fr
# ---- 기본 변환 ----
def test_identity_frame_keeps_coordinates():
f = fr.Frame(0.0, 0.0, 0.0)
assert f.to_local(10.0, 20.0, 5.0) == pytest.approx((10.0, 20.0, 5.0))
def test_origin_maps_to_zero_zero():
f = fr.Frame(1000.0, 2000.0, 0.3)
s, t, z = f.to_local(1000.0, 2000.0, 77.0)
assert (s, t) == pytest.approx((0.0, 0.0))
assert z == 77.0
def test_z_is_not_changed():
f = fr.Frame(1000.0, 2000.0, 0.3)
assert f.to_local(1500.0, 2500.0, 9331.0)[2] == 9331.0
assert f.to_world(10.0, 20.0, 9331.0)[2] == 9331.0
def test_angle_90_degrees_swaps_axes():
f = fr.Frame(0.0, 0.0, math.pi / 2)
# 월드 +Y 방향이 로컬 +s 방향
assert f.to_local(0.0, 100.0, 0.0) == pytest.approx((100.0, 0.0, 0.0))
# 월드 -X 방향이 로컬 +t 방향
assert f.to_local(-50.0, 0.0, 0.0) == pytest.approx((0.0, 50.0, 0.0))
def test_angle_45_degrees():
f = fr.Frame(0.0, 0.0, math.pi / 4)
r = math.sqrt(2.0) * 50.0
assert f.to_local(50.0, 50.0, 0.0) == pytest.approx((r, 0.0, 0.0))
def test_to_world_basic():
f = fr.Frame(100.0, 200.0, math.pi / 2)
# 로컬 s=10 -> 월드 +Y 방향으로 10
assert f.to_world(10.0, 0.0, 0.0) == pytest.approx((100.0, 210.0, 0.0))
# 로컬 t=10 -> 월드 -X 방향으로 10
assert f.to_world(0.0, 10.0, 0.0) == pytest.approx((90.0, 200.0, 0.0))
# ---- 왕복 변환 (월드 -> 로컬 -> 월드가 원래 값이어야 함) ----
@pytest.mark.parametrize("point", [
(480708784.0, 153831202.0, 7738.0),
(480721068.0, 153835457.0, -342.0),
(0.0, 0.0, 0.0),
(-5.5, 1234.5, 10.0),
])
def test_round_trip_world_local_world(point):
f = fr.from_two_points((480708784.0, 153831202.0), (480721068.0, 153835457.0))
back = f.to_world(*f.to_local(*point))
assert back == pytest.approx(point, abs=1e-6)
def test_round_trip_local_world_local():
f = fr.Frame(480714926.0, 153833329.5, 0.3333)
local = (1234.5, -678.9, 10405.3)
back = f.to_local(*f.to_world(*local))
assert back == pytest.approx(local, abs=1e-6)
# ---- 두 기둥 중심으로 프레임 만들기 (대호지교 P1 값) ----
def test_from_two_points_origin_is_midpoint():
f = fr.from_two_points((480708784.0, 153831202.0), (480721068.0, 153835457.0))
assert f.origin_x == pytest.approx(480714926.0)
assert f.origin_y == pytest.approx(153833329.5)
def test_from_two_points_angle_matches_p1():
f = fr.from_two_points((480708784.0, 153831202.0), (480721068.0, 153835457.0))
assert f.angle == pytest.approx(math.atan2(4255.0, 12284.0))
assert math.degrees(f.angle) == pytest.approx(19.105, abs=0.001)
def test_p1_columns_are_on_s_axis_at_half_spacing():
a = (480708784.0, 153831202.0)
b = (480721068.0, 153835457.0)
f = fr.from_two_points(a, b)
sa, ta, _ = f.to_local(a[0], a[1], 0.0)
sb, tb, _ = f.to_local(b[0], b[1], 0.0)
# 기둥 중심간격 13,000 (좌표 반올림 때문에 0.1mm 이내)
assert sa == pytest.approx(-6500.0, abs=0.1)
assert sb == pytest.approx(6500.0, abs=0.1)
assert ta == pytest.approx(0.0, abs=1e-6)
assert tb == pytest.approx(0.0, abs=1e-6)
def test_from_two_points_rejects_same_point():
with pytest.raises(ValueError):
fr.from_two_points((1.0, 1.0), (1.0, 1.0))
# ---- 프레임 바꾸기 (언제든 다시 바꿀 수 있어야 함) ----
def test_convert_between_frames_matches_going_through_world():
f1 = fr.Frame(100.0, 200.0, 0.2)
f2 = fr.Frame(-500.0, 40.0, 1.1)
local1 = (321.0, -45.0, 12.0)
via_world = f2.to_local(*f1.to_world(*local1))
assert fr.convert(local1, f1, f2) == pytest.approx(via_world, abs=1e-6)
def test_convert_there_and_back_gives_original():
f1 = fr.Frame(100.0, 200.0, 0.2)
f2 = fr.Frame(-500.0, 40.0, 1.1)
local1 = (321.0, -45.0, 12.0)
back = fr.convert(fr.convert(local1, f1, f2), f2, f1)
assert back == pytest.approx(local1, abs=1e-6)
def test_convert_same_frame_changes_nothing():
f = fr.Frame(100.0, 200.0, 0.2)
assert fr.convert((1.0, 2.0, 3.0), f, f) == pytest.approx((1.0, 2.0, 3.0))
def test_new_origin_changes_local_values_but_not_world():
# 원점만 바꿔도 월드 위치는 그대로, 로컬 값만 달라집니다.
world = (1000.0, 2000.0, 5.0)
f1 = fr.Frame(0.0, 0.0, 0.3)
f2 = fr.Frame(500.0, 500.0, 0.3)
l1 = f1.to_local(*world)
l2 = f2.to_local(*world)
assert l1 != pytest.approx(l2)
assert f1.to_world(*l1) == pytest.approx(world, abs=1e-6)
assert f2.to_world(*l2) == pytest.approx(world, abs=1e-6)
# ---- 방향 벡터 (이동 없이 회전만) ----
def test_direction_to_local_ignores_origin():
f = fr.Frame(1000.0, 2000.0, math.pi / 2)
assert f.direction_to_local(0.0, 1.0, 0.0) == pytest.approx((1.0, 0.0, 0.0))
def test_direction_round_trip():
f = fr.Frame(1000.0, 2000.0, 0.7)
d = (0.3, -0.8, 0.5)
assert f.direction_to_world(*f.direction_to_local(*d)) == pytest.approx(d)
# ---- 점 여러 개 ----
def test_to_local_many_matches_single_calls():
f = fr.Frame(10.0, 20.0, 0.4)
pts = [(1.0, 2.0, 3.0), (100.0, -50.0, 7.0)]
assert f.to_local_many(pts) == [f.to_local(*p) for p in pts]
def test_to_world_many_matches_single_calls():
f = fr.Frame(10.0, 20.0, 0.4)
pts = [(1.0, 2.0, 3.0), (100.0, -50.0, 7.0)]
assert f.to_world_many(pts) == [f.to_world(*p) for p in pts]
+117
View File
@@ -0,0 +1,117 @@
"""rebar_calc 테스트 (Rhino 없이 실행됩니다).
기준값 출처: docs/design_rules.md
"""
import pytest
import rebar_calc as rc
# ---- 철근 이름 -> 공칭 직경 ----
@pytest.mark.parametrize("name, expected", [
("H13", 12.7),
("H16", 15.9),
("H19", 19.1),
("H22", 22.2),
("H25", 25.4),
("H29", 28.6),
])
def test_nominal_diameter(name, expected):
assert rc.nominal_diameter(name) == expected
def test_nominal_diameter_accepts_d_prefix_and_lowercase():
assert rc.nominal_diameter("D25") == 25.4
assert rc.nominal_diameter("h25") == 25.4
def test_nominal_diameter_unknown_bar_raises():
with pytest.raises(ValueError):
rc.nominal_diameter("H99")
# ---- 구부림 반지름 (프로젝트 적용값, 건축기준 기반) ----
def test_bend_radius_h25_is_3db():
# 두부보 H25 주철근: R = 76.2mm
assert rc.bend_radius("H25") == pytest.approx(76.2)
def test_bend_radius_h29_is_4db():
# 두부보 H29: R = 114.4mm
assert rc.bend_radius("H29") == pytest.approx(114.4)
def test_bend_radius_h38_is_5db():
assert rc.bend_radius("H38") == pytest.approx(5 * 38.1)
def test_bend_radius_stirrup_d16_or_less_is_2db():
assert rc.bend_radius("H16", is_stirrup=True) == pytest.approx(2 * 15.9)
assert rc.bend_radius("H13", is_stirrup=True) == pytest.approx(2 * 12.7)
def test_bend_radius_stirrup_d19_or_more_uses_general_table():
assert rc.bend_radius("H19", is_stirrup=True) == pytest.approx(3 * 19.1)
def test_bend_radius_main_bar_h16_is_3db_not_stirrup_rule():
assert rc.bend_radius("H16") == pytest.approx(3 * 15.9)
# ---- KDS 24 14 21 최소 내면 지름 ----
def test_kds_min_inner_diameter_d16_or_less_is_2db():
assert rc.kds_min_inner_diameter("H16") == pytest.approx(2 * 15.9)
def test_kds_min_inner_diameter_d19_or_more_is_3_5db():
assert rc.kds_min_inner_diameter("H25") == pytest.approx(3.5 * 25.4)
def test_project_radius_satisfies_kds_for_all_bars():
# 프로젝트 R(반지름)의 2배 = 내면 지름이 KDS 최소값 이상이어야 합니다.
for name in ["H13", "H16", "H19", "H22", "H25", "H29"]:
assert rc.satisfies_kds_bend(name, rc.bend_radius(name))
assert rc.satisfies_kds_bend(name, rc.bend_radius(name, is_stirrup=True))
def test_too_small_radius_fails_kds():
# H25는 내면 지름 88.9mm 이상이 필요 -> 반지름 40mm(지름 80mm)는 부족
assert not rc.satisfies_kds_bend("H25", 40.0)
# ---- 갈고리 연장 길이 ----
def test_hook_extension_90_is_12db():
assert rc.hook_extension("H25", 90) == pytest.approx(12 * 25.4)
def test_hook_extension_135_is_6db():
assert rc.hook_extension("H25", 135) == pytest.approx(6 * 25.4)
def test_hook_extension_180_is_4db():
assert rc.hook_extension("H25", 180) == pytest.approx(4 * 25.4)
def test_hook_extension_180_has_60mm_minimum():
# H13: 4db = 50.8mm < 60mm 이므로 60mm
assert rc.hook_extension("H13", 180) == pytest.approx(60.0)
def test_hook_extension_unsupported_angle_raises():
with pytest.raises(ValueError):
rc.hook_extension("H25", 45)
def test_kds_min_hook_extension_is_5db_for_90_to_150():
assert rc.kds_min_hook_extension("H25", 90) == pytest.approx(5 * 25.4)
assert rc.kds_min_hook_extension("H25", 135) == pytest.approx(5 * 25.4)
def test_project_hook_extension_satisfies_kds_for_90_and_135():
for name in ["H13", "H16", "H19", "H22", "H25", "H29"]:
for angle in (90, 135):
assert rc.hook_extension(name, angle) >= rc.kds_min_hook_extension(name, angle)
@@ -0,0 +1,154 @@
"""rebar_layout 테스트 (Rhino 없이 실행됩니다).
기준: docs/design_rules.md, 기존 프로젝트 rebar_pipeline/README.md §3
"""
import pytest
import rebar_layout as rl
# ---- 중심에서 바깥으로 배치 ----
def test_center_out_even_count_has_no_bar_on_center():
assert rl.center_out_positions(4, 250) == [-375, -125, 125, 375]
def test_center_out_odd_count_has_bar_on_center():
assert rl.center_out_positions(3, 250) == [-250, 0, 250]
def test_center_out_13_bars_goes_to_1500():
# 2CYCLE: 0, ±250 ... ±1500
pos = rl.center_out_positions(13, 250)
assert len(pos) == 13
assert pos[0] == -1500
assert pos[-1] == 1500
assert 0 in pos
def test_center_out_zero_bars_is_empty():
assert rl.center_out_positions(0, 250) == []
def test_center_out_rejects_bad_input():
with pytest.raises(ValueError):
rl.center_out_positions(-1, 250)
with pytest.raises(ValueError):
rl.center_out_positions(5, 0)
# ---- 바깥 철근을 피복에 맞추는 배치 (1CYCLE) ----
def test_snapped_array_matches_1cycle_drawing():
# 두부보 폭 3500, 피복 120, 14본, 간격 250
pos = rl.snapped_array(14, 250, 3500, 120)
assert len(pos) == 14
assert pos[0] == -1630
assert pos[1] == -1375
assert pos[6] == -125
assert pos[7] == 125
assert pos[-2] == 1375
assert pos[-1] == 1630
def test_snapped_array_is_symmetric_and_sorted():
pos = rl.snapped_array(14, 250, 3500, 120)
assert pos == sorted(pos)
assert pos == [-p for p in reversed(pos)]
def test_snapped_array_outer_bars_sit_at_cover():
pos = rl.snapped_array(14, 250, 3500, 120)
assert max(pos) == 3500 / 2 - 120
def test_snapped_array_rejects_too_few_bars():
with pytest.raises(ValueError):
rl.snapped_array(1, 250, 3500, 120)
def test_snapped_array_rejects_when_inner_bars_pass_outer_bars():
# 폭이 너무 좁아서 안쪽 철근이 바깥 철근을 넘어감
with pytest.raises(ValueError):
rl.snapped_array(14, 250, 2000, 120)
# ---- 수평 순간격 (KDS 24 14 21 4.5.3) ----
def test_min_horizontal_clear_h25_is_40mm():
# max(1.5*25.4=38.1, 1.5*25=37.5, 40) = 40
assert rl.min_horizontal_clear("H25", gmax=25) == pytest.approx(40.0)
def test_min_horizontal_clear_h29_uses_1_5db():
assert rl.min_horizontal_clear("H29", gmax=25) == pytest.approx(1.5 * 28.6)
def test_min_horizontal_clear_large_aggregate_governs():
assert rl.min_horizontal_clear("H25", gmax=40) == pytest.approx(60.0)
def test_clear_spacing_is_center_gap_minus_diameter():
assert rl.clear_spacing(250, "H25") == pytest.approx(250 - 25.4)
def test_horizontal_spacing_ok_for_250_array():
pos = rl.snapped_array(14, 250, 3500, 120)
assert rl.horizontal_spacing_ok(pos, "H25", gmax=25)
def test_horizontal_spacing_fails_when_too_close():
# 중심 간격 50 -> 순간격 24.6mm < 40mm
assert not rl.horizontal_spacing_ok([0, 50], "H25", gmax=25)
# ---- 다단 연직 배치 ----
def test_min_vertical_clear_h25_is_db():
# max(25, 25.4) = 25.4
assert rl.min_vertical_clear("H25") == pytest.approx(25.4)
def test_min_vertical_clear_small_bar_uses_25mm():
assert rl.min_vertical_clear("H16") == pytest.approx(25.0)
def test_vertical_pitch_100_ok_for_h25():
assert rl.vertical_pitch_ok("H25", 100)
def test_vertical_pitch_40_fails_for_h25():
# 순간격 14.6mm < 25.4mm
assert not rl.vertical_pitch_ok("H25", 40)
def test_tier_elevations_go_down_from_top_cover():
assert rl.tier_elevations(1000, 120, 2, 100) == [880, 780]
def test_tier_elevations_single_tier():
assert rl.tier_elevations(1000, 120, 1, 100) == [880]
# ---- 수량, 중량 ----
def test_unit_weight_h25():
assert rl.unit_weight("H25") == pytest.approx(3.98)
def test_unit_weight_unknown_bar_raises():
with pytest.raises(ValueError):
rl.unit_weight("H99")
def test_bar_weight_one_bar():
# H25, 9.930m 1본 = 3.98 * 9.93
assert rl.bar_weight("H25", 9930) == pytest.approx(3.98 * 9.93)
def test_array_summary_2cycle_main_bars():
pos = rl.center_out_positions(13, 250)
s = rl.array_summary("H25", 9930, pos)
assert s["count"] == 13
assert s["total_length_m"] == pytest.approx(13 * 9.93)
assert s["weight_kg"] == pytest.approx(3.98 * 9.93 * 13)
+193
View File
@@ -0,0 +1,193 @@
==================================================
교각 철근 프로그램 사용법 (메모장용)
==================================================
[1] 이 프로그램이 뭔가요?
- 철근의 구부림 반지름, 갈고리 길이 같은 값을 계산하는 도구입니다.
- 지금은 계산 모듈 1개(rebar_calc.py)와 테스트가 있습니다.
- 앞으로 철근 수량, 간격, Rhino 배치 코드를 이 폴더에 계속 추가합니다.
[2] 폴더 위치 (전부 D 드라이브)
프로젝트 폴더 : D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program
계산 모듈 : D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\scripts\rebar_calc.py
테스트 : D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\tests\test_rebar_calc.py
설계 규칙 : D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\docs\design_rules.md
할 일 목록 : D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\docs\tasks.md
경로 목록 : D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\docs\paths.md
이 사용법 : D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\사용법.txt
※ 기존 프로젝트 D:\Rhino-MCP\rhino-mcp-main 은 참고용입니다. 수정하지 마세요.
[3] 터미널(PowerShell) 여는 방법
1) 윈도우 탐색기에서 D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program 폴더를 엽니다.
2) 위쪽 주소창을 클릭하고 powershell 이라고 입력한 뒤 Enter.
3) 파란 창이 열리면 이미 프로젝트 폴더 안에 있는 상태입니다.
[4] 테스트 실행 (계산이 맞는지 확인)
터미널에서 아래를 입력하고 Enter:
python -m pytest -q
- 마지막 줄에 "119 passed" 라고 나오면 모두 정상입니다.
(앞의 숫자는 테스트가 늘어나면 같이 늘어납니다. "failed" 가 없으면 정상입니다.)
- "failed" 라고 나오면 어느 계산이 틀렸는지 표시됩니다. 그 내용을 Claude에게 붙여넣으세요.
※ "python" 이 안 되면 아래처럼 전체 경로로 실행하세요:
C:\Users\User\AppData\Local\Programs\Python\Python312\python.exe -m pytest -q
[5] 계산 모듈 직접 써보기
터미널에서 아래를 입력하면 계산기처럼 쓸 수 있습니다.
cd scripts
python
파란 창에 ">>>" 가 나오면 한 줄씩 입력합니다.
>>> import rebar_calc as rc
>>> rc.nominal_diameter("H25") # 공칭 직경
25.4
>>> round(rc.bend_radius("H25"), 1) # 구부림 반지름 (3db)
76.2
>>> round(rc.bend_radius("H29"), 1) # H29는 4db
114.4
>>> round(rc.bend_radius("H16", is_stirrup=True), 1) # 스터럽 D16 이하는 2db
31.8
>>> round(rc.hook_extension("H25", 90), 1) # 90도 갈고리 연장 길이 (12db)
304.8
>>> round(rc.hook_extension("H25", 135), 1) # 135도는 6db
152.4
>>> rc.satisfies_kds_bend("H25", 76.2) # KDS 기준을 만족하나요?
True
끝낼 때는 exit() 를 입력합니다.
모든 길이의 단위는 mm 입니다.
※ round(값, 1) 은 소수 첫째 자리까지 반올림하는 명령입니다.
round 없이 쓰면 76.19999999999999 처럼 아주 작은 오차가 보일 수 있습니다.
컴퓨터가 소수를 계산하는 방식 때문이고, 값은 맞습니다.
[6] 함수 설명표
nominal_diameter(이름) 공칭 직경 (예: "H25" -> 25.4)
bend_radius(이름, is_stirrup) 프로젝트 적용 구부림 반지름
(D10~D25 3db, D29~D35 4db, D38 이상 5db,
스터럽 D16 이하 2db)
kds_min_inner_diameter(이름) KDS 24 14 21 최소 내면 지름
(D16 이하 2db, D19 이상 3.5db)
satisfies_kds_bend(이름, 반지름) KDS 최소값을 만족하면 True
hook_extension(이름, 각도) 갈고리 연장 길이 (90도 12db, 135도 6db,
180도 4db 또는 60mm 중 큰 값)
kds_min_hook_extension(이름, 각도) KDS 최소 연장 길이 (90~150도 5db)
철근 이름은 "H25" 처럼 씁니다. "D25", 소문자 "h25" 도 됩니다.
지원 규격: H10, H13, H16, H19, H22, H25, H29, H32, H35, H38
--- 배치 계산 (scripts\rebar_layout.py) ---
사용: import rebar_layout as rl
center_out_positions(본수, 간격) 중심에서 바깥으로 놓은 폭방향 위치 목록
(짝수=중심에 철근 없음, 홀수=중심에 1본)
snapped_array(본수, 간격, 폭, 피복) 바깥 2본은 피복에 맞추고 안쪽은 중심에서 바깥으로
(1CYCLE 방식. 예: 14본, 250, 3500, 120)
min_horizontal_clear(이름, gmax) 수평 최소 순간격 max(1.5db, 1.5Gmax, 40)
horizontal_spacing_ok(위치목록, 이름) 간격이 충분하면 True
min_vertical_clear(이름) 다단 연직 최소 순간격 max(25, db)
vertical_pitch_ok(이름, 단 간격) 단 간격이 충분하면 True
tier_elevations(상면높이, 피복, 단수, 간격) 각 단의 높이 목록
bar_weight(이름, 길이mm, 본수) 중량(kg)
array_summary(이름, 길이mm, 위치목록) 본수, 총길이(m), 중량(kg)
예) >>> import rebar_layout as rl
>>> rl.snapped_array(14, 250, 3500, 120)
[-1630.0, -1375.0, ..., -125.0, 125.0, ..., 1375.0, 1630.0]
※ 굵은골재 최대치수(gmax)는 가정값 25mm 입니다. 설계서 값이 다르면 알려주세요.
※ 단위중량은 H16, H19, H22, H25, H29 만 등록되어 있습니다. 다른 규격은 오류가 납니다.
--- 좌표 변환 (scripts\frame.py) ---
월드 좌표 (x, y, z) 를 코핑 기준 좌표 (s, t, z) 로 바꾸거나 되돌립니다.
s = 코핑 길이방향, t = 폭방향, z = 높이(그대로)
원점과 각도는 언제든 바꿀 수 있습니다. (값으로 받기 때문입니다)
사용: import frame as fr
fr.Frame(원점x, 원점y, 각도) 프레임 직접 만들기 (각도는 라디안)
fr.from_two_points((x1,y1), (x2,y2)) 두 점으로 만들기 (원점=가운데, s축=1번->2번 방향)
예: 2주식 교각의 두 기둥 중심
f.to_local(x, y, z) 월드 -> 로컬
f.to_world(s, t, z) 로컬 -> 월드
fr.convert(점, 프레임1, 프레임2) 프레임1 기준 값을 프레임2 기준 값으로
예) >>> import frame as fr
>>> f = fr.from_two_points((480708784, 153831202), (480721068, 153835457))
>>> f.to_local(480708784, 153831202, 7738)
(-6500.032326842691, -2.2737367544323206e-13, 7738)
<- 기둥 중심: 코핑 중앙에서 s 방향으로 약 -6500, t는 0 (e-13 은 0에 아주 가까운 계산 오차)
원점이나 각도를 바꾸고 싶으면 새 프레임을 만들어 쓰면 됩니다.
월드 위치는 변하지 않고 로컬 숫자만 달라집니다.
--- 철근 중심선 꺾임점 (scripts\centerline.py) ---
단면 외곽선을 안쪽으로 피복만큼 옮긴 직선들의 교점으로 철근 중심선을 구합니다.
2D 좌표는 (s, z) 입니다. (s=길이방향, z=높이)
사용: import centerline as cl
cl.centerline_points(선분목록, 피복, "left"/"right", start_len=, end_len=)
선분목록 : 철근이 지나가는 순서와 방향대로 [(시작점, 끝점), ...]
"left"/"right" : 콘크리트 안쪽이 진행 방향의 왼쪽인지 오른쪽인지
start_len / end_len : 도면에 정해진 시작·끝 연장 길이 (없으면 생략)
cl.segment_lengths(점목록) 각 구간 길이 (도면의 a, b, c, d 와 비교)
cl.polyline_length(점목록) 전체 길이
cl.turn_angles(점목록) 꺾임 각도(도)
예) 대호지교 P1 형식 VII (C8/C10), 도면과 같은 값이 나옵니다:
>>> import centerline as cl
>>> segs = [((12100,10738),(12100,9238)), ((12100,9238),(8100,7738)),
... ((8100,7738),(4900,7738)), ((4900,7738),(3400,8238))]
>>> pts = cl.centerline_points(segs, 120, "right", start_len=630, end_len=2331)
>>> [round(x, 1) for x in cl.segment_lengths(pts)]
[630.0, 4167.1, 3158.8, 2331.0] <- 도면 a, b, c, d
※ 길이는 중심선 길이입니다. 재료표의 전개길이(꺾임 보정 포함)와는 다를 수 있습니다.
--- 철근 간섭 검사 (scripts\clash.py) ---
철근 중심선을 선분으로 쪼개서 서로 겹치는(간섭) 철근을 찾습니다.
간섭 = 두 철근 반지름의 합 - 중심선 최단거리 가 0.5mm 보다 클 때.
같은 철근이나 같은 그룹(겹침이음인 뚜껑+바닥 조각)끼리는 검사하지 않습니다.
사용: import clash as ck
ck.Seg(시작점, 끝점, 반지름, 철근번호, 그룹번호, 마크이름) 선분 하나 만들기
ck.find_clashes(선분목록, min_penetration=0.5) 간섭 목록 (깊은 것부터)
ck.summarize_by_mark(간섭목록) 마크 쌍별 개수와 최대 관입
ck.segment_distance(p1, q1, p2, q2) 선분 두 개 사이 최단거리
예) >>> import clash as ck
>>> a = ck.Seg((-100,0,0), (100,0,0), 12.7, 1, 1, "C1")
>>> b = ck.Seg((0,-100,20), (0,100,20), 12.7, 2, 2, "C8")
>>> ck.find_clashes([a, b])
[{'bar_a': 1, 'bar_b': 2, 'mark_a': 'C1', 'mark_b': 'C8',
'penetration': 5.4, 'point': (0.0, 0.0, 10.0)}]
<- 철근 중심이 20mm 떨어졌는데 반지름 합이 25.4 라서 5.4mm 겹침
(실제 출력은 5.399999999999999 처럼 미세한 오차가 보일 수 있습니다)
※ 월드 좌표(약 4.8억)보다 frame.py 로 바꾼 로컬 좌표를 쓰면 계산이 더 정확합니다.
[7] Claude와 같이 작업하는 순서
1) Claude에게 할 일을 말합니다. (docs\tasks.md 의 번호로 말하면 쉽습니다. 예: "T-003 시작해줘")
2) Claude가 테스트를 먼저 만들고, 실패하는 것을 확인한 뒤 코드를 만듭니다.
3) 끝나면 Claude가 docs 와 README 를 업데이트할지 묻습니다.
4) 커밋(저장 기록)은 Claude가 물어본 뒤 "응" 이라고 하면 합니다.
5) Claude는 매번 응답 끝에 "이번에 쓴 경로"를 알려줍니다.
[8] 헷갈릴 때
- 경로가 헷갈리면 D:\★Pier 자료\pier_v1\Pier_T_rebar\Code\pier-program\docs\paths.md 를 보세요.
- 새 프로그램은 pier-program, 옛날 자료는 rhino-mcp-main 입니다.
- 큰 파일(렌더링, 백업, DXF, Rhino 파일)은 항상 D 드라이브에 저장합니다.
+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()
+6
View File
@@ -0,0 +1,6 @@
# 교량 도면↔모델 검토 파이프라인 (산외교·옥성교·Ramp-A교, 2026-10-19)
순서: DWG→DXF(라이노 headless `RhinoDoc.CreateHeadless` → Import → Export) → `dxfdump.py`(글자 추출) → 교량별 `*_parse/*_analyze/*_crops`(재료표·철근상세 읽기, rhino3dm 으로 모델 치수 추출) → `shots_*.py`(라이노에서 `cap.py` 로 캡처) → `*_build_images.py`(`compose_new.py`) → `*_cfg.py` → `make_review_xlsx.py` → `*_mark_dxf.py`(도면 표시).
- 접두어 ok_=옥성교, sw_=산외교, ra_=Ramp-A교. pylib(rhino3dm·ezdxf·openpyxl·matplotlib·PIL)는 `pip install --target` 으로 설치.
- 주의: DXF 텍스트는 UTF-8(cp949 지정 금지), 터미널 한글 출력은 깨져 보임(파일로 쓰고 Read). 경로는 스크립트 안 SCR 상수를 환경에 맞게 수정.
- 결과 폴더: D:\★Pier 자료\심수석님 검토 파일\검토_산외교P1 / 검토_옥성교P1 / Ramp-A교P1 (각 검토기록_최종.md, 도면내부_오류_보관.md)
+120
View File
@@ -0,0 +1,120 @@
# exec'd inside Rhino via execute_rhino_code: g={}; exec(open(path,encoding='utf-8').read(), g); g['run'](shot)
import scriptcontext as sc
import Rhino
import System
from System.Drawing import Size, Color
SCR = r"C:\Users\User\AppData\Local\Temp\claude\D--Rhino-MCP-rhino-mcp-main\f46b47bc-9105-4079-9b62-fcbee31b26ae\scratchpad"
OUT = SCR + r"\shots"
def all_objs():
st = Rhino.DocObjects.ObjectEnumeratorSettings()
st.HiddenObjects = True
st.NormalObjects = True
st.LockedObjects = True
st.IncludeLights = False
st.IncludeGrips = False
return sc.doc.Objects.GetObjectList(st)
def state_path():
return SCR + r"\hidden_state.txt"
def save_state():
doc = sc.doc
ids = []
for o in doc.Objects.GetObjectList(Rhino.DocObjects.ObjectEnumeratorSettings()):
pass
return None
def run(shot):
doc = sc.doc
import os
if not os.path.isdir(OUT):
os.makedirs(OUT)
layers = shot["layers"] # list of layer-path prefixes to show
names = shot.get("names") # optional name substrings (applies to rebar objects only)
show_solids = shot.get("solids", []) # exact layer paths of concrete to show
for o in all_objs():
ln = doc.Layers[o.Attributes.LayerIndex].FullPath
vis = False
for s in show_solids:
if ln == s:
vis = True
for p in layers:
if ln == p or ln.startswith(p + "::"):
if names:
ok = False
for n in names:
if n in o.Name:
ok = True
if ok:
vis = True
else:
vis = True
# skip centerline curves unless asked
if "중심선" in ln and not shot.get("centerline", False):
vis = False
if vis:
doc.Objects.Show(o, True)
else:
doc.Objects.Hide(o, True)
view = doc.Views.ActiveView
vp = view.ActiveViewport
mode = None
for d in Rhino.Display.DisplayModeDescription.GetDisplayModes():
if d.EnglishName == shot.get("mode", "Ghosted"):
mode = d
if mode:
vp.DisplayMode = mode
pr = shot["view"]
if pr == "Front":
vp.SetProjection(Rhino.Display.DefinedViewportProjection.Front, "Front", False)
elif pr == "Right":
vp.SetProjection(Rhino.Display.DefinedViewportProjection.Right, "Right", False)
elif pr == "Top":
vp.SetProjection(Rhino.Display.DefinedViewportProjection.Top, "Top", False)
else:
vp.SetProjection(Rhino.Display.DefinedViewportProjection.Perspective, "Perspective", False)
c = shot["cam"]
vp.SetCameraLocations(Rhino.Geometry.Point3d(c[3], c[4], c[5]), Rhino.Geometry.Point3d(c[0], c[1], c[2]))
b = shot["box"]
bb = Rhino.Geometry.BoundingBox(Rhino.Geometry.Point3d(b[0], b[1], b[2]), Rhino.Geometry.Point3d(b[3], b[4], b[5]))
vp.ZoomBoundingBox(bb)
view.Redraw()
vs = vp.Size
k = shot.get("k", 2.0)
w = int(vs.Width * k)
h = int(vs.Height * k)
bmp = view.CaptureToBitmap(Size(w, h), False, False, False)
bmp.Save(OUT + "\\" + shot["file"])
xf = vp.GetTransform(Rhino.DocObjects.CoordinateSystem.World, Rhino.DocObjects.CoordinateSystem.Screen)
res = []
for p in shot.get("pts", []):
q = Rhino.Geometry.Point3d(p[0], p[1], p[2])
q.Transform(xf)
res.append("%.1f,%.1f" % (q.X * k, q.Y * k))
open(OUT + "\\" + shot["file"] + ".txt", "w").write(("%d,%d" % (w, h)) + chr(10) + chr(10).join(res))
return shot["file"]
def restore():
doc = sc.doc
f3 = Rhino.FileIO.File3dm.Read(doc.Path)
hid = set()
for ob in f3.Objects:
if not ob.Attributes.Visible:
hid.add(str(ob.Attributes.ObjectId))
f3.Dispose()
n = 0
for o in all_objs():
if str(o.Id) in hid:
doc.Objects.Hide(o, True)
n += 1
else:
doc.Objects.Show(o, True)
doc.Views.Redraw()
return n
+12
View File
@@ -0,0 +1,12 @@
import sys,io; sys.path.insert(0,'pylib')
import openpyxl
p=sys.argv[1]
wb=openpyxl.load_workbook(p)
o=io.open('chk_out.txt','w',encoding='utf-8')
for ws in wb:
o.write(u'== %s rows %d cols %d images %d\n'%(ws.title,ws.max_row,ws.max_column,len(ws._images)))
ws=wb['② 전체 대조표']
for r in list(ws.iter_rows(min_row=5,max_row=9,values_only=True)): o.write(u'%s\n'%(list(r),))
for r in list(ws.iter_rows(min_row=45,max_row=51,values_only=True)): o.write(u'%s\n'%(list(r),))
import collections
c=collections.Counter(r[8] for r in ws.iter_rows(min_row=5,values_only=True)); o.write(u'%s\n'%c)
+134
View File
@@ -0,0 +1,134 @@
"""도면↔모델 불일치 시각자료 합성기 (운곡교 M-5 이미지 형식: 헤더 / 도면 패널(파랑) / 모델 패널(주황·노랑·회색) / 차이·도면 쪽·조치).
사용: from compose_new import make; make(cfg)
cfg = {code, title, subtitle, color:(r,g,b), out,
panels:[{label, color, images:[{path, crop(optional), cap(optional)}], maxh}], rows:[(라벨, 본문)]}
"""
import os, sys
sys.path.insert(0, r"C:\Users\User\AppData\Local\Temp\claude\D--Rhino-MCP-rhino-mcp-main\f46b47bc-9105-4079-9b62-fcbee31b26ae\scratchpad\pylib")
from PIL import Image, ImageDraw, ImageFont
FN = r"C:\Windows\Fonts\malgun.ttf"
FB = r"C:\Windows\Fonts\malgunbd.ttf"
W = 2400
PAD = 40
BLUE = (60, 120, 200)
ORANGE = (230, 110, 20)
DYEL = (190, 150, 0)
GREY = (120, 120, 120)
def font(sz, bold=False):
return ImageFont.truetype(FB if bold else FN, sz)
def fit(img, w, h):
r = min(w / img.width, h / img.height)
return img.resize((max(1, int(img.width * r)), max(1, int(img.height * r))), Image.LANCZOS)
def wrap(draw, text, fnt, maxw):
lines = []
for para in text.split("\n"):
cur = ""
for ch in para:
if draw.textlength(cur + ch, font=fnt) > maxw:
lines.append(cur)
cur = ch
else:
cur += ch
lines.append(cur)
return lines
def autocrop(im, pad=30, thr=14):
bg = im.getpixel((2, 2))
px = im.load()
w, h = im.size
x0, y0, x1, y1 = w, h, 0, 0
for y in range(0, h, 2):
for x in range(0, w, 2):
p = px[x, y]
if abs(p[0] - bg[0]) + abs(p[1] - bg[1]) + abs(p[2] - bg[2]) > thr:
if x < x0: x0 = x
if x > x1: x1 = x
if y < y0: y0 = y
if y > y1: y1 = y
if x1 <= x0:
return im
return im.crop((max(0, x0 - pad), max(0, y0 - pad), min(w, x1 + pad), min(h, y1 + pad)))
def load(spec):
im = Image.open(spec["path"]).convert("RGB")
if spec.get("auto", spec["path"].find("shots") >= 0):
im = autocrop(im)
if spec.get("crop"):
im = im.crop(spec["crop"])
if spec.get("draw"):
spec["draw"](im)
return im
def make(cfg):
PW = W - 2 * PAD
# 패널 이미지 준비
prepared = []
for p in cfg["panels"]:
ims = [load(s) for s in p["images"]]
n = len(ims)
gap = 20
cw = (PW - 8 - gap * (n - 1)) // n
maxh = p.get("maxh", 560)
fitted = [fit(im, cw, maxh) for im in ims]
ph = max(f.height for f in fitted) + 8
capsp = 0
if any(s.get("cap") for s in p["images"]):
capsp = 52
prepared.append((p, fitted, ph, capsp, cw, gap))
# 본문 줄 계산
dtmp = ImageDraw.Draw(Image.new("RGB", (10, 10)))
fr = font(32)
rows = []
for lab, text in cfg["rows"]:
lines = wrap(dtmp, text, fr, PW - 230)
rows.append((lab, lines))
H = 120 + 14
for p, fitted, ph, capsp, cw, gap in prepared:
H += 50 + 6 + ph + capsp + 14
H += 12
for lab, lines in rows:
H += max(46, 42 * len(lines)) + 12
H += 24
img = Image.new("RGB", (W, H), (255, 255, 255))
d = ImageDraw.Draw(img)
# 헤더
d.rectangle([0, 0, W, 120], fill=(38, 44, 56))
d.rounded_rectangle([PAD - 6, 24, PAD + 130, 96], radius=10, fill=cfg.get("color", ORANGE))
d.text((PAD + 62, 60), cfg["code"], font=font(52, True), fill=(255, 255, 255), anchor="mm")
d.text((PAD + 170, 14), cfg["title"], font=font(48, True), fill=(255, 255, 255))
d.text((PAD + 170, 74), cfg["subtitle"], font=font(30), fill=(210, 215, 225))
y = 134
for p, fitted, ph, capsp, cw, gap in prepared:
col = p.get("color", BLUE)
d.rectangle([PAD - 6, y, W - PAD + 6, y + 50], fill=col)
d.text((PAD + 10, y + 25), p["label"], font=font(32, True), fill=(255, 255, 255), anchor="lm")
y += 56
d.rectangle([PAD - 6, y, W - PAD + 6, y + ph + capsp + 4], outline=col, width=4)
x = PAD
for f, spec in zip(fitted, p["images"]):
img.paste(f, (x, y + 4))
if spec.get("cap"):
d.text((x + 6, y + 4 + f.height + 4), spec["cap"], font=font(30, True), fill=(30, 30, 30))
x += cw + gap
y += ph + capsp + 18
y += 6
for lab, lines in rows:
h = max(46, 42 * len(lines))
d.rounded_rectangle([PAD - 6, y, PAD + 180, y + 44], radius=8, fill=(232, 236, 244))
d.text((PAD + 87, y + 22), lab, font=font(30, True), fill=(30, 30, 40), anchor="mm")
for i, ln in enumerate(lines):
d.text((PAD + 210, y + 4 + 42 * i), ln, font=fr, fill=(25, 25, 25))
y += h + 12
os.makedirs(os.path.dirname(cfg["out"]), exist_ok=True)
img.save(cfg["out"])
return img.size
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Loaded 100 of 3978 files, more files were not shown because too many files have changed in this diff. Show more