From 6695dfaee90725007da463927768305b3842e388 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 19 Oct 2026 22:17:13 +0900 Subject: [PATCH] =?UTF-8?q?feat(488-3=20DWG=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0):=20=EC=82=BD=EC=9E=85=20=ED=83=AD=20[DWG=20=EB=B6=88?= =?UTF-8?q?=EB=9F=AC=EC=98=A4=EA=B8=B0](IMPORT)=20=E2=80=94=20=EB=81=8C?= =?UTF-8?q?=EC=96=B4=EB=8B=A4=20=EB=86=93=EA=B8=B0=EC=99=80=20=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20=EC=84=9C=EB=B2=84=20=EB=B3=80=ED=99=98=20=C2=B7=20?= =?UTF-8?q?=EC=9B=90=EC=A2=8C=ED=91=9C=20=C2=B7=20=EA=B3=A8=EB=9D=BC?= =?UTF-8?q?=EC=A7=84=20=EC=B1=84=20=C2=B7=20=ED=99=94=EB=A9=B4=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4=20=C2=B7=20=EB=90=98=EB=8F=8C=EB=A6=AC=EA=B8=B0=20?= =?UTF-8?q?=ED=95=9C=20=EB=B2=88=20=C2=B7=20=ED=8C=8C=EC=9D=BC=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EB=AC=B8=EC=84=9C=EC=97=90=20=EC=88=A8=EA=B2=A8=20?= =?UTF-8?q?=EB=B6=99=EC=9E=84=20=C2=B7=20ByLayer=20=EB=8F=84=ED=98=95=20?= =?UTF-8?q?=3D=20=EB=A0=88=EC=9D=B4=EC=96=B4=20=EC=83=89(B07=20=EB=B6=99?= =?UTF-8?q?=EC=9D=B4=EA=B8=B0=20=C2=B7=20M02=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=EA=B0=99=EC=9D=80=20=5Fcolor)=20=C2=B7=20=EC=8B=9C?= =?UTF-8?q?=ED=97=98=20catalog=20=C2=B7=20test=5Fb07=5Fdxf=5Fclip=20?= =?UTF-8?q?=ED=95=9C=20=EA=B1=B4=EC=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../B07_DesignDetail_Engine_Frame_Import.py | 6 ++-- .../src/commands/command-catalog.json | 1 + .../src/commands/commands.insert.ts | 11 ++++++++ .../openwebcad/src/helpers/pick-file.ts | 11 +++++++- .../src/integration/dxf-clip-paste.ts | 28 +++++++++++++++++-- .../openwebcad/src/ribbon/ribbon.config.ts | 1 + resources/tester/cad/command-catalog.test.ts | 7 +++++ resources/tester/test_b07_dxf_clip.py | 26 ++++++++++++++++- 8 files changed, 84 insertions(+), 7 deletions(-) diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Frame_Import.py b/B07_DesignDetail/B07_DesignDetail_Engine_Frame_Import.py index ae6ce9982..9df19f705 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Frame_Import.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Frame_Import.py @@ -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}" diff --git a/B07_DesignDetail/openwebcad/src/commands/command-catalog.json b/B07_DesignDetail/openwebcad/src/commands/command-catalog.json index 26bdc14f3..fc10f1907 100644 --- a/B07_DesignDetail/openwebcad/src/commands/command-catalog.json +++ b/B07_DesignDetail/openwebcad/src/commands/command-catalog.json @@ -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": []}, diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.insert.ts b/B07_DesignDetail/openwebcad/src/commands/commands.insert.ts index 6bdeb8ebc..6cb2b8d73 100644 --- a/B07_DesignDetail/openwebcad/src/commands/commands.insert.ts +++ b/B07_DesignDetail/openwebcad/src/commands/commands.insert.ts @@ -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 파일 선택'; + }, + }, ]; diff --git a/B07_DesignDetail/openwebcad/src/helpers/pick-file.ts b/B07_DesignDetail/openwebcad/src/helpers/pick-file.ts index efbb0e236..e701d481c 100644 --- a/B07_DesignDetail/openwebcad/src/helpers/pick-file.ts +++ b/B07_DesignDetail/openwebcad/src/helpers/pick-file.ts @@ -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(); } diff --git a/B07_DesignDetail/openwebcad/src/integration/dxf-clip-paste.ts b/B07_DesignDetail/openwebcad/src/integration/dxf-clip-paste.ts index 2be08efa5..bc9855779 100644 --- a/B07_DesignDetail/openwebcad/src/integration/dxf-clip-paste.ts +++ b/B07_DesignDetail/openwebcad/src/integration/dxf-clip-paste.ts @@ -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); }); } diff --git a/B07_DesignDetail/openwebcad/src/ribbon/ribbon.config.ts b/B07_DesignDetail/openwebcad/src/ribbon/ribbon.config.ts index 717341998..c512ed824 100644 --- a/B07_DesignDetail/openwebcad/src/ribbon/ribbon.config.ts +++ b/B07_DesignDetail/openwebcad/src/ribbon/ribbon.config.ts @@ -144,6 +144,7 @@ export const RIBBON_TABS: RibbonTab[] = [ id: 'insert', label: '삽입', panels: [ + { label: '가져오기', big: ['IMPORT'] }, { label: '블록', big: ['INSERT'], commands: ['BLOCK'] }, { label: '참조', commands: ['IMAGEATTACH'] }, ], diff --git a/resources/tester/cad/command-catalog.test.ts b/resources/tester/cad/command-catalog.test.ts index 67dbca5c1..951b6283d 100644 --- a/resources/tester/cad/command-catalog.test.ts +++ b/resources/tester/cad/command-catalog.test.ts @@ -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 불러오기"); + }); }); diff --git a/resources/tester/test_b07_dxf_clip.py b/resources/tester/test_b07_dxf_clip.py index 69f419dc4..4cbcfb83a 100644 --- a/resources/tester/test_b07_dxf_clip.py +++ b/resources/tester/test_b07_dxf_clip.py @@ -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