feat(B07): 클립보드 DXF 글을 웹 CAD 도형으로 바꾸는 서버 길
오토캐드 WEBCLIP 이 클립보드에 넣은 DXF 글을 받는 POST /api/projects/dxf-clip 추가 (프로젝트 무관 · M02 양식도 씀 · 저장 안 함). 변환은 기존 도각 불러오기 엔진을 그대로 쓰고 dxf_text_to_entities 만 더함 — 엔티티 layerId 는 DXF 도면층 이름 그대로 · 쓰인 층 이름과 층 색을 함께 돌려줌. 시험 = 견본 DXF(선 · 폴리선 · 원 · 글 · 도면층 둘) 3건. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K7JS47SacjPxZ718QsZKyr
This commit is contained in:
@@ -16,6 +16,7 @@ DXF 로 저장해 달라」는 안내로 떨어진다.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
@@ -194,7 +195,31 @@ def _expand(entity: Any) -> list[Any]:
|
||||
|
||||
def dxf_to_entities(path: Path) -> list[dict[str, Any]]:
|
||||
"""DXF 파일을 도각 엔티티 목록으로. 지원 밖 도형(해치·솔리드 등)은 버린다."""
|
||||
document = ezdxf.readfile(str(path))
|
||||
return _document_entities(ezdxf.readfile(str(path)))
|
||||
|
||||
|
||||
def _layer_color(document: Any, name: str) -> str | None:
|
||||
"""도면층 색(ACI) → 화면 색. 꺼진 층은 음수로 적히므로 절댓값."""
|
||||
try:
|
||||
aci = abs(int(document.layers.get(name).dxf.color))
|
||||
red, green, blue = ezdxf_colors.aci2rgb(aci)
|
||||
return f"#{red:02x}{green:02x}{blue:02x}"
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def dxf_text_to_entities(text: str) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
"""오토캐드 WEBCLIP 이 클립보드에 넣은 DXF 글 → (엔티티 목록, 쓰인 도면층 [{name, color}]).
|
||||
|
||||
엔티티 `layerId` 는 DXF 도면층 이름 그대로 — 웹 CAD 가 이름으로 자기 도면층에 잇는다.
|
||||
"""
|
||||
document = ezdxf.read(io.StringIO(text))
|
||||
entities = _document_entities(document)
|
||||
names = sorted({str(entity["layerId"]) for entity in entities})
|
||||
return entities, [{"name": name, "color": _layer_color(document, name)} for name in names]
|
||||
|
||||
|
||||
def _document_entities(document: Any) -> list[dict[str, Any]]:
|
||||
entities: list[dict[str, Any]] = []
|
||||
index = 0
|
||||
for source in document.modelspace():
|
||||
|
||||
@@ -11,12 +11,17 @@ from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
from uuid import UUID
|
||||
|
||||
import ezdxf
|
||||
from fastapi import APIRouter, File, Response, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Export import export_drawing
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Import import import_frame_file
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Import import (
|
||||
dxf_text_to_entities,
|
||||
import_frame_file,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
frame_document,
|
||||
frame_template_document,
|
||||
@@ -145,6 +150,45 @@ async def import_frame_template(
|
||||
)
|
||||
|
||||
|
||||
class DxfClipRequest(BaseModel):
|
||||
"""오토캐드 WEBCLIP 이 클립보드에 넣은 DXF 글."""
|
||||
|
||||
text: str
|
||||
|
||||
|
||||
@router.post("/dxf-clip")
|
||||
async def paste_dxf_clip(request: DxfClipRequest) -> JSONResponse:
|
||||
"""클립보드 DXF 글 → 붙일 엔티티 · 도면층. 프로젝트 무관(M02 양식도 씀) · 저장 안 함."""
|
||||
try:
|
||||
if len(request.text) > _FRAME_IMPORT_MAX_BYTES:
|
||||
raise ValueError("붙여넣을 도형이 너무 많습니다(20MB 넘음).")
|
||||
entities, layers = await asyncio.to_thread(dxf_text_to_entities, request.text)
|
||||
validate_template_entities(entities)
|
||||
return JSONResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"entities": entities,
|
||||
"layers": layers,
|
||||
"entity_count": len(entities),
|
||||
}
|
||||
)
|
||||
except ValueError as exc:
|
||||
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
|
||||
except (UnicodeError, ezdxf.DXFError):
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"status": "error",
|
||||
"message": "클립보드 DXF 를 읽지 못했습니다 — 깨진 글입니다.",
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B07 DXF 붙여넣기 실패")
|
||||
return JSONResponse(
|
||||
status_code=500, content={"status": "error", "message": "DXF 를 붙여넣지 못했습니다."}
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{project_id}/frame-template", response_model=FrameTemplateSaveResponse)
|
||||
async def put_frame_template(
|
||||
project_id: UUID, request: FrameTemplateSaveRequest
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
"""B07 DXF 붙여넣기 — 오토캐드 WEBCLIP 이 클립보드에 넣은 DXF 글 → 웹 CAD 엔티티 · 도면층."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Import import dxf_text_to_entities
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Frame import router
|
||||
|
||||
SAMPLE = Path("resources/tester/fixtures/b07_webclip_sample.dxf").read_text(encoding="utf-8")
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_engine_keeps_layer_names_and_shapes():
|
||||
entities, layers = dxf_text_to_entities(SAMPLE)
|
||||
kinds = sorted(entity["type"] for entity in entities)
|
||||
# 선 1 · 닫힌 폴리선 1 · 원(선분 묶음) 1 · 글 1
|
||||
assert kinds == ["Line", "PolyLine", "PolyLine", "Text"]
|
||||
assert {entity["layerId"] for entity in entities} == {"구조물", "치수글"}
|
||||
assert [layer["name"] for layer in layers] == ["구조물", "치수글"]
|
||||
assert layers[0]["color"] == "#ff0000" # ACI 1
|
||||
text = next(entity for entity in entities if entity["type"] == "Text")
|
||||
assert text["shapeData"]["label"] == "배수관 D600"
|
||||
assert text["shapeData"]["basePoint"] == {"x": 5.0, "y": 50.0}
|
||||
line = next(entity for entity in entities if entity["type"] == "Line")
|
||||
assert line["shapeData"]["endPoint"] == {"x": 100.0, "y": 0.0} # 원좌표 그대로
|
||||
|
||||
|
||||
def test_route_returns_entities():
|
||||
response = client.post("/api/projects/dxf-clip", json={"text": SAMPLE})
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["entity_count"] == 4
|
||||
assert [layer["name"] for layer in body["layers"]] == ["구조물", "치수글"]
|
||||
|
||||
|
||||
def test_route_rejects_broken_text():
|
||||
response = client.post("/api/projects/dxf-clip", json={"text": " 0\nSECTION\n 2\nENTITIES\n"})
|
||||
assert response.status_code == 400
|
||||
assert response.json()["status"] == "error"
|
||||
Reference in New Issue
Block a user