feat(488-3 DWG 불러오기): 삽입 탭 [DWG 불러오기](IMPORT) — 끌어다 놓기와 같은 서버 변환 · 원좌표 · 골라진 채 · 화면 맞춤 · 되돌리기 한 번 · 파일 입력 문서에 숨겨 붙임 · ByLayer 도형 = 레이어 색(B07 붙이기 · M02 불러오기 같은 _color) · 시험 catalog · test_b07_dxf_clip 한 건씩

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 22:17:13 +09:00
co-authored by Claude Opus 5.5
parent 99df7c2512
commit 6695dfaee9
8 changed files with 84 additions and 7 deletions
@@ -62,10 +62,12 @@ def _entity_id(index: int) -> str:
def _color(entity: Any) -> str:
"""DXF 색 번호를 화면 색으로. 도면층 색(BYLAYER)이면 기본색을 쓴다."""
"""DXF 색 번호를 화면 색으로. BYLAYER 면 레이어 색(488 · B07 붙이기 · M02 불러오기 같이) · BYBLOCK 은 기본색."""
try:
aci = int(entity.dxf.color)
if aci in (0, 256): # BYBLOCK · BYLAYER
if aci == 256: # BYLAYER
return _layer_color(entity.doc, entity.dxf.layer) or _DEFAULT_COLOR
if aci == 0: # BYBLOCK
return _DEFAULT_COLOR
red, green, blue = ezdxf_colors.aci2rgb(aci)
return f"#{red:02x}{green:02x}{blue:02x}"
@@ -136,6 +136,7 @@
{"id": "IMAGEATTACH", "label": "이미지 부착", "aliases": ["IAT"]},
{"id": "BLOCK", "label": "블록 작성", "aliases": ["B"]},
{"id": "INSERT", "label": "블록 삽입", "aliases": ["I"]},
{"id": "IMPORT", "label": "DWG 불러오기", "aliases": []},
{"id": "SELECT", "label": "선택", "aliases": []},
{"id": "ZOOM", "label": "범위 줌", "aliases": ["Z"]},
{"id": "ZOOMIN", "label": "확대", "aliases": []},
@@ -4,6 +4,7 @@ import { saveBlockFromEntities } from '../blocks/block-library';
import { setBlockLibraryVisible } from '../components/ui-state';
import { importImageFromFile } from '../helpers/import-export-handlers/import-image-from-file';
import { pickFile } from '../helpers/pick-file';
import { importDrawingFile } from '../integration/dxf-clip-paste';
import { getActiveToolActor, getSelectedEntities } from '../state';
import { Tool } from '../tools';
import { imageImportToolStateMachine } from '../tools/image-import-tool';
@@ -58,4 +59,14 @@ export const INSERT_COMMANDS: CadCommand[] = [
return '블록 라이브러리';
},
},
{
id: 'IMPORT',
label: 'DWG 불러오기',
glyph: '📂',
hint: 'DWG · DXF 파일을 지금 도면에 원좌표 그대로 더한다(골라진 채 · 되돌리기 한 번에 빠짐)',
run: () => {
pickFile('.dwg,.dxf', importDrawingFile);
return 'DWG · DXF 파일 선택';
},
},
];
@@ -1,11 +1,20 @@
/** 파일 선택창을 띄우고 고른 파일 하나를 돌려준다. 취소하면 아무 일도 없다. */
/**
* 파일 선택창을 띄우고 고른 파일 하나를 돌려준다. 취소하면 아무 일도 없다.
* 입력은 고르는 동안 문서에 숨겨 붙임(`.cad-pick-file` · 자동 시험 업로드가 닿게) · 고르거나 취소하면 뗌.
*/
export function pickFile(accept: string, onPicked: (file: File) => void): void {
const input = document.createElement('input');
input.type = 'file';
input.accept = accept;
input.hidden = true;
input.className = 'cad-pick-file';
document.querySelectorAll('.cad-pick-file').forEach((old) => old.remove());
input.addEventListener('change', () => {
const file = input.files?.[0];
input.remove();
if (file) onPicked(file);
});
input.addEventListener('cancel', () => input.remove());
document.body.append(input);
input.click();
}
@@ -1,8 +1,8 @@
/**
* 오토캐드 도형 붙이기 — 두 길 모두 서버가 DXF 를 도형으로 바꿔 현재 도면에 원좌표 그대로 붙이고
* 고른 상태로 둔다(되돌리기 한 번에 빠짐).
* - 끌어다 놓기: 오토캐드 WBLOCK(DWG) · DXFOUT(DXF) 로 저장한 파일, 또는 도면 파일 통째.
* 끄는 동안 `body.cad-file-dragging` 으로 흐린 테두리.
* - 끌어다 놓기 · [삽입 → DWG 불러오기](IMPORT · 488): 오토캐드 WBLOCK(DWG) · DXFOUT(DXF) 로 저장한 파일,
* 또는 도면 파일 통째 · 붙인 범위로 화면 맞춤. 끄는 동안 `body.cad-file-dragging` 으로 흐린 테두리.
* - Ctrl+V: 시스템 클립보드 글이 DXF 면 같은 길 · 아니면 앱 안 붙여넣기(PASTECLIP).
* 키 처리(`input-controller.ts`)는 Ctrl+V 의 기본 동작을 막지 않아 브라우저가 `paste` 를 쏜다 —
* `paste` 이벤트의 글은 권한 없이 읽힘(iframe 안에서도).
@@ -10,10 +10,14 @@
import { toast } from 'react-toastify';
import { runCommandInput } from '../commands/run-command';
import { hasClipboardContent } from '../helpers/cad-clipboard';
import { fitView } from '../drawControllers/fit-view';
import { getEntitiesAndLayersFromJsonObject } from '../helpers/import-export-handlers/import-entities-from-json';
import { changeView } from '../inputController/zoom-prompt';
import {
addEntities,
getLayers,
getScreenCanvasDrawController,
getSelectedEntities,
isDrawingReadOnly,
setLayers,
setSelectedEntityIds,
@@ -73,6 +77,24 @@ function refuseReadOnly(): boolean {
return true;
}
/** 붙인 것(골라진 채)이 화면에 다 보이게 — ZOOM 이전(P)으로 돌아감 */
function fitToSelection(count: number): number {
const picked = getSelectedEntities();
const controller = getScreenCanvasDrawController();
if (picked.length)
changeView(() => {
const { zoom, offset } = fitView(picked, controller.getCanvasSize());
controller.setScreenScale(zoom);
controller.setScreenOffset(offset);
});
return count;
}
/** DWG · DXF 파일 → 지금 도면에 더함(원좌표 · 골라진 채 · 화면 맞춤 · 되돌리기 한 번) */
export function importDrawingFile(file: File): void {
if (!refuseReadOnly()) reportPaste(pasteDrawingFile(file).then(fitToSelection));
}
export function registerDxfClipPaste(): void {
document.addEventListener('paste', (event: ClipboardEvent) => {
if ((event.target as HTMLElement | null)?.closest?.('input, textarea, select')) return;
@@ -101,6 +123,6 @@ export function registerDxfClipPaste(): void {
event.preventDefault();
setDragging(false);
const file = event.dataTransfer?.files[0];
if (file && !refuseReadOnly()) reportPaste(pasteDrawingFile(file));
if (file) importDrawingFile(file);
});
}
@@ -144,6 +144,7 @@ export const RIBBON_TABS: RibbonTab[] = [
id: 'insert',
label: '삽입',
panels: [
{ label: '가져오기', big: ['IMPORT'] },
{ label: '블록', big: ['INSERT'], commands: ['BLOCK'] },
{ label: '참조', commands: ['IMAGEATTACH'] },
],
@@ -7,6 +7,7 @@ import { describe, expect, it } from "vitest";
import catalog from "../../../B07_DesignDetail/openwebcad/src/commands/command-catalog.json";
import { getAllCommands } from "../../../B07_DesignDetail/openwebcad/src/commands/registry";
import { RIBBON_TABS } from "../../../B07_DesignDetail/openwebcad/src/ribbon/ribbon.config";
describe("command-catalog.json", () => {
it("registry 와 같은 명령 · 한글 이름 · 기본 별칭 · 차례", () => {
@@ -37,4 +38,10 @@ describe("command-catalog.json", () => {
expect(words.filter((w) => /도면층|선가중치/.test(w))).toEqual([]);
expect(catalog.find((c) => c.id === "LAYER")?.label).toContain("레이어");
});
it("488 — 삽입 탭에 [DWG 불러오기](IMPORT)", () => {
const insert = RIBBON_TABS.find((tab) => tab.id === "insert");
expect(insert?.panels.flatMap((panel) => panel.big ?? [])).toContain("IMPORT");
expect(catalog.find((c) => c.id === "IMPORT")?.label).toBe("DWG 불러오기");
});
});
+25 -1
View File
@@ -5,7 +5,14 @@ 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
import io
import ezdxf
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Import import (
dxf_text_to_entities,
import_frame_file,
)
from B07_DesignDetail.B07_DesignDetail_Router_Frame import router
SAMPLE = Path("resources/tester/fixtures/b07_webclip_sample.dxf").read_text(encoding="utf-8")
@@ -30,6 +37,23 @@ def test_engine_keeps_layer_names_and_shapes():
assert line["shapeData"]["endPoint"] == {"x": 100.0, "y": 0.0} # 원좌표 그대로
def test_bylayer_color_is_layer_color():
"""488 — BYLAYER 도형 = 레이어 색(B07 붙이기 · M02 불러오기 같은 길) · 제 색은 그대로."""
document = ezdxf.new()
document.layers.add("빨강", color=1)
space = document.modelspace()
space.add_line((0, 0), (10, 0), dxfattribs={"layer": "빨강"}) # 색 = BYLAYER(256)
space.add_line((0, 5), (10, 5), dxfattribs={"layer": "빨강", "color": 3})
buffer = io.StringIO()
document.write(buffer)
text = buffer.getvalue()
for entities in (
dxf_text_to_entities(text)[0],
import_frame_file("a.dxf", text.encode("utf-8")),
):
assert [entity["lineColor"] for entity in entities] == ["#ff0000", "#00ff00"]
def test_route_returns_entities():
response = client.post("/api/projects/dxf-clip", json={"text": SAMPLE})
assert response.status_code == 200