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>
No files matched your search
@@ -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`에 남겨 다음 세션이 재발견하지 않게 합니다.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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()))
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 42 KiB |
|
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))]
|
||||
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 375 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 120 KiB |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
.venv/
|
||||
@@ -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)
|
||||
@@ -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\` 에 있습니다.
|
||||
@@ -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)를 어떻게 일반화할지 정해야 합니다.
|
||||
@@ -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`)
|
||||
@@ -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 폴더에서) |
|
||||
@@ -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에서 확인 필요)
|
||||
@@ -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시간 | 대기 (레이어·형식 코드 일반화 결정 필요) |
|
||||
@@ -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])
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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),
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
"""테스트에서 scripts/ 폴더의 모듈을 불러올 수 있게 경로를 추가합니다."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
|
||||
@@ -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)
|
||||
@@ -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}
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
@@ -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 드라이브에 저장합니다.
|
||||
@@ -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-루프로 대기.
|
||||
@@ -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()}
|
||||
@@ -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()}
|
||||
@@ -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()}
|
||||
@@ -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)"
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 45 KiB |