#!/usr/bin/env python3 """BLOCK8 DXF에서 평면/종단/단면 영역을 벡터 SVG 참조 이미지로 추출한다.""" from collections import defaultdict from html import escape import json import math from pathlib import Path import re import sys ROOT = Path(__file__).resolve().parents[1] sys.dont_write_bytecode = True sys.path.insert(0, str(ROOT / "scripts")) import extract_block8_model as dxf OUTPUT = ROOT / "public/drawing-images" WIDTH = 1400 VIEWS = { "plan": {"title": "평면도", "bounds": (1_195_000, 1_230_000, -90_000, -61_000)}, "upper-plan": {"title": "상부평면도", "bounds": (1_230_000, 1_267_000, -90_000, -61_000)}, "longitudinal": {"title": "종단면도", "bounds": (1_213_000, 1_247_000, -108_000, -90_000)}, "section-a": {"title": "단면 A-A · STA. 0K+319.000", "bounds": (1_280_000, 1_312_000, -83_000, -64_000)}, "section-b": {"title": "단면 B-B · STA. 0K+339.000", "bounds": (1_280_000, 1_312_000, -102_000, -83_000)}, } ACI = { 1: "#d25245", 2: "#b58b32", 3: "#43845c", 4: "#438b9e", 5: "#5273a6", 6: "#995d96", 7: "#26332b", 8: "#777f78", 9: "#a1aaa2", } FALLBACK = { "CS-CONC-MAJR": "#26332b", "CS-CONC-MINR": "#3d817e", "CS-CNTL": "#c05045", "CS-DIML": "#ad584c", "CS-DIMT": "#43845c", "CS-NOTE": "#43845c", "CS-TEXT": "#43845c", "CZ-TEX1": "#995d96", "CZ-TEX2": "#43845c", "CS-SYMB": "#43845c", "CS-CUTL": "#5273a6", } def compose(a, b): return ( a[0]*b[0] + a[2]*b[1], a[1]*b[0] + a[3]*b[1], a[0]*b[2] + a[2]*b[3], a[1]*b[2] + a[3]*b[3], a[0]*b[4] + a[2]*b[5] + a[4], a[1]*b[4] + a[3]*b[5] + a[5], ) def transform(matrix, point): x, y = point return matrix[0]*x + matrix[2]*y + matrix[4], matrix[1]*x + matrix[3]*y + matrix[5] def make_insert_matrix(entity, block): p = dxf.props(entity) ix, iy = float(dxf.first(p, 10, "0")), float(dxf.first(p, 20, "0")) sx, sy = float(dxf.first(p, 41, "1")), float(dxf.first(p, 42, "1")) angle = math.radians(float(dxf.first(p, 50, "0"))) bx, by = block["base"][:2] co, si = math.cos(angle), math.sin(angle) a, c = co*sx, -si*sy b, e = si*sx, co*sy return a, b, c, e, ix-a*bx-c*by, iy-b*bx-e*by def entity_layer(entity, inherited=None): name = dxf.layer(entity) return inherited if name == "0" and inherited else name def parse_primitives(entity, blocks, matrix, inherited=None, depth=0, visited=()): if depth > 5: return [] kind = entity["type"] layer = entity_layer(entity, inherited) p = dxf.props(entity) if kind == "INSERT": name = dxf.first(p, 2, "") block = blocks.get(name) if not block or name in visited: return [] next_matrix = compose(matrix, make_insert_matrix(entity, block)) result = [] for child in block["entities"]: result.extend(parse_primitives(child, blocks, next_matrix, layer, depth+1, visited+(name,))) return result if kind in ("LINE", "LWPOLYLINE", "ARC", "CIRCLE", "SOLID", "TRACE"): points = [] if kind == "LINE": points = [(float(dxf.first(p, 10)), float(dxf.first(p, 20))), (float(dxf.first(p, 11)), float(dxf.first(p, 21)))] elif kind == "LWPOLYLINE": raw_x, raw_y = [float(v) for v in p.get(10, [])], [float(v) for v in p.get(20, [])] points = list(zip(raw_x, raw_y)) if int(dxf.first(p, 70, "0")) & 1 and points: points.append(points[0]) elif kind == "ARC": points = dxf.arc_points(entity, 48) elif kind == "CIRCLE": cx, cy, r = float(dxf.first(p, 10)), float(dxf.first(p, 20)), float(dxf.first(p, 40)) points = [(cx+r*math.cos(2*math.pi*i/64), cy+r*math.sin(2*math.pi*i/64)) for i in range(65)] else: points = [(float(dxf.first(p, x)), float(dxf.first(p, y))) for x, y in ((10,20),(11,21),(12,22),(13,23)) if x in p and y in p] if points: points.append(points[0]) return [("path", [transform(matrix, point) for point in points], layer)] if len(points) > 1 else [] if kind in ("TEXT", "MTEXT", "ATTRIB", "ATTDEF"): x, y = float(dxf.first(p, 10, "0")), float(dxf.first(p, 20, "0")) pos = transform(matrix, (x, y)) text = "".join(p.get(1, []) + p.get(3, [])) text = text.replace("\\P", " ").replace("%%d", "°").replace("%%p", "±").replace("%%c", "Ø") text = re.sub(r"\\[A-Za-z][^;]*;", "", text) size = float(dxf.first(p, 40, "250")) # Text heights are in drawing units; image uses the same uniform drawing scale. angle = float(dxf.first(p, 50, "0")) align = int(dxf.first(p, 72, "0")) anchor = {0: "start", 1: "middle", 2: "end"}.get(align, "start") return [("text", [pos, text, size, angle, anchor], layer)] if text.strip() else [] return [] def get_colors(pairs): begin, end = dxf.parse_sections(pairs, "TABLES") colors = {} for record in dxf.records(pairs, begin+2, end): if record["type"] != "LAYER": continue p = dxf.props(record) name = dxf.first(p, 2, "") try: colors[name] = ACI.get(abs(int(dxf.first(p, 62, "7"))), FALLBACK.get(name, "#374239")) except ValueError: colors[name] = FALLBACK.get(name, "#374239") return colors def render_view(name, spec, entities, blocks, colors): x0, x1, y0, y1 = spec["bounds"] HEIGHT = round(WIDTH * (y1-y0) / (x1-x0)) scale = WIDTH / (x1-x0) def screen(point): x, y = point return (x-x0)*scale, HEIGHT-(y-y0)*scale primitives = [] for entity in entities: primitives.extend(parse_primitives(entity, blocks, (1,0,0,1,0,0))) elements = [] for kind, data, layer in primitives: color = colors.get(layer, FALLBACK.get(layer, "#667267")) if kind == "path": px = [point[0] for point in data] py = [point[1] for point in data] if max(px) < x0 or min(px) > x1 or max(py) < y0 or min(py) > y1: continue # Canvas crop clips geometry outside the requested drawing region. points = [screen(point) for point in data] path = "M" + " L".join(f"{x:.2f},{y:.2f}" for x,y in points) width = 1.25 if layer == "CS-CONC-MAJR" else 0.8 elements.append(f'') else: (x,y), text, height, angle, anchor = data if not (x0-1000 <= x <= x1+1000 and y0-1000 <= y <= y1+1000): continue sx, sy = screen((x,y)) size = max(4, min(26, height*scale)) escaped = escape(text, quote=False) # Negative angle compensates for the SVG screen's downward-positive y axis. elements.append(f'{escaped}') svg = [ f'', f'{escape(spec["title"])} · 원본 DXF 벡터 캡처', f'{"".join(elements)}', '', ] path = OUTPUT / f"BLOCK8_{name}.svg" path.write_text("\n".join(svg), encoding="utf-8") print(f"{path.relative_to(ROOT)}: {len(elements)}개 도면 요소") return { "title": spec["title"], "pixelSize": [WIDTH, HEIGHT], "cadBounds": { "xMin": x0, "xMax": x1, "yMin": y0, "yMax": y1, }, "placement": None, } def main(): pairs = dxf.read_pairs(dxf.SOURCE) entities = dxf.parse_modelspace(pairs) blocks = dxf.parse_blocks(pairs) colors = get_colors(pairs) OUTPUT.mkdir(parents=True, exist_ok=True) metadata = {name: render_view(name, spec, entities, blocks, colors) for name, spec in VIEWS.items()} metadata["plan"]["placement"] = {"kind": "plan", "xOffset": 0, "displayEL": 108.8} metadata["upper-plan"]["placement"] = {"kind": "plan", "xOffset": -35_000, "displayEL": 108.8} metadata["longitudinal"]["placement"] = { "kind": "longitudinal", "stationCad": [1_218_301.059683612, 1_241_698.059683612], "elevationOffset": 205.2898, "lateralOffset": 15, } metadata["section-a"]["placement"] = { "kind": "section", "stationT": -0.0005005005, "cadXCenter": 1_296_487.109248054, "cadYBase": -78_059.37855593952, "baseEL": 101.777, } metadata["section-b"]["placement"] = { "kind": "section", "stationT": 1.0005005005, "cadXCenter": 1_296_487.109248054, "cadYBase": -97_286.37855593952, "baseEL": 101.292, } model_metadata = ROOT / "src/model/drawingViews.json" model_metadata.write_text(json.dumps(metadata, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(f"{model_metadata.relative_to(ROOT)}: 도면/모델 배치 기준 기록") if __name__ == "__main__": main()