- 정리는 코치 세션이 저장소 밖 도구로 수행, 하이쿠 세션이 커밋. - 옛 81커밋은 로컬에만 보존. 기록은 evidence/gitlog_v1.txt. - F13 기준 변경: .gitignore 에 private/*.dwg·private/full.dxf 제외 (SPEC 34장 끝 문장). - check.sh 결과: 결과: PASS 151 / FAIL 0 / SKIP 3 (검사 범위 F01~F46) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
1181 lines
56 KiB
Python
1181 lines
56 KiB
Python
#!/usr/bin/env python3
|
||
# extract_from_dxf.py — 도면 DXF 에서 모델 데이터를 뽑는다 (표준 라이브러리만 사용, SPEC 20.1)
|
||
# 사용: python3 tools/extract_from_dxf.py <도면.dxf> model_extracted.js
|
||
# 단위: DXF 의 1 = 1 m → 출력은 mm. 좌표·정답 값은 코드에 두지 않고 도면에서 찾는다.
|
||
import json
|
||
import math
|
||
import os
|
||
import re
|
||
import sys
|
||
from collections import Counter
|
||
|
||
# SPEC 규칙의 상수 (도면 좌표가 아니다)
|
||
UNIT = 1000.0 # 1 m = 1000 mm
|
||
WALL_MIN_LEN = 60.0 # 전체 단면 벽: 길이 60 m 넘는 세로 선분
|
||
SLAB_THICK = 0.3 # 슬래브 두께 0.3 m
|
||
EPS = 0.01 # 좌표 허용 오차 (도면 단위)
|
||
NEAR = 0.2 # 「근처」 허용
|
||
R_IN = 4.85 # 평면 안쪽 원 반지름
|
||
R_OUT = 5.5 # 평면 바깥 원 반지름
|
||
RAIL_H = 1100 # 난간 높이 mm
|
||
WALL_COLOR = "#8a9bb0"
|
||
SLAB_COLOR = "#c8c8c8"
|
||
RAIL_COLOR = "#d8c890"
|
||
GREEN = 3 # 지반선 색
|
||
RAIL_PEN = 2 # 난간 색 (노랑 계열 ACI 2)
|
||
STAIR_PEN = 8 # 단면 계단 윤곽 색 (ACI 8)
|
||
STAIR_MIN_VERTS = 6 # 단면 계단 윤곽 꼭짓점 하한
|
||
STAIR_HOLE_MIN = 4000 # 계단 구멍: 폭 4 m 넘는 것 (mm)
|
||
STAIR_RUNS = 18 # 계단 런 수
|
||
STAIR_COLOR = "#7fb07f"
|
||
TUNNEL_COLOR = "#b08a5a"
|
||
TUNNEL_EXT = 1.0 # 단면 터널 선: 벽 바깥으로 1 m 넘게 뻗는다
|
||
TUNNEL_SLOPE = 0.05 # 단면 터널 선: 기울기 5% 미만
|
||
TUNNEL_PLAN_LAYER = "-평면계획"
|
||
TUNNEL_PLAN_PEN = 8 # 평면 터널 선 색 (ACI 8)
|
||
TUNNEL_PLAN_MIN = 4.8 # 평면 선분 두 끝의 중심 거리 (m)
|
||
TUNNEL_PLAN_MAX = 8.0
|
||
TUNNEL_LEN_EXTRA = 20000 # 벽 바깥으로 20 m
|
||
GRAVEL_T = 125 # 잡석 두께 mm
|
||
PAD_TOP_T = 50 # 패드 1단 두께 mm
|
||
PAD_TOP_HALF = 400 # 패드 1단 폭 800 의 절반 mm
|
||
Z_TOL = 5 # 높이 비교 허용 오차 (mm)
|
||
HOLE_LAYERS = ("오프닝", "구조물 계단")
|
||
RAIL_LAYER = "계단"
|
||
WALL_LAYER = "구조물"
|
||
TITLE_LAYER = "S-TITLE"
|
||
PLAN_TITLES = {"중간슬래브(홀수층)": "odd", "중간슬래브(짝수층)": "even",
|
||
"중간슬래브(18B)": "18B", "중간슬래브(19B)": "19B", "하부슬래브(20B)": "20B"}
|
||
FRAME_TITLE = "평면도" # 평면도 1 글자 (공백 무시)
|
||
VENT_LAYER = "환기구" # 환기구 중심선 레이어
|
||
PLAN_LEG_PEN = 1 # 평면도 다리 꺾은선 색 (ACI 1)
|
||
PARALLEL_RAD = 0.01 # 다리와 나란함 허용 (rad)
|
||
R_TOL = 0.02 # 다리에서 r_out 떨어짐 허용 (m)
|
||
BRANCH_COLOR = "#9fb8c8"
|
||
VENT_COLOR = "#b8c8a0"
|
||
BRANCH_S_MIN = 4.55 # 분기구 윤곽: 다리 방향 s 가 이 값(m)보다 커야 한다
|
||
BRANCH_T_MAX = 2.0 # 분기구 윤곽: |t| 가 이 값(m)보다 작아야 한다
|
||
BRANCH_END_EXTRA = 0.5 # 분기구 윤곽: s 는 다리 길이 + 0.5 m 보다 작다
|
||
X_MIN = 1.0 # 벽 개구부 X 사선: 길이 1 m 넘는 것
|
||
X_DEPTH = 10.0 # 벽 개구부 X 사선: 지표 아래 10 m 안
|
||
HAUNCH_MIN = 0.1 # 헌치 사선 길이 하한 (m)
|
||
HAUNCH_MAX = 0.5 # 헌치 사선 길이 상한 (m)
|
||
DUCT_NEAR = 3.0 # 덕트 벽선: 가운데 꼭짓점이 V1 에서 3 m 안
|
||
GROUND_Z = 0 # 지표 높이 (mm)
|
||
TOP_COLOR = "#c8c8c8" # 상부 슬래브 색
|
||
NECK_COLOR = "#b8b8b8" # 맨홀 목 색
|
||
PAD_COLOR = "#e0b060" # 19B 패드 색
|
||
BOTTOM_COLOR = "#a0a0a0" # 하부 슬래브 색
|
||
LEAN_COLOR = "#707070" # 바닥 콘크리트 색
|
||
WEIR_COLOR = "#9a9a9a" # 월류벽 색
|
||
PIT_PUMP_COLOR = "#8fc3e0" # 집수조 색
|
||
PIT_TBM_COLOR = "#a8d8f0" # 침사조 색
|
||
PIT_ALPHA = 0.25
|
||
HOLE_R_MAX = 1.0 # 맨홀 구멍 원: 반지름 1 m 미만 (m)
|
||
TOP_SPAN_FRAC = 0.9 # 상부 슬래브 밑면 수평선: 안쪽 지름의 0.9 배 넘는 길이
|
||
FLOOR_HAUNCH = (0.1, 0.6) # 하부 슬래브 윗면 사선 길이 범위 (m)
|
||
PAD_LEN_MIN = 9.0 # 패드 윗면 수평선 길이 하한 (m)
|
||
X_MARK_MIN = 0.5 # X 표시 사선 길이 하한 (m)
|
||
X_MARK_LAYERS = ("오프닝", "구조물")
|
||
WEIR_WALL_MIN = 5.0 # 월류벽 세로 선 길이 하한 (m, 평면)
|
||
SECTION_WEIR_MIN = 1.0 # 월류벽 세로 선 길이 하한 (m, 단면)
|
||
WEIR_SIDE = 0.05 # 안쪽 벽선에서 이만큼 떨어진 선만 (m)
|
||
PIT_LABEL = "집수조" # 단면 글자: 왼쪽이면 왼쪽 칸이 펌프 쪽
|
||
|
||
|
||
def load_dxf(path):
|
||
# DXF 는 「그룹 코드 줄 + 값 줄」이 번갈아 나오는 텍스트다
|
||
with open(path, encoding="utf-8", errors="replace") as f:
|
||
lines = f.read().split("\n")
|
||
pairs = [(lines[i].strip(), lines[i + 1].strip()) for i in range(0, len(lines) - 1, 2)]
|
||
|
||
# 레이어 색 표 (TABLES 의 LAYER)
|
||
layer_color = {}
|
||
in_layer = False
|
||
cur_name = None
|
||
for code, val in pairs:
|
||
if code == "0" and val == "LAYER":
|
||
in_layer = True
|
||
cur_name = None
|
||
elif code == "2" and in_layer and cur_name is None:
|
||
cur_name = val
|
||
elif code == "62" and in_layer and cur_name is not None:
|
||
layer_color[cur_name] = abs(int(val))
|
||
cur_name = None
|
||
in_layer = False
|
||
elif code == "0" and val == "ENDTAB":
|
||
in_layer = False
|
||
|
||
# ENTITIES 구역의 객체를 묶는다
|
||
start = next(i for i, (c, v) in enumerate(pairs) if c == "2" and v == "ENTITIES")
|
||
end = next(i for i, (c, v) in enumerate(pairs) if i > start and c == "0" and v == "ENDSEC")
|
||
raw = []
|
||
for code, val in pairs[start + 1:end]:
|
||
if code == "0":
|
||
raw.append({"type": val, "codes": []})
|
||
else:
|
||
raw[-1]["codes"].append((code, val))
|
||
|
||
ents = []
|
||
poly_head = None
|
||
for r in raw:
|
||
t = r["type"]
|
||
codes = r["codes"]
|
||
if t == "VERTEX" and poly_head is not None:
|
||
x = next(float(v) for c, v in codes if c == "10")
|
||
y = next(float(v) for c, v in codes if c == "20")
|
||
poly_head["pts"].append((x, y))
|
||
continue
|
||
if t == "SEQEND":
|
||
poly_head = None
|
||
continue
|
||
layer = next((v for c, v in codes if c == "8"), "0")
|
||
color = next((int(v) for c, v in codes if c == "62"), None)
|
||
if color in (None, 0, 256): # 없거나 「레이어 따름」이면 레이어 색
|
||
color = layer_color.get(layer)
|
||
e = {"type": t, "layer": layer, "color": color, "pts": [], "closed": False}
|
||
if t == "LINE":
|
||
vals = {c: float(v) for c, v in codes if c in ("10", "20", "11", "21")}
|
||
e["pts"] = [(vals["10"], vals["20"]), (vals["11"], vals["21"])]
|
||
elif t == "LWPOLYLINE":
|
||
x = None
|
||
for c, v in codes:
|
||
if c == "10":
|
||
x = float(v)
|
||
elif c == "20" and x is not None:
|
||
e["pts"].append((x, float(v)))
|
||
x = None
|
||
elif c == "70":
|
||
e["closed"] = bool(int(v) & 1)
|
||
elif t == "POLYLINE":
|
||
for c, v in codes:
|
||
if c == "70":
|
||
e["closed"] = bool(int(v) & 1)
|
||
poly_head = e
|
||
elif t == "CIRCLE":
|
||
e["center"] = (float(next(v for c, v in codes if c == "10")), float(next(v for c, v in codes if c == "20")))
|
||
e["r"] = float(next(v for c, v in codes if c == "40"))
|
||
elif t == "TEXT":
|
||
e["text"] = next((v for c, v in codes if c == "1"), "")
|
||
e["pts"] = [(float(next(v for c, v in codes if c == "10")), float(next(v for c, v in codes if c == "20")))]
|
||
else:
|
||
continue
|
||
ents.append(e)
|
||
return ents
|
||
|
||
|
||
def segments_of(ents, layer=None):
|
||
# 선분 목록: LINE 과 꼭짓점 두 개짜리 꺾은선(POLYLINE·LWPOLYLINE 의 이웃 점) 전부
|
||
out = []
|
||
for e in ents:
|
||
if layer is not None and e["layer"] != layer:
|
||
continue
|
||
if e["type"] == "LINE":
|
||
out.append((e["pts"][0], e["pts"][1], e))
|
||
elif e["type"] in ("POLYLINE", "LWPOLYLINE") and len(e["pts"]) >= 2:
|
||
for a, b in zip(e["pts"], e["pts"][1:]):
|
||
out.append((a, b, e))
|
||
return out
|
||
|
||
|
||
def section_walls(ents, start=0):
|
||
# 1. 전체 단면: 구조물 세로 선분 중 길이 60 m 넘는 것, X 가 작은 쪽부터 네 선 (start=4 면 B-B, 5~8번째)
|
||
tall = []
|
||
for a, b, _ in segments_of(ents, WALL_LAYER):
|
||
if abs(a[0] - b[0]) <= EPS and abs(a[1] - b[1]) > WALL_MIN_LEN:
|
||
tall.append(a[0])
|
||
xs = sorted(tall)[start:start + 4]
|
||
if len(xs) < 4:
|
||
raise SystemExit("전체 단면 벽 선 네 개를 찾지 못했다")
|
||
outer_l, inner_l, inner_r, outer_r = xs
|
||
return {
|
||
"r_out": (outer_r - outer_l) / 2 * UNIT,
|
||
"r_in": (inner_r - inner_l) / 2 * UNIT,
|
||
"axis": (outer_l + outer_r) / 2,
|
||
"inner_l": inner_l,
|
||
"inner_r": inner_r,
|
||
"outer_l": outer_l,
|
||
"outer_r": outer_r,
|
||
}
|
||
|
||
|
||
def ground_y(ents, axis, tol=EPS):
|
||
# 2. 지표: 녹색 선 중 단면 축 X 를 지나는 것, 그 X 에서의 Y (선이 살짝 기울어도 된다)
|
||
for a, b, e in segments_of(ents):
|
||
if e["color"] != GREEN or abs(a[0] - b[0]) <= EPS:
|
||
continue
|
||
lo, hi = min(a[0], b[0]), max(a[0], b[0])
|
||
if lo - tol <= axis <= hi + tol:
|
||
t = (axis - a[0]) / (b[0] - a[0])
|
||
return a[1] + t * (b[1] - a[1])
|
||
raise SystemExit("지표선을 찾지 못했다")
|
||
|
||
|
||
def slab_tops(ents, walls, gy):
|
||
# 3. 슬래브 윗면: 왼쪽 안쪽 벽에서 시작하는 수평선 S, 0.3 아래에 같은 시작점 근처의 선 T 가 있는 것
|
||
hs = []
|
||
for a, b, _ in segments_of(ents, WALL_LAYER):
|
||
if abs(a[1] - b[1]) > EPS:
|
||
continue
|
||
left = a if a[0] < b[0] else b
|
||
hs.append((left, max(a[0], b[0])))
|
||
tops = []
|
||
for left, _ in hs:
|
||
if abs(left[0] - walls["inner_l"]) > EPS:
|
||
continue
|
||
below = [h for h, _ in hs if abs(h[1] - (left[1] - SLAB_THICK)) <= EPS and abs(h[0] - walls["inner_l"]) <= NEAR]
|
||
if below:
|
||
tops.append(left[1])
|
||
tops = sorted(set(round(y, 4) for y in tops), reverse=True)
|
||
if len(tops) != 19:
|
||
raise SystemExit(f"슬래브 윗면을 19개 찾지 못했다 ({len(tops)}개)")
|
||
return [(y - gy) * UNIT for y in tops]
|
||
|
||
|
||
def find_slab_plans(ents):
|
||
# 4. 슬래브 평면: 글자 바로 아래에서 가장 가까운, 반지름 4.85 와 5.5 원이 겹친 중심
|
||
circles = [e for e in ents if e["type"] == "CIRCLE"]
|
||
centers = []
|
||
for c in circles:
|
||
if abs(c["r"] - R_OUT) <= EPS:
|
||
if any(abs(d["r"] - R_IN) <= EPS and math.dist(c["center"], d["center"]) <= EPS for d in circles):
|
||
centers.append(c["center"])
|
||
plans = {}
|
||
for t in ents:
|
||
if t["type"] != "TEXT" or t["layer"] != TITLE_LAYER:
|
||
continue
|
||
key = PLAN_TITLES.get(t["text"].replace(" ", ""))
|
||
if key is None:
|
||
continue
|
||
tx, ty = t["pts"][0]
|
||
below = [c for c in centers if c[1] < ty]
|
||
if below:
|
||
plans[key] = min(below, key=lambda c: math.dist(c, (tx, ty)))
|
||
if len(plans) != 5:
|
||
raise SystemExit("슬래브 평면 다섯 개를 찾지 못했다")
|
||
return plans
|
||
|
||
|
||
def inside_plan(e, center):
|
||
# 평면 안쪽 원 안에 있는 객체: 점들의 무게중심이 안쪽 원 안
|
||
xs = [p[0] for p in e["pts"]]
|
||
ys = [p[1] for p in e["pts"]]
|
||
gx, gy = sum(xs) / len(xs), sum(ys) / len(ys)
|
||
return math.dist((gx, gy), center) <= R_IN
|
||
|
||
|
||
def axis_aligned(a, b):
|
||
return abs(a[0] - b[0]) <= EPS or abs(a[1] - b[1]) <= EPS
|
||
|
||
|
||
def same_pt(p, q):
|
||
return abs(p[0] - q[0]) <= EPS and abs(p[1] - q[1]) <= EPS
|
||
|
||
|
||
def close_open_triangle(e, lines):
|
||
# 나: 꼭짓점 3개 열린 꺾은선의 끝점 B 에서 수평·수직 선을 차례로 이어 끝점 A 에 닿으면 후보
|
||
a, m, b = e["pts"]
|
||
frontier = [(b, [])]
|
||
seen = []
|
||
while frontier:
|
||
here, trail = frontier.pop(0)
|
||
if len(trail) >= 2:
|
||
continue
|
||
for p, q, _ in lines:
|
||
if not axis_aligned(p, q):
|
||
continue
|
||
if same_pt(p, here):
|
||
nxt = q
|
||
elif same_pt(q, here):
|
||
nxt = p
|
||
else:
|
||
continue
|
||
if same_pt(nxt, a):
|
||
return [a, m, b] + trail
|
||
if any(same_pt(nxt, s) for s in seen):
|
||
continue
|
||
seen.append(nxt)
|
||
frontier.append((nxt, trail + [nxt]))
|
||
return None
|
||
|
||
|
||
def same_shape(p, q):
|
||
return sorted((round(x, 1), round(y, 1)) for x, y in p) == sorted((round(x, 1), round(y, 1)) for x, y in q)
|
||
|
||
|
||
def area(poly):
|
||
s = 0.0
|
||
for i in range(len(poly)):
|
||
x1, y1 = poly[i]
|
||
x2, y2 = poly[(i + 1) % len(poly)]
|
||
s += x1 * y2 - x2 * y1
|
||
return abs(s) / 2
|
||
|
||
|
||
def centroid(poly):
|
||
return (sum(p[0] for p in poly) / len(poly), sum(p[1] for p in poly) / len(poly))
|
||
|
||
|
||
def point_in_poly(pt, poly):
|
||
x, y = pt
|
||
inside = False
|
||
for i in range(len(poly)):
|
||
x1, y1 = poly[i]
|
||
x2, y2 = poly[(i + 1) % len(poly)]
|
||
if (y1 > y) != (y2 > y) and x < (x2 - x1) * (y - y1) / (y2 - y1) + x1:
|
||
inside = not inside
|
||
return inside
|
||
|
||
|
||
def plan_holes(ents, center):
|
||
# 5. 구멍: 평면 안쪽 원 안의 오프닝·구조물 계단 객체
|
||
lines = [(a, b, e) for a, b, e in segments_of(ents) if e["type"] == "LINE" and e["layer"] in HOLE_LAYERS]
|
||
cands = []
|
||
for e in ents:
|
||
if e["layer"] not in HOLE_LAYERS or e["type"] not in ("LWPOLYLINE", "POLYLINE") or not inside_plan(e, center):
|
||
continue
|
||
n = len(e["pts"])
|
||
if n >= 4:
|
||
cands.append(e["pts"])
|
||
elif n == 3 and not e["closed"]:
|
||
poly = close_open_triangle(e, [(a, b, x) for a, b, x in lines if x["layer"] == e["layer"]])
|
||
if poly:
|
||
cands.append(poly)
|
||
uniq = []
|
||
for c in cands:
|
||
if not any(same_shape(c, u) for u in uniq):
|
||
uniq.append(c)
|
||
# 다른 후보를 감싸는 후보(난간 윤곽)는 뺀다: 더 크고, 다른 후보의 무게중심을 품은 것
|
||
keep = []
|
||
for c in uniq:
|
||
wraps = any(d is not c and area(c) > area(d) and point_in_poly(centroid(d), c) for d in uniq)
|
||
if not wraps:
|
||
keep.append(c)
|
||
return [[[round((x - center[0]) * UNIT), round((y - center[1]) * UNIT)] for x, y in poly] for poly in keep]
|
||
|
||
|
||
def plan_railings(ents, center):
|
||
# 6. 방호 난간: 평면 원 안의 계단 레이어, 색 2, 꼭짓점 3개 이상 꺾은선
|
||
paths = []
|
||
for e in ents:
|
||
if e["layer"] != RAIL_LAYER or e["color"] != RAIL_PEN or e["type"] not in ("LWPOLYLINE", "POLYLINE"):
|
||
continue
|
||
if len(e["pts"]) < 3 or not inside_plan(e, center):
|
||
continue
|
||
pts = list(e["pts"])
|
||
if e["closed"]:
|
||
pts.append(pts[0])
|
||
paths.append([[round((x - center[0]) * UNIT), round((y - center[1]) * UNIT)] for x, y in pts])
|
||
return paths
|
||
|
||
|
||
def section_stairs(ents, walls, gy):
|
||
# 21.1: 단면 계단 윤곽 — 구조물 색 8 의 닫힌 꺾은선, 꼭짓점 6개 이상, 바깥 벽 두 선 사이
|
||
# 돌려주는 값: 윤곽마다 [(x, z)] (mm, x = 단면 축 기준, z = 지표 기준, 위 +)
|
||
out = []
|
||
for e in ents:
|
||
if e["layer"] != WALL_LAYER or e["color"] != STAIR_PEN or e["type"] not in ("LWPOLYLINE", "POLYLINE"):
|
||
continue
|
||
if not e["closed"] or len(e["pts"]) < STAIR_MIN_VERTS:
|
||
continue
|
||
xs = [p[0] for p in e["pts"]]
|
||
if min(xs) < walls["outer_l"] - EPS or max(xs) > walls["outer_r"] + EPS:
|
||
continue
|
||
out.append([((x - walls["axis"]) * UNIT, (y - gy) * UNIT) for x, y in e["pts"]])
|
||
return out
|
||
|
||
|
||
def stair_profile(contours):
|
||
# 21.2: 단 높이 = 세로 변 길이 중 가장 많은 값, 첫 단 = 가장 작은 세로 변, 디딤판 = 가로 변 가장 많은 값
|
||
verts, horiz = [], []
|
||
for poly in contours:
|
||
n = len(poly)
|
||
for i in range(n):
|
||
(x1, z1), (x2, z2) = poly[i], poly[(i + 1) % n]
|
||
length = round(math.dist((x1, z1), (x2, z2)))
|
||
if length == 0:
|
||
continue
|
||
if abs(x1 - x2) <= EPS * UNIT:
|
||
verts.append(round(abs(z1 - z2)))
|
||
elif abs(z1 - z2) <= EPS * UNIT:
|
||
horiz.append(round(abs(x1 - x2)))
|
||
riser = Counter(verts).most_common(1)[0][0]
|
||
tread = Counter(horiz).most_common(1)[0][0]
|
||
return min(verts), riser, tread
|
||
|
||
|
||
def stair_dir(contours, z_low):
|
||
# 21.4: z_low 에 가장 낮은 꼭짓점이 있는 윤곽에서, 그 꼭짓점들이 왼쪽 끝이면 −1, 오른쪽 끝이면 +1
|
||
for poly in contours:
|
||
zmin = min(z for _, z in poly)
|
||
if abs(zmin - z_low) > Z_TOL:
|
||
continue
|
||
lows = [x for x, z in poly if abs(z - zmin) <= Z_TOL]
|
||
mid = (min(x for x, _ in poly) + max(x for x, _ in poly)) / 2
|
||
return -1 if sum(lows) / len(lows) < mid else 1
|
||
return None
|
||
|
||
|
||
def stair_parts(ents, walls, gy, tops, holes_of):
|
||
# 21.3~21.6: 18런. tops[k] = k+1 층 윗면(mm, 1B 가 맨 위). 런 i: z_high = tops[i-1], z_low = tops[i]
|
||
contours = section_stairs(ents, walls, gy)
|
||
first_riser, riser, tread = stair_profile(contours)
|
||
dirs = []
|
||
for i in range(1, STAIR_RUNS + 1):
|
||
found = stair_dir(contours, tops[i])
|
||
# 윤곽이 없는 런(18)은 바로 위 런(i−1)의 반대
|
||
dirs.append(found if found is not None else -dirs[-1])
|
||
parts = []
|
||
for i in range(1, STAIR_RUNS + 1):
|
||
z_high, z_low = round(tops[i - 1]), round(tops[i])
|
||
name = f"{i}B"
|
||
key = name if name in ("18B", "19B") else ("odd" if i % 2 else "even")
|
||
# 21.5: 같은 층 평면의 계단 구멍(폭 4 m 넘는 것)
|
||
hole = next(h for h in holes_of[key] if max(p[0] for p in h) - min(p[0] for p in h) > STAIR_HOLE_MIN)
|
||
d = dirs[i - 1]
|
||
x_top = max(p[0] for p in hole) if d == -1 else min(p[0] for p in hole)
|
||
parts.append({
|
||
"id": f"stair_{i:02d}", "name": f"계단 {i}런 ({name}→{i + 1}B)", "kind": "stair", "type": "stair",
|
||
"color": STAIR_COLOR, "z_low": z_low, "z_high": z_high,
|
||
"risers": round((z_high - z_low - first_riser) / riser + 1), "first_riser": first_riser,
|
||
"riser": riser, "tread": tread, "x_top": x_top, "dir": d,
|
||
"y0": min(p[1] for p in hole), "y1": max(p[1] for p in hole),
|
||
})
|
||
return parts
|
||
|
||
|
||
def section_tunnel(ents, walls, gy):
|
||
# 23.1: 단면 A-A 오른쪽 바깥 벽선(X 최대)에서 바깥으로 1 m 넘게 뻗는 구조물 수평선(기울기 5% 미만)
|
||
# 벽 쪽 끝의 z 다섯 개를 위에서부터 [바깥 위, 안 위, 패드 윗면, 안 아래, 바깥 아래] 로 본다
|
||
zs = []
|
||
for a, b, _ in segments_of(ents, WALL_LAYER):
|
||
if abs(a[0] - b[0]) <= EPS:
|
||
continue
|
||
near, far = (a, b) if a[0] < b[0] else (b, a)
|
||
if abs(near[0] - walls["outer_r"]) > EPS or far[0] - near[0] <= TUNNEL_EXT:
|
||
continue
|
||
if abs(far[1] - near[1]) / (far[0] - near[0]) >= TUNNEL_SLOPE:
|
||
continue
|
||
zs.append((near[1] - gy) * UNIT)
|
||
zs.sort(reverse=True)
|
||
if len(zs) != 5:
|
||
raise SystemExit(f"단면 터널 선을 다섯 개 찾지 못했다 ({len(zs)}개)")
|
||
outer_top, inner_top, pad_top, inner_bot, outer_bot = zs
|
||
return {
|
||
"z": (outer_top + outer_bot) / 2,
|
||
"r_out": (outer_top - outer_bot) / 2,
|
||
"r_in": (inner_top - inner_bot) / 2,
|
||
"pad_z": pad_top,
|
||
}
|
||
|
||
|
||
def plan_tunnel(ents, center):
|
||
# 23.2: 19B 평면 중심에서 두 끝이 4.8~8 m 인 -평면계획 색 8 선분. 방향 = 단위벡터 합, 축 어긋남 = 왼쪽 +(mm)
|
||
segs = []
|
||
for a, b, e in segments_of(ents, TUNNEL_PLAN_LAYER):
|
||
if e["color"] != TUNNEL_PLAN_PEN:
|
||
continue
|
||
da, db = math.dist(a, center), math.dist(b, center)
|
||
if TUNNEL_PLAN_MIN <= da <= TUNNEL_PLAN_MAX and TUNNEL_PLAN_MIN <= db <= TUNNEL_PLAN_MAX:
|
||
segs.append((a, b) if da < db else (b, a))
|
||
if not segs:
|
||
raise SystemExit("19B 평면의 터널 선을 찾지 못했다")
|
||
dx = dy = 0.0
|
||
for near, far in segs:
|
||
length = math.dist(near, far)
|
||
dx += (far[0] - near[0]) / length
|
||
dy += (far[1] - near[1]) / length
|
||
norm = math.hypot(dx, dy)
|
||
ux, uy = dx / norm, dy / norm
|
||
offs = [(near[0] - center[0]) * -uy + (near[1] - center[1]) * ux for near, _ in segs]
|
||
return {"ux": ux, "uy": uy, "angle": math.degrees(math.atan2(uy, ux)) % 360,
|
||
"off": sum(offs) / len(offs) * UNIT}
|
||
|
||
|
||
def tunnel_parts(ents, walls, gy, center):
|
||
# 23.3·23.4: 터널 1개(pipe) + 채움 3개(잡석·패드 2단·패드 1단). 채움은 터널과 같은 축
|
||
sec = section_tunnel(ents, walls, gy)
|
||
pl = plan_tunnel(ents, center)
|
||
ux, uy = pl["ux"], pl["uy"]
|
||
sx = pl["off"] * -uy + walls["r_in"] * ux
|
||
sy = pl["off"] * ux + walls["r_in"] * uy
|
||
length = (walls["outer_r"] - walls["inner_r"]) * UNIT + TUNNEL_LEN_EXTRA
|
||
axis = {"x": round(sx), "y": round(sy), "z": round(sec["z"]), "angle_deg": round(pl["angle"], 2),
|
||
"length": round(length)}
|
||
r_in = sec["r_in"]
|
||
pad_v = sec["pad_z"] - sec["z"]
|
||
tunnel = {"id": "tunnel_TBM", "name": "TBM 터널", "kind": "tunnel", "type": "pipe", "color": TUNNEL_COLOR,
|
||
**axis, "r_out": round(sec["r_out"]), "r_in": round(r_in)}
|
||
fills = [
|
||
("fill_gravel", "터널 잡석", "#7a7a7a", -r_in, -r_in + GRAVEL_T, 0),
|
||
("fill_pad_low", "터널 패드 2단 (아래)", "#c8bfa8", -r_in + GRAVEL_T, pad_v - PAD_TOP_T, 0),
|
||
("fill_pad_high", "터널 패드 1단 (위)", "#e0d8c4", pad_v - PAD_TOP_T, pad_v, PAD_TOP_HALF),
|
||
]
|
||
parts = [tunnel]
|
||
for fid, name, color, v0, v1, half in fills:
|
||
parts.append({"id": fid, "name": name, "kind": "fill", "type": "fill", "color": color, **axis,
|
||
"r": round(r_in), "v0": round(v0), "v1": round(v1), "half_width": half})
|
||
return parts
|
||
|
||
|
||
def plan_center(ents):
|
||
# 24.1-1: 「평면도」 글자 아래에서 가장 가까운 반지름 4.85 원의 중심
|
||
circles = [e["center"] for e in ents if e["type"] == "CIRCLE" and abs(e["r"] - R_IN) <= EPS]
|
||
for t in ents:
|
||
if t["type"] == "TEXT" and t["layer"] == TITLE_LAYER and t["text"].replace(" ", "") == FRAME_TITLE:
|
||
tx, ty = t["pts"][0]
|
||
below = [c for c in circles if c[1] < ty]
|
||
if below:
|
||
return min(below, key=lambda c: math.dist(c, (tx, ty)))
|
||
raise SystemExit("평면도 중심을 찾지 못했다")
|
||
|
||
|
||
def ang_of(p, c):
|
||
return math.degrees(math.atan2(p[1] - c[1], p[0] - c[0]))
|
||
|
||
|
||
def frame_angle(ents, center, r_out_mm, tunnel_angle):
|
||
# 24.1-2~4: 평면도 다리 두 개 중 터널 다리(나란한 색 8 선분 2개 이상, r_out 떨어짐)를 고르고 φ 를 구한다
|
||
legs = []
|
||
for e in ents:
|
||
if e["layer"] == TUNNEL_PLAN_LAYER and e["color"] == PLAN_LEG_PEN and e["type"] in ("LWPOLYLINE", "POLYLINE") \
|
||
and len(e["pts"]) == 3 and same_pt(e["pts"][1], center):
|
||
legs = [e["pts"][0], e["pts"][2]]
|
||
if len(legs) != 2:
|
||
raise SystemExit("평면도 다리 꺾은선을 찾지 못했다")
|
||
r_m = r_out_mm / UNIT
|
||
segs8 = [(a, b) for a, b, e in segments_of(ents, TUNNEL_PLAN_LAYER) if e["color"] == TUNNEL_PLAN_PEN]
|
||
|
||
def parallel_count(leg):
|
||
a = math.radians(ang_of(leg, center))
|
||
ux, uy = math.cos(a), math.sin(a)
|
||
n = 0
|
||
for p, q in segs8:
|
||
dx, dy = q[0] - p[0], q[1] - p[1]
|
||
if abs(dx * uy - dy * ux) / math.hypot(dx, dy) > math.sin(PARALLEL_RAD):
|
||
continue
|
||
dist = abs((p[0] - center[0]) * uy - (p[1] - center[1]) * ux)
|
||
if abs(dist - r_m) <= R_TOL:
|
||
n += 1
|
||
return n
|
||
|
||
tun = [leg for leg in legs if parallel_count(leg) >= 2]
|
||
if len(tun) != 1:
|
||
raise SystemExit("터널 다리를 하나로 고르지 못했다")
|
||
br = next(leg for leg in legs if leg is not tun[0])
|
||
phi = (tunnel_angle - ang_of(tun[0], center)) % 360
|
||
return phi, br
|
||
|
||
|
||
def rot(x, y, deg):
|
||
c, s = math.cos(math.radians(deg)), math.sin(math.radians(deg))
|
||
return x * c - y * s, x * s + y * c
|
||
|
||
|
||
def obox(kind, pid, name, color, cx, cy, ang, s0, s1, t0, t1, z0, z1):
|
||
return {"id": pid, "name": name, "kind": kind, "type": "obox", "color": color,
|
||
"cx": round(cx), "cy": round(cy), "angle_deg": round(ang, 2),
|
||
"s0": round(s0), "s1": round(s1), "t0": round(t0), "t1": round(t1),
|
||
"z0": round(z0), "z1": round(z1)}
|
||
|
||
|
||
def section_heights(ents, walls, gy):
|
||
# 24.2: X 사선이 있는 쪽 = 구조물 쪽. X 의 아래끝 = 바닥 윗면, 위끝 = 지붕 아랫면.
|
||
# 그 쪽 바깥면에서 나간 헌치: 지붕 윗면(내려감), 바닥 아랫면(올라감). 값은 gy 기준 mm
|
||
sides = {-1: (walls["outer_l"], walls["inner_l"]), 1: (walls["outer_r"], walls["inner_r"])}
|
||
diag = [(a, b) for a, b, _ in segments_of(ents, WALL_LAYER)
|
||
if abs(a[0] - b[0]) > EPS and abs(a[1] - b[1]) > EPS and math.dist(a, b) > X_MIN]
|
||
found = {}
|
||
for side, (outer, inner) in sides.items():
|
||
zs = []
|
||
for a, b in diag:
|
||
if not all(-X_DEPTH <= (p[1] - gy) <= 0 for p in (a, b)):
|
||
continue
|
||
if (abs(a[0] - outer) <= EPS and abs(b[0] - inner) <= EPS) or \
|
||
(abs(b[0] - outer) <= EPS and abs(a[0] - inner) <= EPS):
|
||
zs += [(a[1] - gy) * UNIT, (b[1] - gy) * UNIT]
|
||
if zs:
|
||
found[side] = zs
|
||
if len(found) != 1:
|
||
raise SystemExit("벽 개구부 X 를 한 쪽에서 찾지 못했다")
|
||
side, zs = found.popitem()
|
||
floor_top, roof_bottom = min(zs), max(zs)
|
||
outer = sides[side][0]
|
||
roof_tops, floor_bots = [], []
|
||
for a, b, _ in segments_of(ents, WALL_LAYER):
|
||
if abs(a[0] - b[0]) <= EPS or abs(a[1] - b[1]) <= EPS or not HAUNCH_MIN <= math.dist(a, b) <= HAUNCH_MAX:
|
||
continue
|
||
for w, o in ((a, b), (b, a)):
|
||
if abs(w[0] - outer) > EPS or (o[0] - outer) * side <= EPS:
|
||
continue
|
||
wz, oz = (w[1] - gy) * UNIT, (o[1] - gy) * UNIT
|
||
if roof_bottom < oz < wz:
|
||
roof_tops.append(oz)
|
||
elif floor_top > oz > wz:
|
||
floor_bots.append(oz)
|
||
if len(roof_tops) != 1 or len(floor_bots) != 1:
|
||
raise SystemExit("헌치 사선을 지붕·바닥에서 각각 하나 찾지 못했다")
|
||
return {"side": side, "floor_bottom": round(floor_bots[0]), "floor_top": round(floor_top),
|
||
"roof_bottom": round(roof_bottom), "roof_top": round(roof_tops[0])}
|
||
|
||
|
||
def branch_parts(ents, walls, center, phi, leg, h):
|
||
# 24.3: 분기구 다리 방향 s, 왼쪽 수직 t (C 기준 m). 윤곽 두 개 → 안 (t_in, s_in), 바깥 (t_out, s_out)
|
||
a = ang_of(leg, center)
|
||
ua = math.radians(a)
|
||
ux, uy = math.cos(ua), math.sin(ua)
|
||
length = math.dist(leg, center)
|
||
|
||
def st(p):
|
||
dx, dy = p[0] - center[0], p[1] - center[1]
|
||
return dx * ux + dy * uy, -dx * uy + dy * ux
|
||
cands = []
|
||
for e in ents:
|
||
if e["layer"] != WALL_LAYER or e["closed"] or e["type"] not in ("LWPOLYLINE", "POLYLINE") or len(e["pts"]) < 4:
|
||
continue
|
||
sts = [st(p) for p in e["pts"]]
|
||
if all(BRANCH_S_MIN < s < length + BRANCH_END_EXTRA and abs(t) < BRANCH_T_MAX for s, t in sts):
|
||
cands.append(sts)
|
||
if len(cands) != 2:
|
||
raise SystemExit(f"분기구 윤곽 두 개를 찾지 못했다 ({len(cands)}개)")
|
||
inner, outer = sorted(cands, key=lambda sts: max(abs(t) for _, t in sts))
|
||
t_in = max(abs(t) for _, t in inner) * UNIT
|
||
s_in = max(s for s, _ in inner) * UNIT
|
||
t_out = max(abs(t) for _, t in outer) * UNIT
|
||
s_out = max(s for s, _ in outer) * UNIT
|
||
r_in, ang = walls["r_in"], a + phi
|
||
fb, ft, rb, rt = h["floor_bottom"], h["floor_top"], h["roof_bottom"], h["roof_top"]
|
||
return [
|
||
obox("branch", "branch_floor", "분기구 바닥", BRANCH_COLOR, 0, 0, ang, r_in, s_out, -t_out, t_out, fb, ft),
|
||
obox("branch", "branch_roof", "분기구 지붕", BRANCH_COLOR, 0, 0, ang, r_in, s_out, -t_out, t_out, rb, rt),
|
||
obox("branch", "branch_side_1", "분기구 옆벽 1", BRANCH_COLOR, 0, 0, ang, r_in, s_out, t_in, t_out, ft, rb),
|
||
obox("branch", "branch_side_2", "분기구 옆벽 2", BRANCH_COLOR, 0, 0, ang, r_in, s_out, -t_out, -t_in, ft, rb),
|
||
obox("branch", "branch_end", "분기구 끝벽", BRANCH_COLOR, 0, 0, ang, s_in, s_out, -t_in, t_in, ft, rb),
|
||
]
|
||
|
||
|
||
def vent_parts(ents, walls, center, phi, h):
|
||
# 24.4: 중심선 C→V1→T. 다리 1 = C→V1, 다리 2 = V1→T. 덕트 벽 t_in·t_out, 탑 안·바깥 사각형(다리 2 축)
|
||
pl = next((e["pts"] for e in ents if e["layer"] == VENT_LAYER and e["type"] == "LWPOLYLINE"
|
||
and len(e["pts"]) == 3 and same_pt(e["pts"][0], center)), None)
|
||
if pl is None:
|
||
raise SystemExit("환기구 중심선을 찾지 못했다")
|
||
v1, tip = pl[1], pl[2]
|
||
a1, a2 = ang_of(v1, center), ang_of(tip, v1)
|
||
l1 = math.dist(v1, center) * UNIT
|
||
# 꺾이는 쪽: 다리 2 가 다리 1 의 왼쪽으로 꺾이면 +1
|
||
turn = 1 if math.sin(math.radians(a2 - a1)) > 0 else -1
|
||
r1 = math.radians(a1)
|
||
n1x, n1y = -math.sin(r1), math.cos(r1)
|
||
duct = []
|
||
for e in ents:
|
||
if e["layer"] != WALL_LAYER or e["closed"] or e["type"] not in ("LWPOLYLINE", "POLYLINE") or len(e["pts"]) != 3:
|
||
continue
|
||
mid = e["pts"][1]
|
||
if math.dist(mid, v1) <= DUCT_NEAR:
|
||
duct.append(abs((mid[0] - center[0]) * n1x + (mid[1] - center[1]) * n1y))
|
||
if len(duct) != 4:
|
||
raise SystemExit(f"덕트 벽선을 네 개 찾지 못했다 ({len(duct)}개)")
|
||
ts = sorted(duct)
|
||
t_in = (ts[0] + ts[1]) / 2 * UNIT
|
||
t_out = (ts[2] + ts[3]) / 2 * UNIT
|
||
tower = [e["pts"] for e in ents if e["layer"] == WALL_LAYER and e["closed"] and len(e["pts"]) >= 4
|
||
and point_in_poly(tip, e["pts"])]
|
||
if len(tower) != 2:
|
||
raise SystemExit("환기탑 윤곽 두 개를 찾지 못했다")
|
||
outer_pts, inner_pts = min(tower, key=len), max(tower, key=len)
|
||
r2 = math.radians(a2)
|
||
u2 = (math.cos(r2), math.sin(r2))
|
||
n2 = (-math.sin(r2), math.cos(r2))
|
||
|
||
def st2(p):
|
||
dx, dy = p[0] - v1[0], p[1] - v1[1]
|
||
return (dx * u2[0] + dy * u2[1]) * UNIT, (dx * n2[0] + dy * n2[1]) * UNIT
|
||
so = [st2(p) for p in outer_pts]
|
||
si = [st2(p) for p in inner_pts]
|
||
s_o0, s_o1 = min(s for s, _ in so), max(s for s, _ in so)
|
||
t_o0, t_o1 = min(t for _, t in so), max(t for _, t in so)
|
||
s_i0, s_i1 = min(s for s, _ in si), max(s for s, _ in si)
|
||
t_i0, t_i1 = min(t for _, t in si), max(t for _, t in si)
|
||
r_in = walls["r_in"]
|
||
fb, ft, rb, rt = h["floor_bottom"], h["floor_top"], h["roof_bottom"], h["roof_top"]
|
||
ang1, ang2 = a1 + phi, a2 + phi
|
||
cx2, cy2 = rot((v1[0] - center[0]) * UNIT, (v1[1] - center[1]) * UNIT, phi)
|
||
ins = (sorted([turn * t_in, turn * t_out]))
|
||
outs = (sorted([-turn * t_out, -turn * t_in]))
|
||
return [
|
||
obox("vent", "duct_leg1_floor", "환기 덕트 1 바닥", VENT_COLOR, 0, 0, ang1, r_in, l1 + t_out, -t_out, t_out, fb, ft),
|
||
obox("vent", "duct_leg1_roof", "환기 덕트 1 지붕", VENT_COLOR, 0, 0, ang1, r_in, l1 + t_out, -t_out, t_out, rb, rt),
|
||
obox("vent", "duct_leg1_in", "환기 덕트 1 안쪽 벽", VENT_COLOR, 0, 0, ang1, r_in, l1 - t_out, ins[0], ins[1], ft, rb),
|
||
obox("vent", "duct_leg1_out", "환기 덕트 1 바깥 벽", VENT_COLOR, 0, 0, ang1, r_in, l1 + t_out, outs[0], outs[1], ft, rb),
|
||
obox("vent", "duct_leg2_floor", "환기 덕트 2 바닥", VENT_COLOR, cx2, cy2, ang2, t_out, s_o0, -t_out, t_out, fb, ft),
|
||
obox("vent", "duct_leg2_roof", "환기 덕트 2 지붕", VENT_COLOR, cx2, cy2, ang2, t_out, s_o0, -t_out, t_out, rb, rt),
|
||
obox("vent", "duct_leg2_in", "환기 덕트 2 안쪽 벽", VENT_COLOR, cx2, cy2, ang2, t_in, s_o0, ins[0], ins[1], ft, rb),
|
||
obox("vent", "duct_leg2_out", "환기 덕트 2 바깥 벽", VENT_COLOR, cx2, cy2, ang2, -t_out, s_o0, outs[0], outs[1], ft, rb),
|
||
obox("vent", "tower_floor", "환기탑 바닥", VENT_COLOR, cx2, cy2, ang2, s_o0, s_o1, t_o0, t_o1, fb, ft),
|
||
obox("vent", "tower_wall_near", "환기탑 벽 (덕트 쪽)", VENT_COLOR, cx2, cy2, ang2, s_o0, s_i0, t_o0, t_o1, rb, GROUND_Z),
|
||
obox("vent", "tower_wall_far", "환기탑 벽 (먼 쪽)", VENT_COLOR, cx2, cy2, ang2, s_i1, s_o1, t_o0, t_o1, ft, GROUND_Z),
|
||
obox("vent", "tower_wall_side_1", "환기탑 벽 (옆 1)", VENT_COLOR, cx2, cy2, ang2, s_i0, s_i1, t_o0, t_i0, ft, GROUND_Z),
|
||
obox("vent", "tower_wall_side_2", "환기탑 벽 (옆 2)", VENT_COLOR, cx2, cy2, ang2, s_i0, s_i1, t_i1, t_o1, ft, GROUND_Z),
|
||
]
|
||
|
||
|
||
def span_levels(ents, walls, gy):
|
||
# 25.1-1: 벽 전체 수평선(바깥 왼 벽선 ~ 바깥 오른 벽선, 양 끝 EPS) 의 높이, 높은 것부터 (mm)
|
||
levels = set()
|
||
for a, b, _ in segments_of(ents, WALL_LAYER):
|
||
if abs(a[1] - b[1]) > EPS:
|
||
continue
|
||
if abs(min(a[0], b[0]) - walls["outer_l"]) <= EPS and abs(max(a[0], b[0]) - walls["outer_r"]) <= EPS:
|
||
levels.add(round((a[1] - gy) * UNIT, 1))
|
||
if len(levels) < 3:
|
||
raise SystemExit(f"벽 전체 수평선을 셋 이상 찾지 못했다 ({len(levels)}개)")
|
||
return sorted(levels, reverse=True)
|
||
|
||
|
||
def top_slab_bottom(ents, walls, gy, top_z):
|
||
# 25.1-2: 윗면보다 낮고 바깥 벽 사이에 있으며 길이가 안쪽 지름의 0.9 배 넘는 수평선 중 가장 높은 것 (mm)
|
||
span = TOP_SPAN_FRAC * 2 * walls["r_in"] / UNIT
|
||
best = None
|
||
for a, b, _ in segments_of(ents, WALL_LAYER):
|
||
if abs(a[1] - b[1]) > EPS:
|
||
continue
|
||
lo, hi = min(a[0], b[0]), max(a[0], b[0])
|
||
if lo < walls["outer_l"] - EPS or hi > walls["outer_r"] + EPS or hi - lo <= span:
|
||
continue
|
||
z = round((a[1] - gy) * UNIT, 1)
|
||
if z < top_z - Z_TOL and (best is None or z > best):
|
||
best = z
|
||
if best is None:
|
||
raise SystemExit("상부 슬래브 밑면 선을 찾지 못했다")
|
||
return best
|
||
|
||
|
||
def bottom_slab_top(ents, walls, gy):
|
||
# 25.1-4: 안쪽 벽면에 한 끝이 있고 안쪽·아래로 내려가는 짧은 사선(길이 범위 FLOOR_HAUNCH)의 아래 끝 중 가장 낮은 것 (mm)
|
||
lows = []
|
||
for a, b, _ in segments_of(ents, WALL_LAYER):
|
||
if abs(a[0] - b[0]) <= EPS or abs(a[1] - b[1]) <= EPS:
|
||
continue
|
||
if not FLOOR_HAUNCH[0] <= math.dist(a, b) <= FLOOR_HAUNCH[1]:
|
||
continue
|
||
for w, o in ((a, b), (b, a)):
|
||
if abs(w[0] - walls["inner_l"]) > EPS and abs(w[0] - walls["inner_r"]) > EPS:
|
||
continue
|
||
if abs(o[0] - walls["axis"]) < abs(w[0] - walls["axis"]) and o[1] < w[1]:
|
||
lows.append((o[1] - gy) * UNIT)
|
||
if not lows:
|
||
raise SystemExit("하부 슬래브 윗면 사선을 찾지 못했다")
|
||
return round(min(lows), 1)
|
||
|
||
|
||
def plan1_circles(ents, center):
|
||
# 25.2: 평면도 1 안의 반지름 1 m 미만 구조물 원 (중복 하나로), 돌려주는 값: (중심 m, 반지름 m)
|
||
out = []
|
||
for e in ents:
|
||
if e["type"] != "CIRCLE" or e["layer"] != WALL_LAYER or e["r"] >= HOLE_R_MAX:
|
||
continue
|
||
if math.dist(e["center"], center) > R_IN or any(same_pt(c, e["center"]) for c, _ in out):
|
||
continue
|
||
out.append((e["center"], e["r"]))
|
||
return out
|
||
|
||
|
||
def rot_circle(c, r, center, phi):
|
||
# C 기준 mm 를 φ 만큼 돌린 원 {"circle": [x, y, r]}
|
||
x, y = rot((c[0] - center[0]) * UNIT, (c[1] - center[1]) * UNIT, phi)
|
||
return {"circle": [round(x), round(y), round(r * UNIT)]}
|
||
|
||
|
||
def top_slab_part(walls, top_z, bottom_z, center, phi, circles):
|
||
# 25.2: 상부 슬래브 — disc, r = 바깥 반지름, 구멍 = 평면도 1 의 작은 원
|
||
return {"id": "topslab", "name": "상부 슬래브", "kind": "topslab", "type": "disc", "color": TOP_COLOR,
|
||
"cx": 0, "cy": 0, "z0": round(bottom_z), "z1": round(top_z), "r": round(walls["r_out"]),
|
||
"holes": [rot_circle(c, r, center, phi) for c, r in circles]}
|
||
|
||
|
||
def neck_parts(ents, center, phi, top_z, circles):
|
||
# 25.3: 맨홀 목 — 평면도 1 의 닫힌 꼭짓점 4개 구조물 꺾은선 중 구멍 원 중심을 품는 것. y 가 작은 쪽이 neck_1
|
||
polys = []
|
||
for e in ents:
|
||
if e["layer"] != WALL_LAYER or not e["closed"] or e["type"] not in ("LWPOLYLINE", "POLYLINE") \
|
||
or len(e["pts"]) != 4 or not inside_plan(e, center):
|
||
continue
|
||
if not any(same_shape(e["pts"], p) for p in polys):
|
||
polys.append(e["pts"])
|
||
necks = []
|
||
for n, (c, r) in enumerate(sorted(circles, key=lambda h: h[0][1]), 1):
|
||
found = [p for p in polys if point_in_poly(c, p)]
|
||
if len(found) != 1:
|
||
raise SystemExit(f"맨홀 목 윤곽을 구멍마다 하나 찾지 못했다 ({len(found)}개)")
|
||
poly = [[round(v) for v in rot((x - center[0]) * UNIT, (y - center[1]) * UNIT, phi)] for x, y in found[0]]
|
||
necks.append({"id": f"neck_{n}", "name": f"맨홀 목 {n}", "kind": "neck", "type": "prism", "color": NECK_COLOR,
|
||
"polygon": poly, "z0": round(top_z), "z1": GROUND_Z,
|
||
"holes": [rot_circle(c, r, center, phi)]})
|
||
return necks
|
||
|
||
|
||
def x_marks(ents, center, layers):
|
||
# 25.5: X 표시 = 같은 레이어의 엇갈린 사선 LINE 두 개(길이 0.5 m 넘고 X·Y 범위가 같은 것).
|
||
# 돌려주는 값: 범위 (x0, x1, y0, y1) m, 중복 하나로
|
||
diag = []
|
||
for a, b, e in segments_of(ents):
|
||
if e["type"] != "LINE" or e["layer"] not in layers or not inside_plan(e, center):
|
||
continue
|
||
if abs(a[0] - b[0]) <= EPS or abs(a[1] - b[1]) <= EPS or math.dist(a, b) <= X_MARK_MIN:
|
||
continue
|
||
diag.append((a, b, e["layer"]))
|
||
marks = []
|
||
for i, (a, b, la) in enumerate(diag):
|
||
for c, d, lc in diag[i + 1:]:
|
||
if la != lc or (b[0] - a[0]) * (b[1] - a[1]) * (d[0] - c[0]) * (d[1] - c[1]) >= 0:
|
||
continue
|
||
ra = (min(a[0], b[0]), max(a[0], b[0]), min(a[1], b[1]), max(a[1], b[1]))
|
||
rc = (min(c[0], d[0]), max(c[0], d[0]), min(c[1], d[1]), max(c[1], d[1]))
|
||
if any(abs(p - q) > NEAR for p, q in zip(ra, rc)):
|
||
continue
|
||
rect = (min(ra[0], rc[0]), max(ra[1], rc[1]), min(ra[2], rc[2]), max(ra[3], rc[3]))
|
||
if not any(all(abs(p - q) <= EPS for p, q in zip(rect, m)) for m in marks):
|
||
marks.append(rect)
|
||
return marks
|
||
|
||
|
||
def rect_poly(x0, x1, y0, y1):
|
||
return [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]
|
||
|
||
|
||
def bbox_mm(poly):
|
||
xs = [p[0] for p in poly]
|
||
ys = [p[1] for p in poly]
|
||
return min(xs), max(xs), min(ys), max(ys)
|
||
|
||
|
||
def pad_and_sump(ents, walls, gy, center, slab_top, slab_holes):
|
||
# 25.5: 19B 패드 — 19B 슬래브 윗면 위 0~0.3 m 의 안쪽 벽 사이 길이 9 m 넘는 수평선 → 윗면, r = 길이/2.
|
||
# 구멍 = X 표시 사각형을 ±r 로 자른 것. 자르지 않은 사각형이 슬래브 구멍에 없으면 침사구로 더한다
|
||
band = []
|
||
for a, b, _ in segments_of(ents, WALL_LAYER):
|
||
if abs(a[1] - b[1]) > EPS:
|
||
continue
|
||
lo, hi = min(a[0], b[0]), max(a[0], b[0])
|
||
z = round((a[1] - gy) * UNIT, 1)
|
||
if not slab_top + Z_TOL < z <= slab_top + SLAB_THICK * UNIT + Z_TOL:
|
||
continue
|
||
if lo < walls["inner_l"] - EPS or hi > walls["inner_r"] + EPS or hi - lo <= PAD_LEN_MIN:
|
||
continue
|
||
band.append((z, (hi - lo) / 2 * UNIT))
|
||
if len(band) != 1:
|
||
raise SystemExit(f"19B 패드 윗면 선을 하나 찾지 못했다 ({len(band)}개)")
|
||
pad_top, pad_r = band[0]
|
||
cut = lambda v: max(-pad_r, min(pad_r, v))
|
||
pad_holes, sump = [], []
|
||
for x0, x1, y0, y1 in x_marks(ents, center, X_MARK_LAYERS):
|
||
mm = (round((x0 - center[0]) * UNIT), round((x1 - center[0]) * UNIT),
|
||
round((y0 - center[1]) * UNIT), round((y1 - center[1]) * UNIT))
|
||
pad_holes.append(rect_poly(cut(mm[0]), cut(mm[1]), cut(mm[2]), cut(mm[3])))
|
||
if not any(all(abs(p - q) <= 10 for p, q in zip(bbox_mm(h), mm)) for h in slab_holes):
|
||
sump.append(rect_poly(*mm))
|
||
return round(pad_top), round(pad_r), pad_holes, sump
|
||
|
||
|
||
def plan_weir_walls(ents, center):
|
||
# 25.6-1: 20B 평면 안쪽 원 안의 길이 5 m 넘는 구조물 세로 LINE 두 개 → x0, x1 (m, C 기준)
|
||
xs = set()
|
||
for e in ents:
|
||
if e["type"] != "LINE" or e["layer"] != WALL_LAYER or not inside_plan(e, center):
|
||
continue
|
||
a, b = e["pts"]
|
||
if abs(a[0] - b[0]) > EPS or abs(a[1] - b[1]) <= WEIR_WALL_MIN:
|
||
continue
|
||
xs.add(round(a[0] - center[0], 4))
|
||
if len(xs) != 2:
|
||
raise SystemExit(f"월류벽 세로 선을 두 개 찾지 못했다 ({len(xs)}개)")
|
||
return min(xs), max(xs)
|
||
|
||
|
||
def weir_heights(ents, walls, gy, floor_top):
|
||
# 25.6-2: 단면 안쪽 벽 사이(벽선에서 WEIR_SIDE 넘게 떨어진) 길이 1 m 넘는 세로 선 중 아래 끝이
|
||
# 하부 슬래브 윗면 위 0~0.3 m 인 것 → 위끝 = 월류벽 윗면. 그 사이에 걸친 수평선 중 윗면보다 낮은 것 = 낮은 칸 윗면
|
||
verts = []
|
||
for a, b, _ in segments_of(ents, WALL_LAYER):
|
||
if abs(a[0] - b[0]) > EPS or abs(a[1] - b[1]) <= SECTION_WEIR_MIN:
|
||
continue
|
||
x = a[0]
|
||
if not walls["inner_l"] + WEIR_SIDE < x < walls["inner_r"] - WEIR_SIDE:
|
||
continue
|
||
lo, hi = sorted((a[1], b[1]))
|
||
z_lo = (lo - gy) * UNIT
|
||
if floor_top - Z_TOL <= z_lo <= floor_top + SLAB_THICK * UNIT + Z_TOL:
|
||
verts.append((x, round((hi - gy) * UNIT, 1)))
|
||
if len(verts) != 2 or verts[0][1] != verts[1][1]:
|
||
raise SystemExit(f"월류벽 세로 선 두 개의 윗끝을 맞추지 못했다 ({len(verts)}개)")
|
||
top_z = verts[0][1]
|
||
x_lo, x_hi = sorted(x for x, _ in verts)
|
||
low = []
|
||
for a, b, _ in segments_of(ents, WALL_LAYER):
|
||
if abs(a[1] - b[1]) > EPS:
|
||
continue
|
||
lo, hi = min(a[0], b[0]), max(a[0], b[0])
|
||
z = round((a[1] - gy) * UNIT, 1)
|
||
if lo <= x_lo + EPS and hi >= x_hi - EPS and z < top_z - Z_TOL:
|
||
low.append(z)
|
||
if not low:
|
||
raise SystemExit("월류벽 낮은 칸 윗면을 찾지 못했다")
|
||
return top_z, max(low)
|
||
|
||
|
||
def weir_parts(ents, walls, gy, plan20, floor_top, weir_top, low_top):
|
||
# 25.6-1·3: 월류벽 3개 (서쪽 L, 동쪽 R, 홈 아래 mid). 바깥 y 끝 = ±√(안쪽 반지름² − x1²)
|
||
x0, x1 = plan_weir_walls(ents, plan20)
|
||
marks = x_marks(ents, plan20, (WALL_LAYER,))
|
||
if len(marks) != 1:
|
||
raise SystemExit(f"하부슬래브 평면의 X 표시를 하나 찾지 못했다 ({len(marks)}개)")
|
||
_, _, y0, y1 = marks[0]
|
||
y0, y1 = round((y0 - plan20[1]) * UNIT), round((y1 - plan20[1]) * UNIT)
|
||
xa, xb = round(x0 * UNIT), round(x1 * UNIT)
|
||
reach = round(math.sqrt((walls["r_in"] / UNIT) ** 2 - x1 * x1) * UNIT)
|
||
fl = round(floor_top)
|
||
parts = [
|
||
{"id": "weir_L", "name": "월류벽 (넘침 홈 서쪽)", "kind": "weir", "type": "prism", "color": WEIR_COLOR,
|
||
"polygon": rect_poly(xa, xb, -reach, y0), "z0": fl, "z1": round(weir_top)},
|
||
{"id": "weir_R", "name": "월류벽 (넘침 홈 동쪽)", "kind": "weir", "type": "prism", "color": WEIR_COLOR,
|
||
"polygon": rect_poly(xa, xb, y1, reach), "z0": fl, "z1": round(weir_top)},
|
||
{"id": "weir_mid", "name": "월류벽 (홈 아래)", "kind": "weir", "type": "prism", "color": WEIR_COLOR,
|
||
"polygon": rect_poly(xa, xb, y0, y1), "z0": fl, "z1": round(low_top)},
|
||
]
|
||
return parts, xa, xb
|
||
|
||
|
||
def pit_parts(ents, walls, gy, floor_top, weir_top, xa, xb):
|
||
# 25.7: 집수조·침사조 — 월류벽 왼쪽 −r~x0, 오른쪽 x1~r. 단면 글자 「집수조」가 축 왼쪽이면 왼쪽이 pit_pump
|
||
labels = [t for t in ents if t["type"] == "TEXT" and t["text"].replace(" ", "") == PIT_LABEL
|
||
and walls["outer_l"] <= t["pts"][0][0] <= walls["outer_r"] and t["pts"][0][1] < gy]
|
||
if len(labels) != 1:
|
||
raise SystemExit(f"단면의 「{PIT_LABEL}」 글자를 하나 찾지 못했다 ({len(labels)}개)")
|
||
pump_left = labels[0]["pts"][0][0] < walls["axis"]
|
||
r = round(walls["r_in"])
|
||
left_x, right_x = (-r, xa), (xb, r)
|
||
pump_x, tbm_x = (left_x, right_x) if pump_left else (right_x, left_x)
|
||
common = {"kind": "pit", "type": "zone", "alpha": PIT_ALPHA, "r": r, "z0": round(floor_top), "z1": round(weir_top)}
|
||
return [
|
||
{"id": "pit_pump", "name": "집수조 (펌프 쪽)", "color": PIT_PUMP_COLOR, "x_min": pump_x[0], "x_max": pump_x[1], **common},
|
||
{"id": "pit_tbm", "name": "침사조 (터널 쪽)", "color": PIT_TBM_COLOR, "x_min": tbm_x[0], "x_max": tbm_x[1], **common},
|
||
]
|
||
|
||
|
||
def load_design_rules():
|
||
# 27장: 설계 규칙 파일 (🔶 부재 6개 + 색 규칙). 스크립트 옆의 design_rules.json
|
||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "design_rules.json")
|
||
with open(path, encoding="utf-8") as f:
|
||
return json.load(f)
|
||
|
||
|
||
def apply_design_rules(parts, rules):
|
||
# 도면에서 읽은 부재 뒤에 🔶 부재를 붙이고, 모든 부재에 kind → id 순으로 color·alpha 를 덮어쓴다
|
||
out = parts + json.loads(json.dumps(rules["parts"]))
|
||
for p in out:
|
||
p.update(rules["style"].get(p["kind"], {}))
|
||
p.update(rules["ids"].get(p["id"], {}))
|
||
return out
|
||
|
||
|
||
# 29.2 도면 연결값: views.json 의 boxes 순서 (사람 입력) — 0 전체 단면, 1 평면도, 2 생략 단면, 3 슬래브 평면
|
||
SEC_BOX, PLAN_BOX, BREAK_BOX, SLAB_BOX = 0, 1, 2, 3
|
||
DRAWING_SRC = "private/drawing_gen.svg" # 웹이 읽는 SVG (make_model.sh 가 만든다)
|
||
BREAK_LONG = 10.0 # 생략 단면 세로 선: 길이 10 m 넘는 것
|
||
BREAK_LABEL_X = 8.0 # 층 글자: 축에서 8 m 안
|
||
BREAK_LINE_BELOW = 0.3 # 층 글자 아래 0.3 m 안의 구조물 수평선
|
||
BREAK_GROUND_TOL = 0.05 # 지반선이 축을 지나는 허용 (m), 지표와 같은 층 판정 허용
|
||
BREAK_TOP_PAD = 300 # z_upper_min = 위 구간 가장 깊은 윗면 − 300 mm
|
||
PLAN2_MAX = 30.0 # 평면도 2: 평면도 1 아래 30 m 안
|
||
PLAN_VIEW_LABELS = {"plan1": "평면도", "plan2": "평면도 2"}
|
||
SLAB_VIEW_LABELS = {"odd": "중간슬래브 (홀수층)", "even": "중간슬래브 (짝수층)",
|
||
"18B": "중간슬래브 (18B)", "19B": "중간슬래브 (19B)", "20B": "하부슬래브 (20B)"}
|
||
|
||
|
||
def svg_pt(pt, boxes, box):
|
||
# 도면 점 → SVG 점: 상자 place 로 (X − dx, dy − Y), 소수 둘째 자리
|
||
dx, dy = boxes[box]["place"]
|
||
return round(pt[0] - dx, 2), round(dy - pt[1], 2)
|
||
|
||
|
||
def title_x(ents, title):
|
||
for t in ents:
|
||
if t["type"] == "TEXT" and t["layer"] == TITLE_LAYER and t["text"].replace(" ", "") == title:
|
||
return t["pts"][0][0]
|
||
raise SystemExit(f"제목 글자 {title} 을 찾지 못했다")
|
||
|
||
|
||
def section_view(ents, walls, boxes, vid, label, gy_check=EPS):
|
||
# 20.1 ① / 24.2: 전체 단면의 축 X 와 그 축의 지표 Y
|
||
axis = walls["axis"]
|
||
gy = ground_y(ents, axis, gy_check)
|
||
x0, y0 = svg_pt((axis, gy), boxes, SEC_BOX)
|
||
return {"id": vid, "type": "section", "label": label, "x0": x0, "y0": y0}
|
||
|
||
|
||
def break_view(ents, tops, boxes, vid, label, title):
|
||
# 생략 단면: 「단면 X-X」 글자 가까운 구조물 세로선 4개 = 벽, 층 글자마다 지표에 해당하는 Y 로 위·아래 구간을 나눈다
|
||
tx = title_x(ents, title)
|
||
tall = {round(a[0], 2) for a, b, _ in segments_of(ents, WALL_LAYER)
|
||
if abs(a[0] - b[0]) <= EPS and abs(a[1] - b[1]) > BREAK_LONG}
|
||
xs = sorted(sorted(tall, key=lambda x: abs(x - tx))[:4])
|
||
if len(xs) < 4:
|
||
raise SystemExit(f"{title} 생략 단면의 벽 선 네 개를 찾지 못했다")
|
||
outer_l, inner_l, inner_r, outer_r = xs
|
||
axis = (outer_l + outer_r) / 2
|
||
gy = ground_y(ents, axis, BREAK_GROUND_TOL)
|
||
upper, lower = [], []
|
||
for t in ents:
|
||
m = re.fullmatch(r"(\d+)B", t["text"].replace(" ", "")) if t["type"] == "TEXT" else None
|
||
if not m or int(m.group(1)) > len(tops) or abs(t["pts"][0][0] - axis) > BREAK_LABEL_X:
|
||
continue # 19B 까지의 층 글자만 (20B 는 하부 슬래브)
|
||
lx, ly = t["pts"][0]
|
||
below = [a[1] for a, b, _ in segments_of(ents, WALL_LAYER)
|
||
if abs(a[1] - b[1]) <= EPS and abs(min(a[0], b[0]) - inner_l) <= EPS and ly - BREAK_LINE_BELOW <= a[1] <= ly]
|
||
if not below:
|
||
raise SystemExit(f"층 글자 {t['text']} 아래 수평선을 찾지 못했다")
|
||
top = tops[int(m.group(1)) - 1]
|
||
surface = max(below) - top / UNIT
|
||
(upper if abs(surface - gy) <= BREAK_GROUND_TOL else lower).append((top, surface))
|
||
if not upper or not lower:
|
||
raise SystemExit(f"{title} 위·아래 구간을 나누지 못했다")
|
||
x0, y0_upper = svg_pt((axis, gy), boxes, BREAK_BOX)
|
||
y0_lower = svg_pt((axis, sum(s for _, s in lower) / len(lower)), boxes, BREAK_BOX)[1]
|
||
return {"id": vid, "type": "section_break", "label": label, "x0": x0, "y0_upper": y0_upper,
|
||
"y0_lower": y0_lower, "z_upper_min": round(min(z for z, _ in upper) - BREAK_TOP_PAD),
|
||
"z_lower_max": round(max(z for z, _ in lower))}
|
||
|
||
|
||
def plan_views(ents, center, phi, boxes):
|
||
# 24.1: 평면도 1 = 「평면도」 아래 중심, 평면도 2 = 그 아래 30 m 안의 가장 가까운 반지름 4.85 원, 회전 = −φ
|
||
circles = [e["center"] for e in ents if e["type"] == "CIRCLE" and abs(e["r"] - R_IN) <= EPS]
|
||
below = [c for c in circles if c[1] < center[1] and math.dist(c, center) <= PLAN2_MAX]
|
||
if not below:
|
||
raise SystemExit("평면도 2 중심을 찾지 못했다")
|
||
p2 = min(below, key=lambda c: math.dist(c, center))
|
||
rot = round(-phi, 1)
|
||
cx1, cy1 = svg_pt(center, boxes, PLAN_BOX)
|
||
cx2, cy2 = svg_pt(p2, boxes, PLAN_BOX)
|
||
return [
|
||
{"id": "plan1", "type": "plan", "rot_deg": rot, "label": PLAN_VIEW_LABELS["plan1"], "cx": cx1, "cy": cy1, "tunnel": True},
|
||
{"id": "plan2", "type": "plan", "rot_deg": rot, "label": PLAN_VIEW_LABELS["plan2"], "cx": cx2, "cy": cy2, "tunnel": False},
|
||
]
|
||
|
||
|
||
def slab_views(plans, tops, floor_top, boxes):
|
||
# 20.1 ④: 슬래브 평면 다섯 개, z1 = 그 평면을 쓰는 층들의 윗면
|
||
spec = [
|
||
("slabOdd", "odd", list(tops[0:17:2]), "slab", False),
|
||
("slabEven", "even", list(tops[1:16:2]), "slab", False),
|
||
("slab18", "18B", [tops[17]], "slab", False),
|
||
("slab19", "19B", [tops[18]], "slab", True),
|
||
("slab20", "20B", [floor_top], "bottom", False),
|
||
]
|
||
out = []
|
||
for vid, key, z1, kind_, tunnel in spec:
|
||
cx, cy = svg_pt(plans[key], boxes, SLAB_BOX)
|
||
out.append({"id": vid, "type": "slabplan", "label": SLAB_VIEW_LABELS[key], "cx": cx, "cy": cy,
|
||
"kind": kind_, "z1": [round(z) for z in z1], "tunnel": tunnel})
|
||
return out
|
||
|
||
|
||
def drawing_views(ents, walls, tops, floor_top, phi, center, plans, vj):
|
||
# 29.2: 도면 연결값 — views.json 의 상자 배치로 도면 점을 SVG 점으로 옮긴다
|
||
boxes = vj["boxes"]
|
||
sec_a = section_view(ents, section_walls(ents, 0), boxes, "secA", "단면 A-A")
|
||
sec_b = section_view(ents, section_walls(ents, 4), boxes, "secB", "단면 B-B")
|
||
views = [sec_a, sec_b,
|
||
break_view(ents, tops, boxes, "secA2", "단면 A-A (4B~15B 생략)", "단면A-A"),
|
||
break_view(ents, tops, boxes, "secB2", "단면 B-B (4B~15B 생략)", "단면B-B")]
|
||
views += plan_views(ents, center, phi, boxes)
|
||
views += slab_views(plans, tops, floor_top, boxes)
|
||
return {
|
||
"src": DRAWING_SRC, "viewBox": vj["viewBox"], "fit": vj["fit"],
|
||
"section": {"x0": sec_a["x0"], "y0": sec_a["y0"], "mm_per_unit": 1000},
|
||
"plan": {"cx": views[4]["cx"], "cy": views[4]["cy"], "mm_per_unit": 1000},
|
||
"views": views,
|
||
}
|
||
|
||
|
||
def build_model(path, views_path):
|
||
ents = load_dxf(path)
|
||
walls = section_walls(ents)
|
||
gy = ground_y(ents, walls["axis"])
|
||
tops = slab_tops(ents, walls, gy)
|
||
plans = find_slab_plans(ents)
|
||
# 25.1: 벽 전체 수평선 → 바닥 콘크리트 밑면(가장 낮음), 하부 슬래브 밑면(둘째), 상부 슬래브 윗면(가장 높음)
|
||
levels = span_levels(ents, walls, gy)
|
||
top_z, floor_bot, lean_bot = levels[0], levels[-2], levels[-1]
|
||
top_bot = top_slab_bottom(ents, walls, gy, top_z)
|
||
floor_top = bottom_slab_top(ents, walls, gy)
|
||
|
||
parts = [{
|
||
"id": "wall", "name": "벽체", "kind": "wall", "type": "ring", "color": WALL_COLOR, "alpha": 0.25,
|
||
"cx": 0, "cy": 0, "z0": round(floor_bot), "z1": round(top_bot),
|
||
"r_out": round(walls["r_out"]), "r_in": round(walls["r_in"]),
|
||
}]
|
||
slabs, rails = [], []
|
||
holes_of = {key: plan_holes(ents, plans[key]) for key in plans}
|
||
rail_of = {key: plan_railings(ents, plans[key]) for key in plans}
|
||
# 25.5: 19B 패드와 침사구 (19B 슬래브 윗면 = 가장 낮은 슬래브 윗면)
|
||
pad_top, pad_r, pad_holes, sump = pad_and_sump(ents, walls, gy, plans["19B"], tops[18], holes_of["19B"])
|
||
for i, z1 in enumerate(tops):
|
||
name = f"{i + 1}B"
|
||
# 홀수층 1B·3B·…·17B, 짝수층 2B·…·16B, 18B, 19B (같은 평면을 공유)
|
||
key = name if name in ("18B", "19B") else ("odd" if (i + 1) % 2 else "even")
|
||
holes = holes_of[key] + (sump if name == "19B" else [])
|
||
slabs.append({
|
||
"id": f"slab_{name}", "name": f"{name} 슬래브", "kind": "slab", "type": "disc",
|
||
"color": SLAB_COLOR, "cx": 0, "cy": 0, "z0": round(z1 - SLAB_THICK * UNIT), "z1": round(z1),
|
||
"r": round(walls["r_in"]), "holes": holes,
|
||
})
|
||
if rail_of[key]:
|
||
rails.append({
|
||
"id": f"rail_{name}", "name": f"방호 난간 ({name} 윗면)", "kind": "railing", "type": "railing",
|
||
"color": RAIL_COLOR, "z": round(z1), "h": RAIL_H, "paths": rail_of[key],
|
||
})
|
||
pad = {"id": "pad_19B", "name": "19B 패드", "kind": "pad", "type": "disc", "color": PAD_COLOR,
|
||
"cx": 0, "cy": 0, "z0": round(tops[18]), "z1": pad_top, "r": pad_r, "holes": pad_holes}
|
||
bottom = {"id": "bottom_20B", "name": "하부 슬래브(20B)", "kind": "bottom", "type": "disc", "color": BOTTOM_COLOR,
|
||
"cx": 0, "cy": 0, "z0": round(floor_bot), "z1": round(floor_top), "r": round(walls["r_in"])}
|
||
lean = {"id": "lean_concrete", "name": "바닥콘크리트", "kind": "lean", "type": "disc", "color": LEAN_COLOR,
|
||
"cx": 0, "cy": 0, "z0": round(lean_bot), "z1": round(floor_bot), "r": round(walls["r_out"])}
|
||
stairs = stair_parts(ents, walls, gy, tops, holes_of)
|
||
tunnels = tunnel_parts(ents, walls, gy, plans["19B"])
|
||
# 24.1: 평면도 1 의 φ 와 분기구 다리, 24.2: A-A(분기구)·B-B(환기구) 높이
|
||
center = plan_center(ents)
|
||
tunnel_angle = plan_tunnel(ents, plans["19B"])["angle"]
|
||
phi, branch_leg = frame_angle(ents, center, section_tunnel(ents, walls, gy)["r_out"], tunnel_angle)
|
||
# 25.2·25.3: 상부 슬래브 구멍과 맨홀 목 (평면도 1 기준, φ 만큼 돌린다)
|
||
circles = plan1_circles(ents, center)
|
||
top = top_slab_part(walls, top_z, top_bot, center, phi, circles)
|
||
necks = neck_parts(ents, center, phi, top_z, circles)
|
||
# 25.6·25.7: 월류벽·집수조·침사조 (20B 평면, 단면 높이)
|
||
weir_top, low_top = weir_heights(ents, walls, gy, floor_top)
|
||
weirs, xa, xb = weir_parts(ents, walls, gy, plans["20B"], floor_top, weir_top, low_top)
|
||
pits = pit_parts(ents, walls, gy, floor_top, weir_top, xa, xb)
|
||
branches = branch_parts(ents, walls, center, phi, branch_leg, section_heights(ents, walls, gy))
|
||
vents = vent_parts(ents, walls, center, phi, section_heights(ents, section_walls(ents, 4), gy))
|
||
parts = parts[:1] + [top] + necks + slabs + [pad, bottom, lean] + stairs + tunnels + weirs + pits + branches + vents + rails
|
||
with open(views_path, encoding="utf-8") as f:
|
||
vj = json.load(f)
|
||
drawing = drawing_views(ents, walls, tops, floor_top, phi, center, plans, vj)
|
||
return {"units": "mm", "parts": apply_design_rules(parts, load_design_rules()), "drawing": drawing}
|
||
|
||
|
||
def main(argv):
|
||
if len(argv) not in (3, 4):
|
||
print("사용법: python3 tools/extract_from_dxf.py <도면.dxf> model_extracted.js [views.json]")
|
||
return 2
|
||
views_path = argv[3] if len(argv) == 4 else os.path.join(os.path.dirname(os.path.abspath(__file__)), "views.json")
|
||
model = build_model(argv[1], views_path)
|
||
with open(argv[2], "w", encoding="utf-8") as f:
|
||
f.write("const MODEL = " + json.dumps(model, ensure_ascii=False, indent=1) + ";\n")
|
||
print(f"완료: {argv[2]} (부재 {len(model['parts'])}개)")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main(sys.argv))
|