원본 도면 뷰를 3D 모델 기준면에 투영
This commit is contained in:
1 parent
f8ecd4f5c0
commit
27b3230dba
18 files changed
+489
-15
No files matched your search
@@ -2,6 +2,8 @@
|
||||
"""Vite 빌드 결과를 독립 실행 가능한 BLOCK8 HTML로 묶는다."""
|
||||
|
||||
from html.parser import HTMLParser
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
@@ -33,6 +35,10 @@ def main():
|
||||
|
||||
js = (DIST / "assets" / parser.script).read_text(encoding="utf-8")
|
||||
css = (DIST / "assets" / parser.stylesheet).read_text(encoding="utf-8")
|
||||
image_map = {}
|
||||
for name in ("plan", "upper-plan", "longitudinal", "section-a", "section-b"):
|
||||
image = ROOT / "public/drawing-images" / f"BLOCK8_{name}.svg"
|
||||
image_map[name] = "data:image/svg+xml;base64," + base64.b64encode(image.read_bytes()).decode("ascii")
|
||||
# public 폴더 안의 자료 링크가 file:// 환경에서도 열리도록 상대 경로로 바꾼다.
|
||||
for path in ("analysis/", "drawings/", "BLOCK8_", "작업기록.txt"):
|
||||
js = js.replace(f"/{path}", f"./{path}")
|
||||
@@ -51,6 +57,7 @@ def main():
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script>window.BLOCK8_DRAWING_IMAGES={json.dumps(image_map, ensure_ascii=False)};</script>
|
||||
<script>{js}</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -25,19 +25,20 @@ GUIDE = """# 광터지하차도 BLOCK8 전달 자료 안내
|
||||
- `scripts/`: DXF 모델 추출, SVG 미리보기 생성, 단독 HTML 생성, 전달 ZIP 생성 코드
|
||||
- `public/`: 원본 DXF, 분석·작업·커밋 기록, 단독 HTML 결과물
|
||||
- `docs/`: 상세 분석과 모델 미리보기
|
||||
- `public/drawing-images/`: 도면 SVG 오버레이와 픽셀-CAD 연결 정보
|
||||
|
||||
## 포함하지 않은 항목
|
||||
|
||||
`node_modules/`, `dist/`, `.git/`은 포함하지 않았습니다. 의존성은 `npm install`로 설치하고, 빌드 결과는 `npm run build`로 다시 만들 수 있습니다. Git 커밋 목록은 `public/BLOCK8_커밋기록.md`에서 확인하세요.
|
||||
|
||||
현재 기준 커밋: `89065ab` (단독 HTML file 실행 호환성 개선)
|
||||
현재 압축본에는 DXF에서 생성한 평면·상부평면·종단·A-A/B-B 오버레이 이미지가 포함되어 있습니다. 3D 기준면 배치는 참고 시각화이며, 도면 요소의 최종 치수 검증은 원본 CAD에서 수행하세요.
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
files = [ROOT / "AGENTS.md", ROOT / "README.md", ROOT / "index.html", ROOT / "package.json", ROOT / "package-lock.json", ROOT / "vite.config.js"]
|
||||
for folder in ("src", "scripts", "docs", "public"):
|
||||
files.extend(path for path in (ROOT / folder).rglob("*") if path.is_file() and path.name not in EXCLUDED)
|
||||
files.extend(path for path in (ROOT / folder).rglob("*") if path.is_file() and path.name not in EXCLUDED and "__pycache__" not in path.parts and path.suffix != ".pyc")
|
||||
files = sorted(set(files))
|
||||
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/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'<path d="{path}" fill="none" stroke="{color}" stroke-width="{width}" stroke-linejoin="round"/>')
|
||||
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'<text x="{sx:.2f}" y="{sy:.2f}" font-size="{size:.2f}" text-anchor="{anchor}" fill="{color}" transform="rotate({-angle:.2f} {sx:.2f} {sy:.2f})">{escaped}</text>')
|
||||
|
||||
svg = [
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" width="{WIDTH}" height="{HEIGHT}" viewBox="0 0 {WIDTH} {HEIGHT}" role="img" aria-label="{escape(spec["title"])} 원본 DXF 영역">',
|
||||
f'<title>{escape(spec["title"])} · 원본 DXF 벡터 캡처</title>',
|
||||
f'<g>{"".join(elements)}</g>',
|
||||
'</svg>',
|
||||
]
|
||||
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()
|
||||
Reference in new issue
Block a user