- 정리는 코치 세션이 저장소 밖 도구로 수행, 하이쿠 세션이 커밋. - 옛 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>
179 lines
6.1 KiB
Python
179 lines
6.1 KiB
Python
#!/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))
|