공개용 정리본: 1차 제출본(도면 좌표·과업명 제거)으로 새 이력 시작 (SPEC 34장)
- 정리는 코치 세션이 저장소 밖 도구로 수행, 하이쿠 세션이 커밋. - 옛 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>
This commit is contained in:
commit
9068b6c7f2
31 files changed
+374207
No files matched your search
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"_": "설계 규칙 (SPEC 27장). 도면 치수·좌표는 넣지 않는다.",
|
||||
"parts": [
|
||||
{
|
||||
"id": "ladder_19B",
|
||||
"name": "사다리 (침사구 아래 동쪽 벽)",
|
||||
"kind": "ladder",
|
||||
"type": "ladder",
|
||||
"color": "#d0a040",
|
||||
"x0": 4668,
|
||||
"x1": 4708,
|
||||
"yc": 0,
|
||||
"width": 400,
|
||||
"z0": -68700,
|
||||
"z1": -66200,
|
||||
"rung": 300
|
||||
},
|
||||
{
|
||||
"id": "pump_pipe",
|
||||
"name": "펌프 배출관 (STS Ø100)",
|
||||
"kind": "pipe",
|
||||
"type": "ring",
|
||||
"color": "#9aa0a6",
|
||||
"cx": 0,
|
||||
"cy": 4220,
|
||||
"z0": -68800,
|
||||
"z1": -2000,
|
||||
"r_out": 57,
|
||||
"r_in": 50
|
||||
},
|
||||
{
|
||||
"id": "ladder_up_W",
|
||||
"name": "위쪽 사다리 서쪽 (1B→분기구 바닥)",
|
||||
"kind": "ladder_up",
|
||||
"type": "ladder",
|
||||
"color": "#d0a040",
|
||||
"x0": -4622,
|
||||
"x1": -4582,
|
||||
"yc": -1260,
|
||||
"width": 400,
|
||||
"z0": -7400,
|
||||
"z1": -4100,
|
||||
"rung": 300
|
||||
},
|
||||
{
|
||||
"id": "ladder_up_C",
|
||||
"name": "위쪽 사다리 가운데 (1B→맨홀→지표)",
|
||||
"kind": "ladder_up",
|
||||
"type": "ladder",
|
||||
"color": "#d0a040",
|
||||
"x0": -20,
|
||||
"x1": 20,
|
||||
"yc": 0,
|
||||
"width": 360,
|
||||
"z0": -7400,
|
||||
"z1": 0,
|
||||
"rung": 300
|
||||
},
|
||||
{
|
||||
"id": "ladder_vent_wall",
|
||||
"name": "환기구 벽 사다리 (1B→덕트 바닥)",
|
||||
"kind": "ladder_vent",
|
||||
"type": "ladder",
|
||||
"color": "#d0a040",
|
||||
"angle_deg": 300.3,
|
||||
"x0": 4582,
|
||||
"x1": 4622,
|
||||
"yc": 0,
|
||||
"width": 400,
|
||||
"z0": -7400,
|
||||
"z1": -4100,
|
||||
"rung": 300
|
||||
},
|
||||
{
|
||||
"id": "ladder_vent_tower",
|
||||
"name": "환기탑 사다리 (덕트 바닥→지표)",
|
||||
"kind": "ladder_vent",
|
||||
"type": "ladder",
|
||||
"color": "#d0a040",
|
||||
"angle_deg": 30.3,
|
||||
"x0": 10100,
|
||||
"x1": 10140,
|
||||
"yc": -8650,
|
||||
"width": 400,
|
||||
"z0": -4100,
|
||||
"z1": 0,
|
||||
"rung": 300
|
||||
}
|
||||
],
|
||||
"style": {
|
||||
"wall": {
|
||||
"color": "#8a9bb0",
|
||||
"alpha": 0.25
|
||||
},
|
||||
"topslab": {
|
||||
"color": "#c8c8c8"
|
||||
},
|
||||
"neck": {
|
||||
"color": "#b8b8b8"
|
||||
},
|
||||
"slab": {
|
||||
"color": "#c8c8c8"
|
||||
},
|
||||
"pad": {
|
||||
"color": "#e0b060"
|
||||
},
|
||||
"bottom": {
|
||||
"color": "#a0a0a0"
|
||||
},
|
||||
"lean": {
|
||||
"color": "#707070"
|
||||
},
|
||||
"tunnel": {
|
||||
"color": "#b08a5a"
|
||||
},
|
||||
"stair": {
|
||||
"color": "#7fb07f"
|
||||
},
|
||||
"fill": {
|
||||
"color": "#c8bfa8"
|
||||
},
|
||||
"weir": {
|
||||
"color": "#9a9a9a"
|
||||
},
|
||||
"pit": {
|
||||
"color": "#8fc3e0",
|
||||
"alpha": 0.25
|
||||
},
|
||||
"branch": {
|
||||
"color": "#9fb8c8"
|
||||
},
|
||||
"vent": {
|
||||
"color": "#b8c8a0"
|
||||
},
|
||||
"railing": {
|
||||
"color": "#d8c890"
|
||||
},
|
||||
"pipe": {
|
||||
"color": "#9aa0a6"
|
||||
},
|
||||
"ladder": {
|
||||
"color": "#d0a040"
|
||||
},
|
||||
"ladder_up": {
|
||||
"color": "#d0a040"
|
||||
},
|
||||
"ladder_vent": {
|
||||
"color": "#d0a040"
|
||||
}
|
||||
},
|
||||
"ids": {
|
||||
"fill_gravel": {
|
||||
"color": "#7a7a7a"
|
||||
},
|
||||
"fill_pad_high": {
|
||||
"color": "#e0d8c4"
|
||||
},
|
||||
"pit_tbm": {
|
||||
"color": "#a8d8f0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# dwg_to_dxf.sh — DWG 를 DXF(글자 파일)로 바꾼다 (GNU LibreDWG 의 dwg2dxf 사용, SPEC 28장)
|
||||
# 사용: bash tools/dwg_to_dxf.sh <입력.dwg> <출력.dxf>
|
||||
set -eu
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "사용법: bash tools/dwg_to_dxf.sh <입력.dwg> <출력.dxf>"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
DWG2DXF="$(command -v dwg2dxf || true)"
|
||||
if [ -z "$DWG2DXF" ] && [ -x "$HOME/.local/bin/dwg2dxf" ]; then
|
||||
DWG2DXF="$HOME/.local/bin/dwg2dxf"
|
||||
fi
|
||||
if [ -z "$DWG2DXF" ]; then
|
||||
echo "dwg2dxf 를 찾지 못했다. 설치는 README.md 의 「변환 패키지」 절을 본다."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
"$DWG2DXF" -y -o "$2" "$1"
|
||||
echo "완료: $2"
|
||||
@@ -0,0 +1,357 @@
|
||||
#!/usr/bin/env python3
|
||||
# dxf_to_svg.py — DXF 를 웹 왼쪽 칸의 SVG 로 바꾼다 (표준 라이브러리만, SPEC 29.1)
|
||||
# 사용: python3 tools/dxf_to_svg.py <drawing.dxf> <views.json> <출력.svg>
|
||||
# 상자·배치는 views.json 에서만 읽는다. 이 파일에는 도면 좌표가 없다.
|
||||
import colorsys
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
|
||||
COORD_X = ("10", "11")
|
||||
COORD_Y = ("20", "21")
|
||||
ARC_STEP = 2.0 # 원호를 꺾은선으로 나눌 때 한 조각의 각도 (도)
|
||||
TEXT_EM = 1.38942 # 글자 크기 = DXF 글자 높이 × 이 값 (원본 SVG 와 같게)
|
||||
HIDDEN_LAYER = "Defpoints"
|
||||
MAX_DEPTH = 8 # 블록 안 블록 깊이 한계
|
||||
BASIC_ACI = {1: (255, 0, 0), 2: (255, 255, 0), 3: (0, 255, 0), 4: (0, 255, 255), 5: (0, 0, 255),
|
||||
6: (255, 0, 255), 7: (0, 0, 0), 8: (128, 128, 128), 9: (192, 192, 192)}
|
||||
GRAY_ACI = (51, 91, 132, 173, 214, 255) # 250~255
|
||||
BRIGHT_ACI = (1, 1, 0.8, 0.8, 0.6, 0.6, 0.5, 0.5, 0.3, 0.3)
|
||||
TEXT_ANCHOR_72 = {1: "middle", 2: "end", 4: "middle", 5: "middle"}
|
||||
|
||||
|
||||
def aci_rgb(i):
|
||||
if i in BASIC_ACI:
|
||||
return BASIC_ACI[i]
|
||||
if 250 <= i <= 255:
|
||||
g = GRAY_ACI[i - 250]
|
||||
return (g, g, g)
|
||||
if 10 <= i <= 249:
|
||||
hue = ((i - 10) // 10) * 15 / 360
|
||||
sat = 1.0 if i % 2 == 0 else 0.5
|
||||
r, g, b = colorsys.hsv_to_rgb(hue, sat, BRIGHT_ACI[i % 10])
|
||||
return (round(r * 255), round(g * 255), round(b * 255))
|
||||
return BASIC_ACI[7]
|
||||
|
||||
|
||||
def read_pairs(path):
|
||||
# DXF 는 (그룹 코드 줄, 값 줄) 쌍이다
|
||||
with open(path, encoding="utf-8", errors="replace") as f:
|
||||
lines = [ln[:-1] if ln.endswith("\r") else ln for ln in f.read().split("\n")]
|
||||
if lines and lines[-1] == "":
|
||||
lines.pop()
|
||||
return [(lines[i].strip(), lines[i + 1].strip()) for i in range(0, len(lines) - 1, 2)]
|
||||
|
||||
|
||||
def sections(pairs):
|
||||
# 구역 이름 → (내용 시작 줄, ENDSEC 줄)
|
||||
out = {}
|
||||
for i in range(len(pairs) - 1):
|
||||
if pairs[i] == ("0", "SECTION") and pairs[i + 1][0] == "2":
|
||||
end = next(j for j in range(i + 2, len(pairs)) if pairs[j] == ("0", "ENDSEC"))
|
||||
out[pairs[i + 1][1]] = (i + 2, end)
|
||||
return out
|
||||
|
||||
|
||||
def groups_of(pairs, lo, hi):
|
||||
# 객체 묶음: 0 줄마다 시작. POLYLINE 과 코드 66 이 1 인 INSERT 는 SEQEND 까지 한 묶음
|
||||
groups = []
|
||||
i = lo
|
||||
while i < hi:
|
||||
j = i + 1
|
||||
while j < hi and pairs[j][0] != "0":
|
||||
j += 1
|
||||
head = pairs[i][1]
|
||||
flag66 = next((v for c, v in pairs[i + 1:j] if c == "66"), None)
|
||||
if head == "POLYLINE" or (head == "INSERT" and flag66 == "1"):
|
||||
while j < hi and pairs[j] != ("0", "SEQEND"):
|
||||
j += 1
|
||||
while j < hi and pairs[j][0] != "0":
|
||||
j += 1
|
||||
if j < hi:
|
||||
j += 1
|
||||
while j < hi and pairs[j][0] != "0":
|
||||
j += 1
|
||||
groups.append(pairs[i:j])
|
||||
i = j
|
||||
return groups
|
||||
|
||||
|
||||
def val(g, code):
|
||||
return next((v for c, v in g[1:] if c == code), None)
|
||||
|
||||
|
||||
def num(g, code, default=0.0):
|
||||
v = val(g, code)
|
||||
return float(v) if v is not None else default
|
||||
|
||||
|
||||
def layers_of(pairs, lo, hi):
|
||||
# 레이어 표: 이름 → 색(절댓값), 꺼짐(음수), 얼림(70 의 1 비트)
|
||||
tab = {}
|
||||
for g in groups_of(pairs, lo, hi):
|
||||
if g[0][1] != "LAYER":
|
||||
continue
|
||||
color = int(val(g, "62") or 7)
|
||||
tab[val(g, "2")] = {"color": abs(color), "off": color < 0, "frozen": bool(int(val(g, "70") or 0) & 1)}
|
||||
return tab
|
||||
|
||||
|
||||
def blocks_of(pairs, lo, hi):
|
||||
# 블록 이름 → 기준점과 안의 객체 묶음
|
||||
blocks = {}
|
||||
cur = None
|
||||
for g in groups_of(pairs, lo, hi):
|
||||
head = g[0][1]
|
||||
if head == "BLOCK":
|
||||
cur = {"base": (num(g, "10"), num(g, "20")), "ents": []}
|
||||
blocks[val(g, "2")] = cur
|
||||
elif head == "ENDBLK":
|
||||
cur = None
|
||||
elif cur is not None:
|
||||
cur["ents"].append(g)
|
||||
return blocks
|
||||
|
||||
|
||||
def pts_of(g):
|
||||
# 객체 좌표 (select_views.py 와 같은 규칙): 코드 10·11 의 X 와 뒤따르는 20·21 의 Y.
|
||||
# POLYLINE 은 머리 기준점을 빼고 VERTEX 부터, CIRCLE·ARC 는 중심 ± 반지름도
|
||||
start = 0
|
||||
if g[0][1] == "POLYLINE":
|
||||
start = next((k for k, p in enumerate(g) if p == ("0", "VERTEX")), None)
|
||||
if start is None:
|
||||
return []
|
||||
body = g[start:]
|
||||
pts = []
|
||||
for k, (c, v) in enumerate(body[:-1]):
|
||||
if c in COORD_X and body[k + 1][0] in COORD_Y:
|
||||
pts.append((float(v), float(body[k + 1][1])))
|
||||
if g[0][1] in ("CIRCLE", "ARC") and pts:
|
||||
r = num(g, "40")
|
||||
cx, cy = pts[0]
|
||||
pts += [(cx - r, cy - r), (cx + r, cy + r)]
|
||||
return pts
|
||||
|
||||
|
||||
def inside(pts, box):
|
||||
x0, y0, x1, y1 = box
|
||||
return all(x0 <= x <= x1 and y0 <= y <= y1 for x, y in pts)
|
||||
|
||||
|
||||
def mul(p, q):
|
||||
# 행렬 (a, b, c, d, e, f): x' = a x + c y + e, y' = b x + d y + f. mul(p, q) = p∘q, q 를 먼저 적용한다
|
||||
a1, b1, c1, d1, e1, f1 = p
|
||||
a2, b2, c2, d2, e2, f2 = q
|
||||
return (a1 * a2 + c1 * b2, b1 * a2 + d1 * b2, a1 * c2 + c1 * d2, b1 * c2 + d1 * d2,
|
||||
a1 * e2 + c1 * f2 + e1, b1 * e2 + d1 * f2 + f1)
|
||||
|
||||
|
||||
def apply(m, p):
|
||||
a, b, c, d, e, f = m
|
||||
return (a * p[0] + c * p[1] + e, b * p[0] + d * p[1] + f)
|
||||
|
||||
|
||||
def det(m):
|
||||
return m[0] * m[3] - m[1] * m[2]
|
||||
|
||||
|
||||
def dir_angle(m, deg):
|
||||
# 도면에서 deg 방향인 선이 SVG 에서 향하는 각도 (도)
|
||||
rot = math.radians(deg)
|
||||
vx = m[0] * math.cos(rot) + m[2] * math.sin(rot)
|
||||
vy = m[1] * math.cos(rot) + m[3] * math.sin(rot)
|
||||
return math.degrees(math.atan2(vy, vx))
|
||||
|
||||
|
||||
def fmt(v):
|
||||
s = ("%.3f" % v).rstrip("0").rstrip(".")
|
||||
return "0" if s in ("", "-0") else s
|
||||
|
||||
|
||||
def pts_attr(points):
|
||||
return " ".join(f"{fmt(x)},{fmt(y)}" for x, y in points)
|
||||
|
||||
|
||||
def arc_pts(p, q, b):
|
||||
# 꼭짓점 p→q 사이 볼록 원호(bulge b, 양수 = 반시계)의 안쪽 점들 (p, q 제외)
|
||||
th = 4 * math.atan(b)
|
||||
chord = math.dist(p, q)
|
||||
h = chord / 2 / math.tan(th / 2)
|
||||
nx, ny = -(q[1] - p[1]) / chord, (q[0] - p[0]) / chord
|
||||
cx = (p[0] + q[0]) / 2 + nx * h
|
||||
cy = (p[1] + q[1]) / 2 + ny * h
|
||||
r = math.dist(p, (cx, cy))
|
||||
a0 = math.atan2(p[1] - cy, p[0] - cx)
|
||||
k = max(2, math.ceil(abs(math.degrees(th)) / ARC_STEP))
|
||||
return [(cx + r * math.cos(a0 + th * t / k), cy + r * math.sin(a0 + th * t / k)) for t in range(1, k)]
|
||||
|
||||
|
||||
def verts_of(codes):
|
||||
# 꼭짓점: 코드 10 마다 새 점, 20 = Y, 42 = 볼록
|
||||
vs = []
|
||||
for c, v in codes:
|
||||
if c == "10":
|
||||
vs.append([float(v), 0.0, 0.0])
|
||||
elif c == "20" and vs:
|
||||
vs[-1][1] = float(v)
|
||||
elif c == "42" and vs:
|
||||
vs[-1][2] = float(v)
|
||||
return vs
|
||||
|
||||
|
||||
def mtext_clean(s):
|
||||
# MTEXT 서식 코드 지우기: \P 는 공백, \f…; 와 높이·폭 같은 코드, 중괄호
|
||||
s = s.replace("\\P", " ")
|
||||
s = re.sub(r"\\[fF][^;]*;", "", s)
|
||||
s = re.sub(r"\\[HhWwQqTtCcAa][^;]*;", "", s)
|
||||
return s.replace("{", "").replace("}", "")
|
||||
|
||||
|
||||
def text_clean(s):
|
||||
return s.replace("%%c", "Ø").replace("%%d", "°").replace("%%p", "±").replace("%%u", "")
|
||||
|
||||
|
||||
def xml_escape(s):
|
||||
return s.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
||||
|
||||
|
||||
def emit(g, m, layers, blocks, parent_layer, inherit, out, depth):
|
||||
# 객체 하나를 m(도면 → SVG 행렬)으로 옮겨 색별 목록 out 에 넣는다
|
||||
head = g[0][1]
|
||||
layer = val(g, "8") or "0"
|
||||
if depth and layer == "0":
|
||||
layer = parent_layer
|
||||
lay = layers.get(layer, {"color": 7, "off": False, "frozen": False})
|
||||
if lay["off"] or lay["frozen"] or layer == HIDDEN_LAYER:
|
||||
return
|
||||
c = int(val(g, "62") or 256)
|
||||
if c == 256:
|
||||
rgb = aci_rgb(lay["color"])
|
||||
elif c == 0:
|
||||
rgb = inherit if inherit is not None else aci_rgb(lay["color"])
|
||||
else:
|
||||
rgb = aci_rgb(abs(c))
|
||||
key = "rgb(%d,%d,%d)" % rgb
|
||||
els = out.setdefault(key, [])
|
||||
scale = math.sqrt(abs(det(m)))
|
||||
|
||||
if head == "LINE":
|
||||
pts = [apply(m, (num(g, "10"), num(g, "20"))), apply(m, (num(g, "11"), num(g, "21")))]
|
||||
els.append(f'<polyline points="{pts_attr(pts)}" />')
|
||||
elif head in ("LWPOLYLINE", "POLYLINE"):
|
||||
codes = [p for p in g[1:] if p[0] != "0"] if head == "LWPOLYLINE" else \
|
||||
[p for p in g[next(k for k, q in enumerate(g) if q == ("0", "VERTEX")):] if p[0] != "0"]
|
||||
vs = verts_of(codes)
|
||||
closed = bool(int(val(g, "70") or 0) & 1)
|
||||
if len(vs) >= 2:
|
||||
ring = vs + ([vs[0]] if closed else [])
|
||||
pts = []
|
||||
for (x0, y0, b0), (x1, y1, _) in zip(ring, ring[1:]):
|
||||
pts.append((x0, y0))
|
||||
if b0:
|
||||
pts += arc_pts((x0, y0), (x1, y1), b0)
|
||||
pts.append((ring[-1][0], ring[-1][1]))
|
||||
els.append(f'<polyline points="{pts_attr([apply(m, p) for p in pts])}" />')
|
||||
elif head == "LEADER":
|
||||
vs = verts_of([p for p in g[1:] if p[0] in ("10", "20")])
|
||||
if len(vs) >= 2:
|
||||
els.append(f'<polyline points="{pts_attr([apply(m, (v[0], v[1])) for v in vs])}" />')
|
||||
elif head == "CIRCLE":
|
||||
cx, cy = apply(m, (num(g, "10"), num(g, "20")))
|
||||
els.append(f'<circle cx="{fmt(cx)}" cy="{fmt(cy)}" r="{fmt(num(g, "40") * scale)}" />')
|
||||
elif head == "ARC":
|
||||
cx, cy, r = num(g, "10"), num(g, "20"), num(g, "40")
|
||||
s0 = num(g, "50")
|
||||
span = (num(g, "51") - s0) % 360 or 360
|
||||
k = max(2, math.ceil(span / ARC_STEP))
|
||||
pts = [(cx + r * math.cos(math.radians(s0 + span * t / k)), cy + r * math.sin(math.radians(s0 + span * t / k)))
|
||||
for t in range(k + 1)]
|
||||
els.append(f'<polyline points="{pts_attr([apply(m, p) for p in pts])}" />')
|
||||
elif head == "SOLID":
|
||||
# 채움 사각형: 점 순서 10·11·13·12
|
||||
order = [("10", "20"), ("11", "21"), ("13", "23"), ("12", "22")]
|
||||
pts = [apply(m, (num(g, x), num(g, y))) for x, y in order]
|
||||
els.append(f'<polygon points="{pts_attr(pts)}" fill="{key}" stroke="none" />')
|
||||
elif head == "TEXT":
|
||||
h = num(g, "40")
|
||||
ax, ay = ("11", "21") if (val(g, "72") or "0") != "0" or (val(g, "73") or "0") != "0" else ("10", "20")
|
||||
x, y = apply(m, (num(g, ax), num(g, ay)))
|
||||
ang = dir_angle(m, num(g, "50"))
|
||||
anchor = TEXT_ANCHOR_72.get(int(val(g, "72") or 0), "start")
|
||||
tr = f' transform="rotate({fmt(ang)} {fmt(x)} {fmt(y)})"' if abs(ang) > 1e-9 else ""
|
||||
els.append(f'<text x="{fmt(x)}" y="{fmt(y)}" font-family="Dotum,\'sans-serif\'" font-size="{fmt(h * TEXT_EM * scale)}" '
|
||||
f'text-anchor="{anchor}" fill="{key}" stroke="none"{tr}>{xml_escape(text_clean(val(g, "1") or ""))}</text>')
|
||||
elif head == "MTEXT":
|
||||
h = num(g, "40")
|
||||
text = "".join(v for c, v in g if c == "3") + (val(g, "1") or "")
|
||||
x, y = apply(m, (num(g, "10"), num(g, "20")))
|
||||
ang = dir_angle(m, num(g, "50"))
|
||||
col = (int(val(g, "71") or 1) - 1) % 3
|
||||
anchor = ("start", "middle", "end")[col]
|
||||
tr = f' transform="rotate({fmt(ang)} {fmt(x)} {fmt(y)})"' if abs(ang) > 1e-9 else ""
|
||||
els.append(f'<text x="{fmt(x)}" y="{fmt(y)}" font-family="Dotum,\'sans-serif\'" font-size="{fmt(h * TEXT_EM * scale)}" '
|
||||
f'text-anchor="{anchor}" fill="{key}" stroke="none"{tr}>{xml_escape(mtext_clean(text))}</text>')
|
||||
elif head == "INSERT":
|
||||
name = val(g, "2")
|
||||
if name in blocks and depth < MAX_DEPTH:
|
||||
# 블록 기준점을 빼고 (배율 → 회전 → 위치) 순으로 놓는다
|
||||
bx, by = blocks[name]["base"]
|
||||
sx, sy = num(g, "41", 1.0), num(g, "42", 1.0)
|
||||
rot = math.radians(num(g, "50"))
|
||||
ix, iy = num(g, "10"), num(g, "20")
|
||||
t = mul((1, 0, 0, 1, ix, iy), mul((math.cos(rot), math.sin(rot), -math.sin(rot), math.cos(rot), 0, 0),
|
||||
mul((sx, 0, 0, sy, 0, 0), (1, 0, 0, 1, -bx, -by))))
|
||||
for child in blocks[name]["ents"]:
|
||||
emit(child, mul(m, t), layers, blocks, layer, rgb, out, depth + 1)
|
||||
elif head == "DIMENSION":
|
||||
name = val(g, "2")
|
||||
if name in blocks and depth < MAX_DEPTH:
|
||||
for child in blocks[name]["ents"]:
|
||||
emit(child, m, layers, blocks, layer, rgb, out, depth + 1)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 4:
|
||||
print("사용법: python3 tools/dxf_to_svg.py <drawing.dxf> <views.json> <출력.svg>")
|
||||
return 2
|
||||
with open(argv[2], encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
pairs = read_pairs(argv[1])
|
||||
secs = sections(pairs)
|
||||
layers = layers_of(pairs, *secs["TABLES"])
|
||||
blocks = blocks_of(pairs, *secs["BLOCKS"])
|
||||
groups = groups_of(pairs, *secs["ENTITIES"])
|
||||
|
||||
out = {}
|
||||
kept = 0
|
||||
for g in groups:
|
||||
pts = pts_of(g)
|
||||
if not pts:
|
||||
continue
|
||||
k = next((k for k, b in enumerate(cfg["boxes"]) if inside(pts, b["range"])), None)
|
||||
if k is None:
|
||||
continue
|
||||
dx, dy = cfg["boxes"][k]["place"]
|
||||
# 도면 (X, Y) → SVG (X − dx, dy − Y): 축척 없이 옮기고 Y 를 뒤집는다
|
||||
emit(g, (1.0, 0.0, 0.0, -1.0, -dx, dy), layers, blocks, "0", None, out, 0)
|
||||
kept += 1
|
||||
|
||||
vb = " ".join(str(v) for v in cfg["viewBox"])
|
||||
lines = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{vb}">',
|
||||
"<style>* { vector-effect: non-scaling-stroke; }</style>"]
|
||||
n_el = 0
|
||||
for key, els in out.items():
|
||||
lines.append(f'<g fill="none" stroke="{key}" stroke-width="1">')
|
||||
lines += els
|
||||
lines.append("</g>")
|
||||
n_el += len(els)
|
||||
lines.append("</svg>")
|
||||
with open(argv[3], "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
print(f"완료: {argv[3]} (객체 {kept}개 → 요소 {n_el}개)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# make_model.sh — DWG 한 장에서 추출 모델까지 한 번에 만든다 (SPEC 28장)
|
||||
# 사용: bash tools/make_model.sh [입력.dwg] [출력 폴더]
|
||||
# 기본: 입력 private/drawing.dwg, 출력 private/
|
||||
# 만드는 것: full.dxf(전체 DXF) → drawing.dxf(상자 안만) → drawing_gen.svg(웹 도면) → model_extracted.js
|
||||
# 출력 폴더가 private/ 이면 리포 루트의 model_extracted.js 도 새로 쓴다.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
IN="${1:-$ROOT/private/drawing.dwg}"
|
||||
OUT="${2:-$ROOT/private}"
|
||||
|
||||
if [ ! -f "$IN" ]; then
|
||||
echo "입력 DWG 가 없다: $IN"
|
||||
exit 2
|
||||
fi
|
||||
mkdir -p "$OUT"
|
||||
OUT="$(cd "$OUT" && pwd)"
|
||||
|
||||
bash "$ROOT/tools/dwg_to_dxf.sh" "$IN" "$OUT/full.dxf"
|
||||
python3 "$ROOT/tools/select_views.py" "$OUT/full.dxf" "$ROOT/tools/views.json" "$OUT/drawing.dxf"
|
||||
python3 "$ROOT/tools/dxf_to_svg.py" "$OUT/drawing.dxf" "$ROOT/tools/views.json" "$OUT/drawing_gen.svg"
|
||||
python3 "$ROOT/tools/extract_from_dxf.py" "$OUT/drawing.dxf" "$OUT/model_extracted.js" "$ROOT/tools/views.json"
|
||||
|
||||
if [ "$OUT" = "$ROOT/private" ]; then
|
||||
cp "$OUT/model_extracted.js" "$ROOT/model_extracted.js"
|
||||
echo "갱신: $ROOT/model_extracted.js"
|
||||
fi
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
# prep_drawing.py — 도면 SVG 를 화면에서 선명하게 만든다 (표준 라이브러리만 사용)
|
||||
# 사용: python3 tools/prep_drawing.py <원본.svg> private/drawing.svg
|
||||
# 원본은 고치지 않는다. 0 이 아닌 stroke-width 를 1 로 바꾸고,
|
||||
# 루트 <svg> 바로 안에 <style> 을 넣어 모든 선이 화면 1px 로 그려지게 한다.
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def prep(text):
|
||||
# 선 굵기 속성: stroke-width="0.75" → "1" (0 은 그대로 둔다: 선 없음)
|
||||
def fix_attr(m):
|
||||
value = m.group(1)
|
||||
try:
|
||||
if float(value) == 0:
|
||||
return m.group(0)
|
||||
except ValueError:
|
||||
return m.group(0)
|
||||
return 'stroke-width="1"'
|
||||
|
||||
text = re.sub(r'stroke-width="([^"]*)"', fix_attr, text)
|
||||
|
||||
# style 속성 안의 stroke-width:0.75 도 같은 규칙
|
||||
def fix_style(m):
|
||||
try:
|
||||
if float(m.group(1)) == 0:
|
||||
return m.group(0)
|
||||
except ValueError:
|
||||
return m.group(0)
|
||||
return 'stroke-width:1'
|
||||
|
||||
text = re.sub(r'stroke-width:\s*([0-9.]+)', fix_style, text)
|
||||
|
||||
# 루트 <svg ...> 바로 뒤에 모든 요소의 선을 화면 기준으로 고정
|
||||
style = '<style>* { vector-effect: non-scaling-stroke; }</style>'
|
||||
text, n = re.subn(r'(<svg\b[^>]*>)', lambda m: m.group(1) + style, text, count=1)
|
||||
if n != 1:
|
||||
raise SystemExit('루트 <svg> 태그를 찾지 못했다')
|
||||
return text
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 3:
|
||||
print('사용법: python3 tools/prep_drawing.py <원본.svg> <출력.svg>')
|
||||
return 2
|
||||
src, dst = argv[1], argv[2]
|
||||
with open(src, encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
with open(dst, 'w', encoding='utf-8') as f:
|
||||
f.write(prep(text))
|
||||
print(f'완료: {dst}')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
# select_views.py — DXF 에서 이 과제가 쓰는 그림 상자 안의 객체만 남긴다 (표준 라이브러리만, SPEC 28장)
|
||||
# 사용: python3 tools/select_views.py <전체.dxf> <views.json> <출력.dxf>
|
||||
# 상자·뺄 레이어·뺄 블록 이름은 views.json 에서만 읽는다. 이 파일에는 도면 좌표·레이어 이름이 없다.
|
||||
import json
|
||||
import sys
|
||||
|
||||
COORD_X = ("10", "11")
|
||||
COORD_Y = ("20", "21")
|
||||
|
||||
|
||||
def read_pairs(path):
|
||||
# DXF 는 (코드 줄, 값 줄) 쌍이다. 원본 글자를 그대로 두려고 줄 끝만 떼고 저장한다.
|
||||
with open(path, encoding="utf-8", errors="surrogateescape", newline="") as f:
|
||||
lines = f.read().split("\n")
|
||||
lines = [ln[:-1] if ln.endswith("\r") else ln for ln in lines]
|
||||
if lines and lines[-1] == "":
|
||||
lines.pop()
|
||||
return [(lines[i], lines[i + 1]) for i in range(0, len(lines) - 1, 2)]
|
||||
|
||||
|
||||
def code_of(pair):
|
||||
return pair[0].strip()
|
||||
|
||||
|
||||
def value_of(pair):
|
||||
return pair[1].strip()
|
||||
|
||||
|
||||
def find_entities(pairs):
|
||||
# ENTITIES 구역의 시작(SECTION 다음 줄이 ENTITIES)과 끝(ENDSEC) 위치
|
||||
for s in range(len(pairs) - 1):
|
||||
if code_of(pairs[s]) == "0" and value_of(pairs[s]) == "SECTION" and value_of(pairs[s + 1]) == "ENTITIES":
|
||||
for e in range(s + 2, len(pairs)):
|
||||
if code_of(pairs[e]) == "0" and value_of(pairs[e]) == "ENDSEC":
|
||||
return s, e
|
||||
raise SystemExit("ENTITIES 구역을 찾지 못했다")
|
||||
|
||||
|
||||
def group_entities(body):
|
||||
# 객체 묶음: 보통은 0 줄 하나에서 다음 0 줄 전까지.
|
||||
# POLYLINE 은 SEQEND 까지. 코드 66 이 1 인 INSERT 는 ATTRIB…SEQEND 까지.
|
||||
groups = []
|
||||
i = 0
|
||||
while i < len(body):
|
||||
start = i
|
||||
i += 1
|
||||
while i < len(body) and code_of(body[i]) != "0":
|
||||
i += 1
|
||||
head_type = value_of(body[start])
|
||||
if head_type == "POLYLINE" or (head_type == "INSERT" and has_attrib_flag(body[start:i])):
|
||||
while i < len(body) and not (code_of(body[i]) == "0" and value_of(body[i]) == "SEQEND"):
|
||||
i += 1
|
||||
if i < len(body):
|
||||
i += 1 # SEQEND 자체도 묶음에 넣는다
|
||||
# 다음 객체 머리(0 줄)까지 같은 묶음
|
||||
while i < len(body) and code_of(body[i]) != "0":
|
||||
i += 1
|
||||
groups.append(body[start:i])
|
||||
return groups
|
||||
|
||||
|
||||
def has_attrib_flag(head_pairs):
|
||||
# INSERT 머리(첫 0 줄 다음) 안의 코드 66 값이 1 이면 속성이 뒤따른다
|
||||
for p in head_pairs[1:]:
|
||||
if code_of(p) == "66":
|
||||
return value_of(p) == "1"
|
||||
return False
|
||||
|
||||
|
||||
def head_flag(group, code):
|
||||
# 묶음 머리 객체의 코드 값 (ATTRIB 등 뒤 객체 값은 보지 않는다)
|
||||
for p in group[1:]:
|
||||
if code_of(p) == "0":
|
||||
break
|
||||
if code_of(p) == code:
|
||||
return value_of(p)
|
||||
return None
|
||||
|
||||
|
||||
def coords(group):
|
||||
# 좌표: 코드 10·11 의 X 와 그 뒤 20·21 의 Y. POLYLINE 은 머리의 기준점을 빼고 VERTEX 부터.
|
||||
start = 0
|
||||
if value_of(group[0]) == "POLYLINE":
|
||||
for k, p in enumerate(group):
|
||||
if code_of(p) == "0" and value_of(p) == "VERTEX":
|
||||
start = k
|
||||
break
|
||||
else:
|
||||
return []
|
||||
pts = []
|
||||
body = group[start:]
|
||||
for k, p in enumerate(body):
|
||||
if code_of(p) in COORD_X and k + 1 < len(body) and code_of(body[k + 1]) in COORD_Y:
|
||||
try:
|
||||
pts.append((float(value_of(p)), float(value_of(body[k + 1]))))
|
||||
except ValueError:
|
||||
pass
|
||||
# CIRCLE·ARC: 중심 ± 반지름(코드 40)
|
||||
if value_of(group[0]) in ("CIRCLE", "ARC") and pts:
|
||||
r = head_flag(group, "40")
|
||||
try:
|
||||
r = float(r)
|
||||
except (TypeError, ValueError):
|
||||
r = 0.0
|
||||
cx, cy = pts[0]
|
||||
pts += [(cx - r, cy - r), (cx + r, cy + r)]
|
||||
return pts
|
||||
|
||||
|
||||
def dropped(group, cfg):
|
||||
head = group[0]
|
||||
if head_flag(group, "67") == "1":
|
||||
return True # 배치 공간
|
||||
if head_flag(group, "8") in cfg["drop_layers"]:
|
||||
return True
|
||||
if value_of(head) == "INSERT" and cfg["drop_block_word"] in (head_flag(group, "2") or ""):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def inside(pts, box):
|
||||
x0, y0, x1, y1 = box
|
||||
return all(x0 <= x <= x1 and y0 <= y <= y1 for x, y in pts)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 4:
|
||||
print("사용법: python3 tools/select_views.py <전체.dxf> <views.json> <출력.dxf>")
|
||||
return 2
|
||||
with open(argv[2], encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
boxes = [b["range"] for b in cfg["boxes"]]
|
||||
|
||||
pairs = read_pairs(argv[1])
|
||||
s, e = find_entities(pairs)
|
||||
groups = group_entities(pairs[s + 2:e])
|
||||
|
||||
kept, allpts = [], []
|
||||
for g in groups:
|
||||
if dropped(g, cfg):
|
||||
continue
|
||||
pts = coords(g)
|
||||
if not pts:
|
||||
continue
|
||||
if any(inside(pts, b) for b in boxes):
|
||||
kept.append(g)
|
||||
allpts += pts
|
||||
|
||||
# 헤더의 $EXTMIN·$EXTMAX 를 남긴 좌표의 최소·최대로 바꾼다 (헤더 안 10/20 줄만)
|
||||
head = pairs[:s]
|
||||
if allpts:
|
||||
xs = [p[0] for p in allpts]
|
||||
ys = [p[1] for p in allpts]
|
||||
new_min, new_max = (min(xs), min(ys)), (max(xs), max(ys))
|
||||
for k in range(len(head) - 1):
|
||||
name = value_of(head[k]) if code_of(head[k]) == "9" else None
|
||||
if name not in ("$EXTMIN", "$EXTMAX"):
|
||||
continue
|
||||
target = new_min if name == "$EXTMIN" else new_max
|
||||
j = k + 1
|
||||
while j + 1 < len(head) and code_of(head[j]) != "9":
|
||||
if code_of(head[j]) == "10":
|
||||
head[j] = (head[j][0], repr(target[0]))
|
||||
elif code_of(head[j]) == "20":
|
||||
head[j] = (head[j][0], repr(target[1]))
|
||||
j += 1
|
||||
|
||||
out = head + pairs[s:s + 2] + [p for g in kept for p in g] + pairs[e:]
|
||||
with open(argv[3], "w", encoding="utf-8", errors="surrogateescape", newline="") as f:
|
||||
for c, v in out:
|
||||
f.write(c + "\r\n" + v + "\r\n")
|
||||
print(f"완료: {argv[3]} (객체 {len(kept)}개 / 전체 {len(groups)}개)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"_": "도면 범위 지정 (사람 입력, SPEC 28장). DWG 한 파일에 수직구 두 개와 옛 개정판이 함께 있어, 이 과제가 쓰는 그림 상자를 사람이 정한다. 좌표는 DWG 모형 공간(m). place = 그 상자의 점 (X, Y) 를 SVG (X − place[0], place[1] − Y) 에 놓는다(축척 없이 옮김, 1 = 1 m). viewBox·fit 은 왼쪽 도면 칸의 크기·처음 화면.",
|
||||
"boxes": [
|
||||
{
|
||||
"name": "전체 단면 A-A·B-B",
|
||||
"range": [
|
||||
11307.483,
|
||||
8061.096,
|
||||
11407.483,
|
||||
8151.096
|
||||
],
|
||||
"place": [
|
||||
11200.381,
|
||||
8314.952
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "평면도 1·2",
|
||||
"range": [
|
||||
11527.483,
|
||||
8103.096,
|
||||
11555.483,
|
||||
8149.096
|
||||
],
|
||||
"place": [
|
||||
11313.624,
|
||||
8313.914
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "단면 A-A·B-B (4B~15B 생략)",
|
||||
"range": [
|
||||
11570.483,
|
||||
8099.096,
|
||||
11619.483,
|
||||
8149.096
|
||||
],
|
||||
"place": [
|
||||
11322.610,
|
||||
8313.912
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "슬래브 평면 5개",
|
||||
"range": [
|
||||
11653.483,
|
||||
8103.096,
|
||||
11725.483,
|
||||
8147.096
|
||||
],
|
||||
"place": [
|
||||
11437.082,
|
||||
8360.590
|
||||
]
|
||||
}
|
||||
],
|
||||
"drop_layers": [
|
||||
"T-타이틀",
|
||||
"0-도곽",
|
||||
"PDF_문자",
|
||||
"Ptpoint",
|
||||
"Grid",
|
||||
"CP",
|
||||
"CP-NO",
|
||||
"CP-EL"
|
||||
],
|
||||
"drop_block_word": "도각",
|
||||
"viewBox": [
|
||||
0,
|
||||
0,
|
||||
443,
|
||||
443
|
||||
],
|
||||
"fit": [
|
||||
118,
|
||||
160,
|
||||
245,
|
||||
250
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user