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