feat: read concrete-lining drawings 04-004..009 and link lining spec to model (Task 6)

Extracted per-sheet pattern groups (입출구부 50 m / 온도미고려), note-5 scope,
fck 27 MPa, SD400, aggregate 25 mm, rebar label counts and printed radii.
Model keeps R1 = 6,982 mm; lining sheets print 6.892 m -> [CONFLICT DETECTED]
for user check. Sheet-to-station and pattern-name mapping left [미확정].
Viewer shows the lining spec on click.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5Cun3RHQKna9qxE6ybZ4A
This commit is contained in:
Claude committed 2026-10-21 06:30:45 +00:00
1 parent 6c7e555c25
commit fe6fe26655
8 files changed
+9593 -4

No files matched your search

+8 -1
View File
@@ -191,7 +191,14 @@ meta = {
"pattern_status": "PLACEHOLDER: 2 m 임의 배정. 실제 측점 구간 아님 (사용자 결정 2026-10-21). data/pattern_zones_placeholder.csv 를 수정 후 재빌드",
"patterns": [{"from": a, "to": b, "pattern": pat} for a, b, pat, _ in ZONES],
"open": ["4+700~4+745 and 5+480~5+500 excluded ([미확정])", "pattern zones (03-003~008) pending Task 5",
"lining details (04-004~009) pending Task 6"]
"lining reference recorded (meta.lining_reference, 04-004~009); sheet-to-station mapping [미확정]",
"[CONFLICT DETECTED] lining sheets print R1=6.892 m, standard section R1=6,982 mm (model keeps 6.982) - user check"]
}
_ls = json.load(open(f"{ROOT}/data/lining_spec.json", encoding="utf-8"))
meta["lining_reference"] = {
"source": _ls["source"], "concrete": "fck 27 MPa", "rebar": "SD400 (fy 400 MPa)", "aggregate_mm": 25,
"summary": "C0060304-004~009 | fck 27 MPa | SD400 | 입출구부 50 m 취약구간 철근 상세 (측점 대응 미확정)",
"sheets": [{"sheet": r["file"][:14], "groups": [[g["group"], g["kind"]] for g in r["pattern_details"]]} for r in _ls["sheets"]]
}
json.dump(meta, open(f"{ROOT}/model/tunnel_skeleton_meta.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
print(json.dumps(checks, ensure_ascii=False, indent=1))
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Explore the concrete-lining structure drawings (04-004 .. 04-009).
Read-only on the source RAG folder. For each DXF prints layers with entity
counts, INSERT block names, dimension measurements and every text string with
its position, and writes a JSON dump for later use.
Usage: explore_lining.py <RAG_04_DIR> <OUT_JSON> [max_texts_per_file]
"""
import collections
import glob
import json
import os
import sys
import ezdxf
from ezdxf import recover
RAG_04 = sys.argv[1]
OUT_JSON = sys.argv[2]
MAX_TEXT = int(sys.argv[3]) if len(sys.argv) > 3 else 400
def load(path):
try:
return ezdxf.readfile(path), None
except Exception as exc:
try:
doc, _ = recover.readfile(path)
return doc, f"recovered after strict error: {type(exc).__name__}"
except Exception as exc2:
return None, f"{type(exc).__name__} | recover: {type(exc2).__name__}: {exc2}"
def explore(path):
rec = {"file": os.path.basename(path)}
doc, note = load(path)
if doc is None:
rec["error"] = note
return rec
rec["note"] = note
msp = doc.modelspace()
layers = collections.Counter()
counts = collections.Counter()
for e in msp:
counts[e.dxftype()] += 1
layers[e.dxf.layer] += 1
rec["layers"] = dict(layers.most_common())
rec["entity_counts"] = dict(counts.most_common())
rec["inserts"] = dict(collections.Counter(e.dxf.name for e in msp if e.dxftype() == "INSERT").most_common(30))
dims, texts = [], []
for e in msp:
t = e.dxftype()
if t == "DIMENSION":
try:
val = round(float(e.get_measurement()), 3)
except Exception:
val = None
dims.append({"layer": e.dxf.layer, "value": val, "text": e.dxf.get("text", "")})
elif t in ("TEXT", "MTEXT"):
s = e.dxf.text if t == "TEXT" else e.plain_text()
if s and s.strip():
pos = e.dxf.insert
texts.append({"layer": e.dxf.layer, "x": round(pos.x, 2), "y": round(pos.y, 2),
"text": s.strip().replace("\n", " | ")[:200]})
rec["dimensions"] = dims[:200]
rec["texts_total"] = len(texts)
rec["texts"] = texts[:MAX_TEXT]
return rec
def main():
files = sorted(glob.glob(os.path.join(RAG_04, "C0060304-00[4-9]*.dxf")))
out = [explore(f) for f in files]
with open(OUT_JSON, "w", encoding="utf-8") as fh:
json.dump(out, fh, ensure_ascii=False, indent=1)
for r in out:
print("==", r["file"])
if "error" in r:
print(" ERROR", r["error"])
continue
print(" note:", r["note"])
print(" entities:", r["entity_counts"])
print(" layers:", dict(list(r["layers"].items())[:25]))
print(" inserts:", r["inserts"])
print(" dims:", len(r["dimensions"]), "texts:", r["texts_total"])
if __name__ == "__main__":
main()
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Extract structured lining-reinforcement data from the concrete-lining
structure drawings 04-004..009 (read-only on RAG).
Per sheet: pattern groups and their detail kind (입출구부 50 m / 온도미고려),
note-5 scope sentence, concrete and rebar grade, rebar sizes present,
radius and lap dimensions as printed, EL datum texts.
Output: data/lining_spec.json (+ printed summary).
Usage: extract_lining_spec.py <RAG_04_DIR> <OUT_JSON>
"""
import glob
import json
import os
import re
import sys
import ezdxf
from ezdxf import recover
RAG_04, OUT = sys.argv[1], sys.argv[2]
BAR_RE = re.compile(r"^0?H(\d{2})$")
PAT_RE = re.compile(r"\(([^()]*?)\s*패턴\)")
GROUP_RE = re.compile(r"주철근 조립도\(([^()]*?)\s*패턴\)")
def load(path):
try:
return ezdxf.readfile(path)
except Exception:
doc, _ = recover.readfile(path)
return doc
def texts_of(doc):
out = []
for e in doc.modelspace():
if e.dxftype() == "TEXT":
s = e.dxf.text
elif e.dxftype() == "MTEXT":
s = e.plain_text()
else:
continue
if s and s.strip():
out.append((e.dxf.layer, round(e.dxf.insert.x, 1), round(e.dxf.insert.y, 1), s.strip()))
return out
def sheet_no(name):
return int(re.search(r"C0060304-(\d{3})", name).group(1))
def main():
files = sorted(glob.glob(os.path.join(RAG_04, "C0060304-00[4-9]*.dxf")))
sheets = []
for f in files:
doc = load(f)
tx = texts_of(doc)
rec = {"sheet": sheet_no(f), "file": os.path.basename(f)}
# pattern groups: detail kind from the text itself
groups = []
for layer, x, y, s in tx:
if layer != "CS-TABL-TEX1":
continue
if "패턴" in s:
kind = "입출구부 50m" if "입출구부" in s else ("온도미고려" if "온도미고려" in s else "기타")
m = PAT_RE.search(s)
groups.append({"group": m.group(1) if m else None, "kind": kind, "raw": s})
rec["pattern_details"] = groups
# scope note 5 (full line, joined with the continuation line)
note5 = [s for _, _, _, s in tx if s.startswith("5. 본 콘크리트 라이닝 구조도")]
cont = [s for _, _, _, s in tx if s.startswith("설계조건과 상이할 경우")]
rec["note5"] = (note5[0] + " " + (cont[0] if cont else "")).strip() if note5 else None
rec["concrete"] = sorted({s for _, _, _, s in tx if "fck" in s})
rec["rebar_grade"] = sorted({s for _, _, _, s in tx if "fy=" in s})
rec["aggregate"] = sorted({s for _, _, _, s in tx if "최대치수" in s})
bars = {}
for layer, x, y, s in tx:
m = BAR_RE.match(s)
if m:
bars[f"H{m.group(1)}"] = bars.get(f"H{m.group(1)}", 0) + 1
rec["rebar_sizes_label_count"] = dict(sorted(bars.items()))
rec["radius_dims"] = sorted({s for layer, _, _, s in tx if layer == "CS-DIMT" and re.match(r"R\d?'?=", s)})
rec["lap_dims"] = sorted({s for layer, _, _, s in tx if layer == "CS-DIMT" and "mm" in s and "이음장" not in s and "600" in s})
rec["el_texts"] = sorted({s for _, _, _, s in tx if s.startswith("EL")})
rec["entrance_exit_50m"] = any("입출구부 50m" in s for _, _, _, s in tx)
sheets.append(rec)
with open(OUT, "w", encoding="utf-8") as fh:
json.dump({"source": "C0060304-004..009 (콘크리트라이닝구조도)", "sheets": sheets},
fh, ensure_ascii=False, indent=1)
for r in sheets:
print("==", r["sheet"], "| groups:", [(g["group"], g["kind"]) for g in r["pattern_details"]])
print(" note5:", (r["note5"] or "")[:140])
print(" concrete:", r["concrete"], "| rebar:", r["rebar_grade"], "| agg:", r["aggregate"])
print(" bars:", r["rebar_sizes_label_count"], "| R:", r["radius_dims"], "| EL:", r["el_texts"])
if __name__ == "__main__":
main()
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Render the lining-reinforcement reference report (DOC/03_설계) from
data/lining_spec.json written by extract_lining_spec.py. Values are read from
the JSON, not typed. Usage: make_lining_doc.py <lining_spec.json> <out.md>"""
import json
import sys
SRC, OUT = sys.argv[1], sys.argv[2]
d = json.load(open(SRC, encoding="utf-8"))
S = d["sheets"]
note5 = S[0]["note5"]
conc = S[0]["concrete"][0].split("=")[-1]
rebar = S[0]["rebar_grade"][0].split("=")[-1]
agg = S[0]["aggregate"][0].split("Ø")[-1]
r1_drawn = next(x for x in S[0]["radius_dims"] if x.startswith("R1="))
r1_mm = float(r1_drawn.split("=")[1])
lines = ["# 콘크리트 라이닝 구조도 판독 및 재료 사양 (초안)\n",
"| 버전 | 일자 | 변경 내용 |", "|---|---|---|",
"| v0.1.00 | 2026-10-21 | 최초 작성. 콘크리트라이닝구조도 C0060304-004~009 판독 (HITL 검토 대상) |\n",
"## 1. 개요",
"- 6장은 **지보패턴 입출구부 취약구간**의 철근 상세와 공통 사양이다. 도면에 측점(STA) 표기가 없어 어느 측점 구간에 적용되는지는 판독되지 않았다 ([미확정]).",
"- 적용 범위(주기 5, 원문): " + note5 + "\n",
"## 2. 시트별 패턴군",
"| 도면 | 패턴군 | 상세 종류 | 철근 라벨(H13~H29 개수) |",
"|---|---|---|---|"]
for r in S:
bars = ", ".join(f"{k} {v}" for k, v in r["rebar_sizes_label_count"].items())
for i, g in enumerate(r["pattern_details"]):
lab = bars if i == 0 else ""
lines.append(f"| {r['file'][:14]} | {g['group']} | {g['kind']} | {lab} |")
lines += ["",
"- 철근 라벨 개수는 도면 시트 단위 합계다 (패턴군별 분리 불가). 첫 행에만 표기했다.\n",
"## 3. 공통 재료 사양 (6장 동일)",
f"- 콘크리트: {conc} ({S[0]['concrete'][0]})",
f"- 철근: {rebar} ({S[0]['rebar_grade'][0]})",
f"- 굵은골재 최대치수: Ø{agg} mm",
"- 설계방법: 강도설계법, 피복두께 6 cm 이상 확보 (주기 1)",
"- 이음장 600 mm (도면 치수), 겹침이음 1 cycle (도면 상세)",
"- 라이닝 뒷면 주입재 강도 1.0 MPa 기대 (주기 11)",
"- 천정부 방수쉬트 부착형 앵커, 철근처짐방지 앵커 적용 (주기 4, 12)\n",
"## 4. 기준선과 치수",
f"- 도면 표기 R1 = {r1_drawn} (6장 모두 동일 표기)",
"- 표준단면 치수표(설계_표준단면치수표) R1 = 6,982 mm, 골격 모델은 6,982 mm 사용",
f"- 라이닝 외면 R 7.282 m 에서 내면 R 6.982 m 를 빼면 두께 0.300 m, 표기 6.892 m 를 쓰면 0.390 m 이다. 표준단면 두께 300 mm 와 맞는 값은 6.982 m 이다.",
"- R2 = 3,712 mm, R2′ = 3,910 mm (도면 표기, 표준단면 치수표와 일치)",
"- EL. 0.000 = 노면 기준 (계획고 F = 0.000 과 일치 기록)",
"- EL. -0.367 = 아치 중심 높이 (골격의 중심 c = -0.367 m 와 일치)\n",
"## 5. 충돌·미확정",
"- [CONFLICT DETECTED] 도면 R1 6.892 m 와 표준단면 R1 6.982 m 가 다르다. 골격은 6.982 m 를 유지하고 확인을 요청한다.",
"- [미확정] 시트별 패턴군이 어느 측점 구간인지 (도면에 측점 없음)",
"- [미확정] 라이닝 패턴군(A~C-2, D-1~2, E-1~2, WL-1~4, RP-1~2, P-2~3)과 굴착 순서도 패턴명(NP-4AD-1-D-2 등)의 대응 관계",
"- 재료표 수량(총 중량)은 이번 판독에서 추출하지 않았다. 필요하면 재료표 추출을 별도 Task로 한다.",
"- 철근 배치는 골격 LOD 범위 밖이라 모델에 넣지 않았다 (사용자 승인 3D 상세 수준: 축선·표준단면 스윕 골격).",
"- 설계 조건과 상이하면 구조해석 검토 후 철근 보강 가능 (주기 5, 시공 중 판단 대상)\n",
"## 6. 확인 요청 (HITL)",
"1. R1 6.892 m 와 6.982 m 중 설계 기준값은 무엇인지",
"2. 라이닝 패턴군과 굴착 패턴명의 대응표가 있는지",
"3. 측점별 지보패턴 구간표가 확보되면 그 값으로 placeholder 구간을 교체할지",
""]
open(OUT, "w", encoding="utf-8").write("\n".join(lines))
print("written", OUT, len(lines), "lines")