334 lines
15 KiB
Python
334 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": f"BLOCK8 평면 중심선 {index + 1}",
|
|
"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
|
|
|
|
|
|
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()
|