feat(B07): LibreDWG 변환기 동봉 및 자동 탐색

- 저장소에 LibreDWG 0.14.8594 Windows 실행 파일 동봉 (dwg2dxf/dxf2dwg + 필요한 DLL 4개, 약 33MB)
  실제 파일은 메인 워크트리 B07_DesignDetail/openwebcad/tools/libredwg 에 두고 보조 워크트리는 junction 으로 연결
- 파이썬 바인딩 미포함 (import 시 GPL 이 본 프로그램까지 확산) — 별도 프로세스 호출 방식 유지
- GPL v3 전문(COPYING)과 원본 출처·구성·교체 방법 README 동봉
- 변환기 탐색 순서: .env 경로 → 동봉본 → PATH
- 옛 DXF 의 유니코드 escape 를 복원해 DWG 왕복 시 한글 깨짐 해결, DWG 출력용 DXF 는 R2004 로 생성
- DWG 왕복 검증 테스트 추가 (tmp/tests, git 추적 제외)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 14:56:16 +09:00
co-authored by Claude Opus 5
parent 2b6e76cd1c
commit 05a6b7e506
11 changed files with 747 additions and 10 deletions
@@ -28,6 +28,7 @@ from uuid import NAMESPACE_URL, uuid5
import ezdxf
from ezdxf import colors as ezdxf_colors
from ezdxf.lldxf.encoding import decode_dxf_unicode, has_dxf_unicode
logger = logging.getLogger(__name__)
@@ -38,6 +39,17 @@ _FLATTEN_MM = 0.2
_MAX_ENTITIES = 20000
# 프로그램에 함께 담은 LibreDWG 자리 — `B07_DesignDetail/openwebcad/tools/libredwg/`.
# 별도 실행 파일로만 부른다(라이브러리로 품지 않는다) — 자세한 것은 그 폴더의 README.
_BUNDLED_DIR = Path(__file__).resolve().parent / "openwebcad" / "tools" / "libredwg"
def bundled_tool(name: str) -> str | None:
"""동봉한 변환기 경로. 없으면 None."""
path = _BUNDLED_DIR / name
return str(path) if path.is_file() else None
def _entity_id(index: int) -> str:
return str(uuid5(_IMPORT_NS, str(index)))
@@ -99,12 +111,17 @@ def _polyline(index: int, entity: Any, points: list[Any], closed: bool) -> dict[
return {**_base(index, entity, "PolyLine"), "shapeData": None, "children": children}
def _plain(label: str) -> str:
"""옛 DXF 는 한글을 유니코드 escape 로 적는다 — 글자로 되돌린다."""
return decode_dxf_unicode(label) if has_dxf_unicode(label) else label
def _text(index: int, entity: Any, label: str, insert: Any, height: float) -> dict[str, Any]:
rotation = float(getattr(entity.dxf, "rotation", 0.0) or 0.0)
return {
**_base(index, entity, "Text"),
"shapeData": {
"label": label,
"label": _plain(label),
"basePoint": _point(insert[0], insert[1]),
"options": {
"textDirection": _point(cos(radians(rotation)), sin(radians(rotation))),
@@ -215,11 +232,11 @@ def dwg_version(data: bytes) -> str | None:
def _dwg2dxf_path() -> str | None:
"""LibreDWG 변환기(dwg2dxf) 자리. `.env` 값이 먼저고, 없으면 PATH 에서 찾는다."""
"""LibreDWG 변환기(dwg2dxf) 자리 — `.env` 값 → 동봉본 → PATH 순으로 찾는다."""
configured = os.getenv("LIBREDWG_DWG2DXF_PATH", "").strip()
if configured:
return configured if Path(configured).is_file() else None
return shutil.which("dwg2dxf")
return bundled_tool("dwg2dxf.exe") or shutil.which("dwg2dxf")
def _dwg_to_dxf(source: Path, work_dir: Path) -> Path: