Files

336 lines
15 KiB
Python

#!/usr/bin/env python3
"""BLOCK8 단면 구조선과 블록 내부 콘크리트선을 모델 데이터로 변환한다."""
import json
import math
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "public/drawings/광터지하차도_BLOCK8.dxf"
OUTPUT = ROOT / "src/model/block8.json"
def read_pairs(path):
lines = path.read_text(encoding="utf-8-sig").splitlines()
if len(lines) % 2:
raise ValueError("DXF 그룹 코드 줄 수가 짝수가 아닙니다.")
return [(int(lines[i].strip()), lines[i + 1].strip()) for i in range(0, len(lines), 2)]
def records(pairs, start, end):
result = []
current = None
for code, value in pairs[start:end]:
if code == 0:
if current:
result.append(current)
current = {"type": value.strip(), "data": []}
elif current:
current["data"].append((code, value.strip()))
if current:
result.append(current)
return result
def parse_sections(pairs, section_name):
begin = next(i for i, (code, value) in enumerate(pairs) if code == 0 and value.strip() == "SECTION" and pairs[i + 1][0] == 2 and pairs[i + 1][1].strip() == section_name)
end = next(i for i in range(begin + 2, len(pairs)) if pairs[i][0] == 0 and pairs[i][1].strip() == "ENDSEC")
return begin, end
def parse_blocks(pairs):
begin, end = parse_sections(pairs, "BLOCKS")
blocks = {}
i = begin + 2
while i < end:
if pairs[i][0] != 0 or pairs[i][1].strip() != "BLOCK":
i += 1
continue
block_start = i
j = i + 1
while j < end and not (pairs[j][0] == 0 and pairs[j][1].strip() == "ENDBLK"):
j += 1
header_end = next((k for k in range(block_start + 1, j) if pairs[k][0] == 0), j)
header = pairs[block_start + 1:header_end]
header_values = [value.strip() for code, value in header]
name_pos = next(k for k, (code, value) in enumerate(header) if code == 100 and value.strip() == "AcDbBlockBegin")
name = next(value.strip() for code, value in header[name_pos + 1:] if code == 2)
base = []
for code, value in header[name_pos + 1:]:
if code in (10, 20, 30):
base.append(float(value))
if len(base) == 3:
break
base += [0.0] * (3 - len(base))
blocks[name] = {"base": base, "entities": records(pairs, header_end, j)}
i = j + 1
return blocks
def parse_modelspace(pairs):
begin, end = parse_sections(pairs, "ENTITIES")
return records(pairs, begin + 2, end)
def props(entity):
result = defaultdict(list)
for code, value in entity["data"]:
result[code].append(value)
return result
def first(p, code, default=None):
return p[code][0] if code in p else default
def layer(entity):
return first(props(entity), 8, "0")
def line_points(entity):
p = props(entity)
return [(float(first(p, 10)), float(first(p, 20))), (float(first(p, 11)), float(first(p, 21)))]
def normalized_segment(entity, center_x, y0, el0):
points = line_points(entity)
points.sort(key=lambda p: (round(p[0], 6), p[1]))
profile = [[(x - center_x) / 1000, el0 + (y - y0) / 1000] for x, y in points]
key = (layer(entity), round((points[0][0] - center_x) / 1000, 3), round((points[1][0] - center_x) / 1000, 3))
average = sum(y for x, y in profile) / 2
slope = profile[1][1] - profile[0][1]
return key, (average, slope), profile
def cross_section(entities, y_bounds, center_x, y0, el0):
grouped = defaultdict(list)
for entity in entities:
if entity["type"] != "LINE" or layer(entity) not in ("CS-CONC-MAJR", "CS-CONC-MINR"):
continue
points = line_points(entity)
if not all(y_bounds[0] <= y <= y_bounds[1] for x, y in points):
continue
if not all(1_280_000 <= x <= 1_310_000 for x, y in points):
continue
key, sort_key, profile = normalized_segment(entity, center_x, y0, el0)
grouped[key].append({"profile": profile, "sort": sort_key, "handle": first(props(entity), 5, "")})
return grouped
def arc_points(entity, steps=16):
p = props(entity)
cx, cy = float(first(p, 10)), float(first(p, 20))
radius = float(first(p, 40))
start = math.radians(float(first(p, 50)))
end = math.radians(float(first(p, 51)))
while end < start:
end += 2 * math.pi
return [(cx + radius * math.cos(start + (end - start) * i / steps), cy + radius * math.sin(start + (end - start) * i / steps)) for i in range(steps + 1)]
def block_entity_points(entity):
p = props(entity)
if entity["type"] == "LINE":
return [(float(first(p, 10)), float(first(p, 20))), (float(first(p, 11)), float(first(p, 21)))]
if entity["type"] == "ARC":
return arc_points(entity, 16)
return []
def transform_block(points, insert, base):
p = props(insert)
ix, iy = float(first(p, 10)), float(first(p, 20))
sx, sy = float(first(p, 41, "1")), float(first(p, 42, "1"))
angle = math.radians(float(first(p, 50, "0")))
result = []
for x, y in points:
x, y = (x - base[0]) * sx, (y - base[1]) * sy
result.append((ix + x * math.cos(angle) - y * math.sin(angle), iy + x * math.sin(angle) + y * math.cos(angle)))
return result
def intersect_line_arc(line, arc):
(x0, y0), (x1, y1) = line
p = props(arc)
cx, cy, radius = float(first(p, 10)), float(first(p, 20)), float(first(p, 40))
dx, dy = x1 - x0, y1 - y0
fx, fy = x0 - cx, y0 - cy
aa = dx * dx + dy * dy
bb = 2 * (fx * dx + fy * dy)
cc = fx * fx + fy * fy - radius * radius
disc = bb * bb - 4 * aa * cc
if disc < 0:
return []
roots = [(-bb - math.sqrt(disc)) / (2 * aa), (-bb + math.sqrt(disc)) / (2 * aa)]
start, end = float(first(p, 50)), float(first(p, 51))
points = []
for t in roots:
if not 0 <= t <= 1:
continue
x, y = x0 + t * dx, y0 + t * dy
theta = math.degrees(math.atan2(y - cy, x - cx)) % 360
if start - 0.01 <= theta <= end + 0.01:
points.append((theta, x, y))
return points
def alignment_units(entities):
arcs = []
for entity in entities:
p = props(entity)
if entity["type"] == "ARC" and first(p, 8) == "CS-CNTL" and abs(float(first(p, 40, "0")) - 1_700_000) < 1:
arcs.append(entity)
cross_lines = []
for entity in entities:
if entity["type"] != "LINE" or layer(entity) != "CS-CONC-MAJR":
continue
(x0, y0), (x1, y1) = line_points(entity)
if -90_000 < y0 < -60_000 and -90_000 < y1 < -60_000 and abs(y1 - y0) > 19_000 and abs(y1 - y0) < 21_000 and abs(x1 - x0) < 200:
cross_lines.append(entity)
if len(arcs) != 2:
raise ValueError(f"평면 중심선 원호 수가 예상과 다릅니다: {len(arcs)}")
center_x_origin = sum(float(first(props(arc), 10)) for arc in arcs) / len(arcs)
center_y_origin = float(first(props(arcs[0]), 20)) - float(first(props(arcs[0]), 40))
units = []
for index, arc in enumerate(sorted(arcs, key=lambda e: float(first(props(e), 10)))):
acx = float(first(props(arc), 10))
candidates = [line for line in cross_lines if abs(sum(x for x, y in line_points(line)) / 2 - acx) < 12_000]
intersections = []
boundary_handles = []
for line in candidates:
found = intersect_line_arc(line_points(line), arc)
if found:
intersections.extend(found)
boundary_handles.append(first(props(line), 5, ""))
intersections.sort()
if len(intersections) != 2:
raise ValueError(f"평면 중심선 {index + 1}의 구조선 교점이 2개가 아닙니다: {len(intersections)}")
theta0, theta1 = intersections[0][0], intersections[1][0]
radius_m = float(first(props(arc), 40)) / 1000
length = radius_m * math.radians(theta1 - theta0)
units.append({
"id": f"alignment-{index + 1}",
"name": "BLOCK8 기준 중심선",
"center": [(acx - center_x_origin) / 1000, (float(first(props(arc), 20)) - center_y_origin) / 1000],
"planOrigin": [center_x_origin, center_y_origin],
"radius": radius_m,
"startAngle": theta0,
"endAngle": theta1,
"length": length,
"centerlineHandle": first(props(arc), 5, ""),
"boundaryHandles": boundary_handles,
})
# 평면도와 상부평면도에 반복된 중심선은 같은 구조물의 도면 표현이다.
# 첫 번째 중심선을 모델 배치 기준으로 사용하고 두 번째는 검산에만 쓴다.
return units[:1]
def add_block_parts(parts, entities, blocks, center_x, sections):
block_name = "A$C2017021411375356"
block = blocks[block_name]
concrete_entities = [e for e in block["entities"] if layer(e) in ("CS-CONC-MAJR", "CS-CONC-MINR") and e["type"] in ("LINE", "ARC")]
inserts = []
for entity in entities:
if entity["type"] != "INSERT" or first(props(entity), 2) != block_name:
continue
p = props(entity)
x, y = float(first(p, 10)), float(first(p, 20))
if 1_280_000 < x < 1_310_000 and any(abs(y - sy) < 100 for sy in (-76_987.31555584459, -96_306.66955584464)):
inserts.append(entity)
# 각 단면에서 좌우 삽입점 한 개씩 확인한다.
section_inserts = []
for section in sections:
chosen = [e for e in inserts if abs(float(first(props(e), 20)) - section["insertY"]) < 1]
chosen.sort(key=lambda e: float(first(props(e), 10)))
if len(chosen) != 2:
raise ValueError(f"블록 내부 콘크리트 삽입점 수가 예상과 다릅니다: {len(chosen)}")
section_inserts.append(chosen)
result = []
for side_idx, side_name in enumerate(("left", "right")):
start_insert, end_insert = section_inserts[0][side_idx], section_inserts[1][side_idx]
for entity_idx, block_entity in enumerate(concrete_entities):
start_raw = transform_block(block_entity_points(block_entity), start_insert, block["base"])
end_raw = transform_block(block_entity_points(block_entity), end_insert, block["base"])
start_profile = [[(x - center_x) / 1000, sections[0]["elTop"] + (y - sections[0]["yTop"]) / 1000] for x, y in start_raw]
end_profile = [[(x - center_x) / 1000, sections[1]["elTop"] + (y - sections[1]["yTop"]) / 1000] for x, y in end_raw]
result.append({
"id": f"insert-{side_name}-{entity_idx + 1:02d}",
"name": f"블록 내부 윤곽 {side_name} {entity_idx + 1:02d}",
"layer": layer(block_entity),
"group": "insert",
"sourceHandle": first(props(block_entity), 5, ""),
"sourceInsertHandles": [first(props(start_insert), 5, ""), first(props(end_insert), 5, "")],
"entityType": block_entity["type"],
"a": start_profile,
"b": end_profile,
})
return result
def main():
pairs = read_pairs(SOURCE)
blocks = parse_blocks(pairs)
entities = parse_modelspace(pairs)
center_x = 1_296_487.109248054
section_defs = [
{"id": "A", "station": 319.000, "yBounds": (-80_000, -70_000), "y0": -78_059.37855593952, "el0": 101.777, "yTop": -72_621.37855593952, "elTop": 107.215, "centerY": -77_742.31539793951, "insertY": -76_987.31555584459},
{"id": "B", "station": 339.000, "yBounds": (-99_000, -89_000), "y0": -97_286.37855593952, "el0": 101.292, "yTop": -90_721.37855593952, "elTop": 107.857, "centerY": -97_061.6690599395, "insertY": -96_306.66955584464},
]
grouped_a = cross_section(entities, section_defs[0]["yBounds"], center_x, section_defs[0]["y0"], section_defs[0]["el0"])
grouped_b = cross_section(entities, section_defs[1]["yBounds"], center_x, section_defs[1]["y0"], section_defs[1]["el0"])
if set(grouped_a) != set(grouped_b):
raise ValueError("A-A/B-B 구조선의 X 범위/레이어 구성이 일치하지 않습니다.")
parts = []
line_no = defaultdict(int)
for key in sorted(grouped_a):
a_list = sorted(grouped_a[key], key=lambda item: (item["sort"][0], item["sort"][1]))
b_list = sorted(grouped_b[key], key=lambda item: (item["sort"][0], item["sort"][1]))
if len(a_list) != len(b_list):
raise ValueError(f"단면 대응 선 개수가 다릅니다: {key}")
for a_item, b_item in zip(a_list, b_list):
source_layer = key[0]
group = "major" if source_layer == "CS-CONC-MAJR" else "minor"
line_no[group] += 1
parts.append({
"id": f"{group}-{line_no[group]:03d}",
"name": f"{source_layer} 구조선 {line_no[group]:03d}",
"layer": source_layer,
"group": group,
"sourceHandles": [a_item["handle"], b_item["handle"]],
"entityType": "LINE",
"a": a_item["profile"],
"b": b_item["profile"],
})
if line_no != {"major": 108, "minor": 6}:
raise ValueError(f"구조선 수 확인 실패: {dict(line_no)}")
parts += add_block_parts(parts, entities, blocks, center_x, section_defs)
units = alignment_units(entities)
for unit in units:
unit["parts"] = [dict(part, id=f"{unit['id']}-{part['id']}") for part in parts]
data = {
"source": "광터지하차도_BLOCK8.dxf",
"unit": "m",
"coordinateNote": "CAD 좌표 차이를 치수 문자열로 검산해 mm로 해석. 표고는 종단/단면 EL 주석에 연결.",
"sections": [
{"id": "A-A", "station": 319.000, "centerEL": section_defs[0]["elTop"] + (section_defs[0]["centerY"] - section_defs[0]["yTop"]) / 1000, "topEL": 107.215, "baseTopEL": 101.777, "baseBottomEL": 100.877, "sourceHandles": ["STA.0K+319.000", "CS-CNTL center mark", "EL.102.094", "EL.107.215", "EL.100.877"]},
{"id": "B-B", "station": 339.000, "centerEL": section_defs[1]["elTop"] + (section_defs[1]["centerY"] - section_defs[1]["yTop"]) / 1000, "topEL": 107.857, "baseTopEL": 101.292, "baseBottomEL": 100.392, "sourceHandles": ["STA.0K+339.000", "CS-CNTL center mark", "EL.101.517", "EL.107.857", "EL.100.392"]},
],
"sectionStationRange": [319.000, 339.000],
"modelStationRange": [319.010, 338.990],
"modelLength": 19.980,
"sectionTrim": 0.010,
"sectionLineCount": {"CS-CONC-MAJR": 108, "CS-CONC-MINR": 6},
"expandedBlock": {"name": "A$C2017021411375356", "layerCounts": {"CS-CONC-MAJR": 18, "CS-CONC-MINR": 0}, "curveCount": 2, "insertCount": 4},
"profileWidth": {"dimensionText": 20.300, "outerLineEnvelope": 20.500, "status": "dimension value and outer contour differ; retain separately"},
"units": units,
}
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"units": len(units), "lengths": [round(u["length"], 6) for u in units], "partsPerUnit": len(parts), "layers": dict(line_no), "expandedBlockPartsPerSide": 20}, ensure_ascii=False))
if __name__ == "__main__":
main()