From cfcba2748195376563168f57cc3d579551feee66 Mon Sep 17 00:00:00 2001 From: junghoonkim Date: Tue, 28 Jul 2026 06:10:38 +0900 Subject: [PATCH 01/66] PushTest --- pushtest | 1 + 1 file changed, 1 insertion(+) create mode 100644 pushtest diff --git a/pushtest b/pushtest new file mode 100644 index 00000000..d60bc529 --- /dev/null +++ b/pushtest @@ -0,0 +1 @@ +evidence to proof the push -- 2.54.0 From 03840407f612b4baec7d0d5007cd842ce21867ea Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 29 Aug 2026 23:20:07 +0900 Subject: [PATCH 02/66] =?UTF-8?q?chore(resources):=20AutoCAD=20=EB=A6=AC?= =?UTF-8?q?=EB=B3=B8=C2=B7=EC=A3=BC=EC=9A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=A1=B0=EC=82=AC=EC=9E=90=EB=A3=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B07 CAD 화면 개선 참조용. AutoCAD 2024 Windows 기본 2D 제도 기능 기준, Autodesk 공식 명령 참조·2024 신규/변경 명령·상태막대와 재대조한 조사 결과. Co-Authored-By: Claude Opus 5 (1M context) --- resources/임시_AutoCAD_리본_기능_조사.md | 366 +++++++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 resources/임시_AutoCAD_리본_기능_조사.md diff --git a/resources/임시_AutoCAD_리본_기능_조사.md b/resources/임시_AutoCAD_리본_기능_조사.md new file mode 100644 index 00000000..ed079bf3 --- /dev/null +++ b/resources/임시_AutoCAD_리본_기능_조사.md @@ -0,0 +1,366 @@ +# AutoCAD 리본 및 주요 기능 조사 (임시) + +- 조사 기준: AutoCAD 2024 Windows의 기본 2D 제도 기능 +- 조사일: 2026-08-29 +- 추가검증: 2026-08-29, Autodesk 공식 명령 참조·2024 신규/변경 명령·상태막대 참조와 재대조 +- 용도: B07 상세설계 웹 CAD 기능 검토용 임시 참고자료 +- 주의: AutoCAD 리본은 제품, 작업공간, 화면 폭, 설치 확장 및 CUI 사용자화에 따라 탭·패널·아이콘의 표시 여부와 위치가 달라진다. 아래 표는 기본 2D 제도 리본의 대표 기능을 작업 기준으로 합친 목록이며, 모든 AutoCAD 명령의 전수 목록은 아니다. 같은 기능이 여러 탭·패널에 나타나도 최초 한 번만 기재한다. 3D 모델링, 전문화 도구 세트, Express Tools 및 타사 애드인은 제외했다. + +## 1. 홈 탭 — 그리기 + +| 그룹(패널) | 기능/명령 | 짧은 설명 | +|---|---|---| +| 그리기 | 선 `LINE` | 두 점 사이에 직선 세그먼트를 작성한다. | +| 그리기 | 폴리선 `PLINE` | 연결된 선·호를 하나의 객체로 작성한다. | +| 그리기 | 원 `CIRCLE` | 중심·반지름 등 여러 조건으로 원을 작성한다. | +| 그리기 | 호 `ARC` | 3점, 중심, 시작·끝점 등의 조건으로 호를 작성한다. | +| 그리기 | 직사각형 `RECTANG` | 직사각형 형태의 닫힌 폴리선을 작성한다. | +| 그리기 | 다각형 `POLYGON` | 지정한 변 수의 정다각형을 작성한다. | +| 그리기 | 타원 `ELLIPSE` | 중심 또는 축 길이로 타원·타원호를 작성한다. | +| 그리기 | 스플라인 `SPLINE` | 맞춤점 또는 조정 정점으로 부드러운 곡선을 작성한다. | +| 그리기 | 다중선 `MLINE` | 여러 평행선으로 구성된 다중선 객체를 작성한다. | +| 그리기 | 다중선 스타일 `MLSTYLE` | 다중선의 요소 수·간격·선종류와 끝막음을 관리한다. | +| 그리기 | 구성선 `XLINE` | 양방향으로 무한한 기준선을 작성한다. | +| 그리기 | 광선 `RAY` | 한 방향으로 무한한 기준선을 작성한다. | +| 그리기 | 점 `POINT` | 점 객체를 작성한다. | +| 그리기 | 도넛 `DONUT` | 채워진 원 또는 링 모양 폴리선을 작성한다. | +| 그리기 | 등분 `DIVIDE` | 객체를 자르지 않고 동일 간격의 점 또는 블록을 지정 개수만큼 배치한다. | +| 그리기 | 길이분할 `MEASURE` | 객체를 자르지 않고 지정 거리마다 점 또는 블록을 배치한다. | +| 그리기 | 해치 `HATCH` | 닫힌 경계에 패턴·솔리드·그라데이션 채움을 작성한다. | +| 그리기 | 그라데이션 `GRADIENT` | 닫힌 영역에 색상 그라데이션을 적용한다. | +| 그리기 | 경계 `BOUNDARY` | 닫힌 영역에서 폴리선 또는 영역 객체를 만든다. | +| 그리기 | 영역 `REGION` | 닫힌 평면 객체를 영역 객체로 변환한다. | +| 그리기 | 와이프아웃 `WIPEOUT` | 뒤쪽 객체를 가리는 마스크 영역을 작성한다. | + +## 2. 홈 탭 — 수정 + +| 그룹(패널) | 기능/명령 | 짧은 설명 | +|---|---|---| +| 수정 | 이동 `MOVE` | 선택 객체를 기준점에서 새 위치로 이동한다. | +| 수정 | 복사 `COPY` | 선택 객체를 하나 이상의 위치에 복제한다. | +| 수정 | 회전 `ROTATE` | 기준점을 중심으로 객체를 회전한다. | +| 수정 | 축척 `SCALE` | 기준점과 비율 또는 참조 길이로 크기를 변경한다. | +| 수정 | 정렬 `ALIGN` | 원본점과 대상점 쌍으로 객체를 이동·회전하고 선택적으로 축척한다. | +| 수정 | 대칭 `MIRROR` | 대칭축을 기준으로 객체를 반사 복사한다. | +| 수정 | 간격띄우기 `OFFSET` | 평행선·동심원·등거리 곡선을 작성한다. | +| 수정 | 배열 `ARRAY` | 직사각형·경로·원형 패턴으로 객체를 반복 배치한다. | +| 수정 | 자르기 `TRIM` | 경계를 기준으로 불필요한 객체 부분을 자른다. | +| 수정 | 연장 `EXTEND` | 객체 끝을 지정 경계까지 연장한다. | +| 수정 | 신축 `STRETCH` | 교차 선택한 정점과 객체 일부를 늘이거나 이동한다. | +| 수정 | 모깎기 `FILLET` | 두 객체를 지정 반지름의 호로 연결한다. | +| 수정 | 모따기 `CHAMFER` | 두 객체를 직선 모따기로 연결한다. | +| 수정 | 곡선 혼합 `BLEND` | 두 곡선을 부드러운 스플라인으로 연결한다. | +| 수정 | 끊기 `BREAK` | 객체의 두 점 사이를 제거하거나 한 점에서 나눈다. | +| 수정 | 점에서 끊기 `BREAKATPOINT` | 객체를 지정점에서 두 객체로 나눈다. | +| 수정 | 결합 `JOIN` | 끝이 맞는 선·호·폴리선 등을 하나로 결합한다. | +| 수정 | 분해 `EXPLODE` | 블록·폴리선 등 복합 객체를 구성요소로 분해한다. | +| 수정 | 지우기 `ERASE` | 선택 객체를 삭제한다. | +| 수정 | 길이조정 `LENGTHEN` | 선과 호의 길이 또는 끼인각을 변경한다. | +| 수정 | 폴리선 편집 `PEDIT` | 폴리선 결합, 폭, 정점, 곡선 맞춤 등을 편집한다. | +| 수정 | 해치 편집 `HATCHEDIT` | 기존 해치의 패턴, 축척, 각도, 경계를 수정한다. | +| 수정 | 그리기 순서 `DRAWORDER` | 객체의 앞뒤 표시 순서를 변경한다. | +| 수정 | 특성 일치 `MATCHPROP` | 원본 객체의 표시 특성을 다른 객체에 복사한다. | +| 수정 | 중복 객체 삭제 `OVERKILL` | 중복·겹침 형상을 제거하고 이어진 선·호를 결합한다. | +| 수정 | 방향 반전 `REVERSE` | 선·폴리선·스플라인 등의 시작점과 끝점 방향을 뒤집는다. | +| 수정 | 특성을 ByLayer로 `SETBYLAYER` | 선택 객체의 특성 재지정을 ByLayer 값으로 변경한다. | + +## 3. 홈 탭 — 도면층·특성·그룹·유틸리티 + +| 그룹(패널) | 기능/명령 | 짧은 설명 | +|---|---|---| +| 도면층 | 도면층 특성 `LAYER` | 도면층을 만들고 이름·색상·선종류·출력 여부를 관리한다. | +| 도면층 | 현재 도면층 설정 | 선택한 도면층을 새 객체의 기본 도면층으로 지정한다. | +| 도면층 | 켜기/끄기 | 도면층 객체의 표시를 켜거나 끈다. | +| 도면층 | 동결/동결해제 | 도면층을 재생성 및 표시 대상에서 제외하거나 복원한다. | +| 도면층 | 잠금/잠금해제 | 도면층 객체의 편집 가능 여부를 전환한다. | +| 도면층 | 분리 `LAYISO` | 선택 객체의 도면층만 남기고 나머지를 숨기거나 잠근다. | +| 도면층 | 분리 해제 `LAYUNISO` | 도면층 분리 이전 상태를 복원한다. | +| 도면층 | 이전 상태 `LAYERP` | 직전 도면층 설정 변경을 복원한다. | +| 도면층 | 도면층 상태 | 도면층 설정 조합을 이름으로 저장하고 복원한다. | +| 도면층 | 도면층 일치 `LAYMCH` | 선택 객체를 대상 객체의 도면층으로 이동한다. | +| 도면층 | 현재 도면층으로 `LAYCUR` | 선택 객체를 현재 도면층으로 이동한다. | +| 도면층 | 도면층 병합 `LAYMRG` | 한 도면층의 객체를 다른 도면층으로 옮기고 원래 도면층을 제거한다. | +| 도면층 | 도면층 삭제 `LAYDEL` | 선택한 도면층과 그 객체를 삭제한다. | +| 도면층 | 도면층 탐색 `LAYWALK` | 도면층을 선택적으로 표시하며 포함 객체를 확인한다. | +| 특성 | 색상 | 객체 색상을 직접 또는 `ByLayer`·`ByBlock`으로 지정한다. | +| 특성 | 선종류 | 실선·점선·중심선 등 객체 선종류를 지정한다. | +| 특성 | 선가중치 | 화면 및 출력에 사용할 선 굵기를 지정한다. | +| 특성 | 투명도 | 객체 또는 도면층의 투명도를 지정한다. | +| 특성 | 특성 팔레트 `PROPERTIES` | 선택 객체의 형상·표시·데이터 특성을 조회하고 수정한다. | +| 특성 | 빠른 특성 `QUICKPROPERTIES` | 선택 객체 주변에 주요 특성만 간단히 표시한다. | +| 그룹 | 그룹 `GROUP` | 여러 객체를 함께 선택할 수 있는 명명 그룹으로 묶는다. | +| 그룹 | 그룹 해제 `UNGROUP` | 객체 그룹을 해제한다. | +| 유틸리티 | 거리 `DIST` | 두 점 사이의 거리와 각도 차이를 측정한다. | +| 유틸리티 | 반지름 | 원 또는 호의 반지름을 측정한다. | +| 유틸리티 | 각도 | 선·호 또는 지정점 사이의 각도를 측정한다. | +| 유틸리티 | 면적 `AREA` | 객체 또는 지정 경계의 면적과 둘레를 계산한다. | +| 유틸리티 | 빠른 측정 `MEASUREGEOM` | 커서 주변의 치수·거리·각도와 닫힌 영역의 면적을 동적으로 표시한다. | +| 유틸리티 | 점 좌표 `ID` | 지정점의 X·Y·Z 좌표를 표시한다. | +| 유틸리티 | 리스트 `LIST` | 선택 객체의 상세 데이터와 기하 정보를 표시한다. | +| 유틸리티 | 빠른 선택 `QSELECT` | 객체 유형과 특성 조건으로 선택 집합을 만든다. | +| 유틸리티 | 계산기 `QUICKCALC` | 수식, 단위 변환 및 도면 값을 계산한다. | +| 유틸리티 | 유사 선택 `SELECTSIMILAR` | 선택 객체와 지정 특성이 같은 객체를 모두 선택한다. | +| 유틸리티 | 객체 분리 `ISOLATEOBJECTS` | 선택 객체만 표시하고 나머지 객체를 숨긴다. | +| 유틸리티 | 객체 숨기기 `HIDEOBJECTS` | 선택 객체를 일시적으로 숨긴다. | +| 유틸리티 | 객체 분리 종료 `UNISOLATEOBJECTS` | 숨기거나 분리한 객체를 다시 표시한다. | +| 클립보드 | 잘라내기·복사·붙여넣기 | 객체를 시스템 클립보드로 이동·복제한다. | +| 클립보드 | 기준점과 함께 복사 | 지정 기준점을 포함해 객체를 클립보드에 복사한다. | +| 클립보드 | 원래 좌표로 붙여넣기 | 복사한 객체를 원본 도면 좌표에 배치한다. | +| 클립보드 | 블록으로 붙여넣기 | 클립보드 객체를 새 블록으로 삽입한다. | + +## 4. 삽입 탭 — 블록·참조·데이터 + +| 그룹(패널) | 기능/명령 | 짧은 설명 | +|---|---|---| +| 블록 | 블록 삽입 `INSERT` | 현재 도면·최근 항목·라이브러리의 블록을 배치한다. | +| 블록 | 스마트 블록 배치 | 블록 팔레트로 삽입할 때 기존 동일 블록을 바탕으로 위치·회전·축척을 제안한다. | +| 블록 | 블록 교체 `BREPLACE` | 선택 블록을 제안 목록 또는 지정한 다른 블록으로 교체한다. | +| 블록 정의 | 블록 작성 `BLOCK` | 선택 객체와 기준점으로 블록 정의를 만든다. | +| 블록 정의 | 블록 편집기 `BEDIT` | 블록 정의와 동적 블록 동작을 편집한다. | +| 블록 정의 | 블록 쓰기 `WBLOCK` | 선택 객체 또는 블록을 별도 DWG로 저장한다. | +| 블록 정의 | 속성 정의 `ATTDEF` | 블록에 포함할 문자 데이터 필드를 정의한다. | +| 블록 정의 | 속성 관리 `BATTMAN` | 블록 속성의 순서·표시·기본값을 관리한다. | +| 블록 정의 | 속성 동기화 `ATTSYNC` | 변경된 속성 정의를 기존 블록 참조에 반영한다. | +| 블록 정의 | 향상된 속성 편집 `EATTEDIT` | 삽입된 블록 참조의 속성 값과 문자·표시 특성을 편집한다. | +| 참조 | DWG 부착 `XATTACH` | 다른 DWG를 외부 참조로 연결한다. | +| 참조 | 이미지 부착 `IMAGEATTACH` | 래스터 이미지를 참조로 연결한다. | +| 참조 | PDF 부착 `PDFATTACH` | PDF 페이지를 언더레이로 연결한다. | +| 참조 | DWF/DGN 부착 | DWF 또는 DGN을 언더레이로 연결한다. | +| 참조 | 점 구름 부착 | 스캔된 점 구름 데이터를 도면에 연결한다. | +| 참조 | 외부 참조 팔레트 `XREF` | 참조의 로드·언로드·재로드·경로·결합을 관리한다. | +| 참조 | 참조 내부 편집 `REFEDIT` | 블록 또는 외부 참조를 현재 도면 안에서 직접 편집한다. | +| 참조 | 외부 참조 자르기 `XCLIP` | DWG 외부 참조 또는 블록의 표시 경계를 지정한다. | +| 참조 | 이미지 자르기 `IMAGECLIP` | 부착 이미지의 표시 경계를 지정한다. | +| 참조 | PDF 자르기 `PDFCLIP` | PDF 언더레이의 표시 경계를 지정한다. | +| 가져오기 | PDF 가져오기 `PDFIMPORT` | PDF의 벡터 형상·문자·채움을 도면 객체로 변환한다. | +| 가져오기 | 가져오기 `IMPORT` | 지원되는 다른 형식의 데이터를 현재 도면으로 가져온다. | +| 링크 및 추출 | 데이터 링크 `DATALINK` | Excel 등 외부 표 데이터와 도면 테이블을 연결한다. | +| 링크 및 추출 | 데이터 추출 `DATAEXTRACTION` | 객체·블록·속성 정보를 표 또는 외부 파일로 추출한다. | +| 링크 및 추출 | 하이퍼링크 `HYPERLINK` | 객체에 웹·파일·도면 뷰 링크를 연결한다. | +| 링크 및 추출 | 필드 `FIELD` | 날짜·도면·객체 특성 등 갱신 가능한 문자 값을 삽입한다. | +| 위치 | 지리적 위치 `GEOGRAPHICLOCATION` | 좌표계와 지도상의 도면 위치를 지정한다. | + +## 5. 주석 탭 — 문자·치수·지시선·표·표식 + +| 그룹(패널) | 기능/명령 | 짧은 설명 | +|---|---|---| +| 문자 | 여러 줄 문자 `MTEXT` | 서식과 줄바꿈을 지원하는 문단 문자를 작성한다. | +| 문자 | 단일 행 문자 `TEXT` | 각 행이 독립 객체인 간단한 문자를 작성한다. | +| 문자 | 문자 스타일 `STYLE` | 글꼴·높이·폭 비율·기울기 등의 스타일을 관리한다. | +| 문자 | 문자 편집 `TEXTEDIT` | 기존 문자·치수 문자·속성 문자를 수정한다. | +| 문자 | 찾기·대치 `FIND` | 도면의 문자 내용을 검색하고 필요한 문자열을 대치한다. | +| 문자 | 맞춤법 검사 `SPELL` | 도면 문자와 속성 값의 철자를 검사한다. | +| 치수 | 빠른 치수 `QDIM` | 선택 객체에 여러 치수를 한 번에 작성한다. | +| 치수 | 치수 `DIM` | 선택한 객체와 지정점에 적합한 치수 유형을 작성한다. | +| 치수 | 선형 치수 `DIMLINEAR` | 수평·수직·회전 선형 치수를 작성한다. | +| 치수 | 정렬 치수 `DIMALIGNED` | 두 점과 평행한 실제 길이 치수를 작성한다. | +| 치수 | 각도 치수 `DIMANGULAR` | 두 선 또는 호의 각도를 기입한다. | +| 치수 | 호 길이 치수 `DIMARC` | 호의 곡선 길이를 기입한다. | +| 치수 | 반지름·지름 치수 | 원과 호의 반지름 또는 지름을 기입한다. | +| 치수 | 꺾기 치수 | 큰 반지름의 중심 위치를 축약해 표시한다. | +| 치수 | 세로좌표 치수 | 기준 원점에 대한 X 또는 Y 좌표를 기입한다. | +| 치수 | 기준선·연속 치수 | 공통 기준 또는 앞 치수 끝점에서 연속 기입한다. | +| 치수 | 중심 표식 `CENTERMARK` | 원 또는 호 중심에 연관 중심 표식을 작성한다. | +| 치수 | 중심선 `CENTERLINE` | 두 선 또는 폴리선 세그먼트 사이에 연관 중심선을 작성한다. | +| 치수 | 치수 스타일 `DIMSTYLE` | 치수선·화살표·문자·단위·공차 형식을 관리한다. | +| 치수 | 치수 업데이트 | 변경한 치수 스타일을 기존 치수에 적용한다. | +| 치수 | 치수 끊기 `DIMBREAK` | 교차 객체나 지정점에서 치수선·연장선을 끊어 표시한다. | +| 치수 | 치수 간격 `DIMSPACE` | 평행 또는 동심 치수 사이의 간격을 조정한다. | +| 치수 | 치수 재연관 `DIMREASSOCIATE` | 끊어진 치수를 실제 형상점과 다시 연관시킨다. | +| 치수 | 검사 치수 `DIMINSPECT` | 검사 빈도·값 정보를 포함한 검사 치수를 작성한다. | +| 기호 | 기하공차 `TOLERANCE` | 형상·자세·위치 공차 기호 프레임을 작성한다. | +| 지시선 | 다중 지시선 `MLEADER` | 화살표와 문자 또는 블록 주석을 연결한다. | +| 지시선 | 다중 지시선 스타일 `MLEADERSTYLE` | 다중 지시선의 형식과 기본 동작을 작성·수정한다. | +| 지시선 | 지시선 추가·제거·정렬 | 기존 다중 지시선의 가지와 배치를 편집한다. | +| 지시선 | 지시선 수집 `MLEADERCOLLECT` | 블록 내용의 여러 다중 지시선을 행 또는 열로 정렬한다. | +| 표 | 테이블 `TABLE` | 행·열과 셀 서식을 가진 도면 표를 작성한다. | +| 표 | 테이블 스타일 `TABLESTYLE` | 표의 제목·머리글·데이터 셀 형식을 관리한다. | +| 표식 | 구름형 리비전 `REVCLOUD` | 변경 또는 검토 범위를 구름형 선으로 표시한다. | +| 주석 축척 | 주석 객체·축척 | 여러 뷰포트 축척에서 일정한 출력 크기를 유지한다. | +| 주석 축척 | 축척 리스트 편집 `SCALELISTEDIT` | 도면에서 사용할 주석·뷰포트 축척 목록을 관리한다. | + +## 6. 파라메트릭 탭 — 구속조건 + +| 그룹(패널) | 기능/명령 | 짧은 설명 | +|---|---|---| +| 기하 구속 | 자동 구속 `AUTOCONSTRAIN` | 객체 관계를 분석해 여러 기하 구속을 자동 적용한다. | +| 기하 구속 | 일치 | 두 점 또는 점과 객체를 같은 위치에 구속한다. | +| 기하 구속 | 동일선상 | 객체들이 같은 무한선 위에 있도록 구속한다. | +| 기하 구속 | 동심 | 원·호·타원이 같은 중심을 갖도록 구속한다. | +| 기하 구속 | 고정 | 점 또는 객체의 위치·방향을 고정한다. | +| 기하 구속 | 평행·직교 | 두 선을 평행 또는 직각으로 유지한다. | +| 기하 구속 | 수평·수직 | 선 또는 두 점을 수평·수직으로 유지한다. | +| 기하 구속 | 접선 | 두 곡선이 서로 접하도록 구속한다. | +| 기하 구속 | 매끄럽게 | 스플라인과 다른 곡선을 부드럽게 연결한다. | +| 기하 구속 | 대칭 | 객체를 지정 대칭축에 대해 대칭으로 유지한다. | +| 기하 구속 | 같음 | 객체의 길이 또는 반지름을 같게 유지한다. | +| 치수 구속 | 선형·정렬·수평·수직 | 거리 값을 매개변수로 지정해 형상을 제어한다. | +| 치수 구속 | 반지름·지름·각도 | 크기와 각도를 매개변수로 지정한다. | +| 치수 구속 | 변환 | 일반 치수를 연관 치수 구속으로 변환한다. | +| 관리 | 매개변수 관리자 | 이름 있는 변수, 값 및 수식을 관리한다. | +| 관리 | 구속 표시·숨기기·삭제 | 구속 막대와 치수 구속의 표시 및 제거를 관리한다. | + +## 7. 뷰 탭 — 탐색·뷰·뷰포트·팔레트 + +| 그룹(패널) | 기능/명령 | 짧은 설명 | +|---|---|---| +| 탐색 | 줌 `ZOOM` | 확대·축소, 범위 전체, 윈도우 등의 방법으로 뷰를 변경한다. | +| 탐색 | 초점이동 `PAN` | 배율을 유지한 채 화면의 관찰 위치를 이동한다. | +| 탐색 | 탐색 막대 | 줌·초점이동 등 화면 탐색 도구를 모아 제공한다. | +| 뷰 | 명명된 뷰 `VIEW` | 현재 화면·도면층·UCS 상태를 이름으로 저장하고 복원한다. | +| 뷰 | 새 명명 뷰 `NEWVIEW` | 현재 화면 또는 지정 영역을 새 이름의 뷰로 저장한다. | +| 뷰 | 재생성 `REGEN` | 현재 뷰포트의 객체 표시를 다시 계산한다. | +| 뷰 | 전체 재생성 `REGENALL` | 모든 뷰포트의 객체 표시를 다시 계산한다. | +| 뷰포트 | 모형 공간 뷰포트 | 도면 영역을 여러 독립 관찰 창으로 나눈다. | +| 뷰포트 | 배치 뷰포트 `MVIEW` | 종이 공간에 모형을 표시하는 출력 창을 작성한다. | +| 뷰포트 | 뷰포트 잠금 | 배치 뷰포트의 축척과 관찰 상태 변경을 막는다. | +| 좌표 | UCS `UCS` | 사용자 좌표계의 원점과 축 방향을 설정한다. | +| 좌표 | 월드 UCS | 좌표계를 기본 WCS로 복원한다. | +| 팔레트 | 도구 팔레트 | 자주 쓰는 블록·해치·명령 도구 모음을 표시한다. | +| 팔레트 | 개수 `COUNT` | 선택 객체·블록의 인스턴스를 세고 결과를 강조·검토한다. | +| 팔레트 | DesignCenter | 다른 도면의 블록·도면층·스타일을 탐색해 가져온다. | +| 팔레트 | 시트 세트 관리자 | 여러 도면 시트의 집합·번호·출력을 관리한다. | +| 팔레트 | 명령행 | 명령 입력, 옵션 선택 및 작업 기록을 표시한다. | +| 팔레트 | 명령 매크로 | 반복 명령 시퀀스를 검토·저장·실행한다. | +| 인터페이스 | 파일 탭 | 열려 있는 여러 도면 사이를 전환한다. | +| 인터페이스 | 타일·계단식 배열 | 여러 열린 도면 창의 배치를 조정한다. | +| 인터페이스 | 전체 화면 정리 `CLEANSCREENON` | 도구막대와 팔레트를 숨겨 도면 영역을 최대화한다. | + +## 8. 관리 탭 — 표준·사용자화·자동화 + +| 그룹(패널) | 기능/명령 | 짧은 설명 | +|---|---|---| +| 작업 기록 | 동작 기록기 | 사용자 명령과 입력을 기록해 반복 재생한다. | +| 사용자화 | 사용자 인터페이스 `CUI` | 리본·도구막대·메뉴·단축키·마우스 동작을 사용자화한다. | +| 사용자화 | 별칭 편집 | 명령행에서 쓰는 짧은 명령 별칭을 관리한다. | +| 사용자화 | 작업공간 | 리본·팔레트·도구막대 배치를 저장하고 전환한다. | +| CAD 표준 | 표준 구성 `STANDARDS` | 도면층·문자·치수·선종류 표준 파일을 연결한다. | +| CAD 표준 | 표준 검사 `CHECKSTANDARDS` | 현재 도면이 연결된 CAD 표준과 다른 항목을 찾는다. | +| CAD 표준 | 도면층 변환 `LAYTRANS` | 현재 도면층을 지정 표준 도면층 이름과 특성으로 매핑한다. | +| 응용프로그램 | 응용프로그램 로드 `APPLOAD` | AutoLISP·ObjectARX 등 확장 프로그램을 로드한다. | +| 응용프로그램 | Visual LISP | AutoLISP 코드를 작성·검사하는 개발 환경을 연다. | +| 스크립트 | 스크립트 실행 `SCRIPT` | 텍스트 파일에 기록된 명령 시퀀스를 실행한다. | +| 정리 | 소거 `PURGE` | 사용하지 않는 블록·도면층·스타일 정의를 제거한다. | + +## 9. 출력 탭 — 배치·플롯·게시·내보내기 + +| 그룹(패널) | 기능/명령 | 짧은 설명 | +|---|---|---| +| 플롯 | 플롯 `PLOT` | 프린터·용지·영역·축척·스타일을 설정해 출력한다. | +| 플롯 | 미리보기 `PREVIEW` | 실제 출력 전 용지 결과를 확인한다. | +| 플롯 | 페이지 설정 `PAGESETUP` | 배치별 출력 장치와 용지 설정을 저장한다. | +| 플롯 | 플롯 스타일 | 색상·선가중치 등의 CTB/STB 출력 규칙을 관리한다. | +| 플롯 | 플로터 관리자 `PLOTTERMANAGER` | 플로터 구성 파일과 장치 설정을 관리한다. | +| 플롯 | 플롯 스타일 관리자 `STYLESMANAGER` | CTB·STB 플롯 스타일 테이블 파일을 관리한다. | +| 게시 | 게시 `PUBLISH` | 여러 도면·배치를 한 번에 DWF·PDF·프린터로 출력한다. | +| 게시 | 배치 플롯 | 시트 목록을 구성해 일괄 출력한다. | +| 내보내기 | PDF 내보내기 `EXPORTPDF` | 도면 또는 배치를 PDF 파일로 만든다. | +| 내보내기 | DWF/DWFx 내보내기 | 검토·배포용 Autodesk 형식으로 내보낸다. | +| 내보내기 | 기타 형식 `EXPORT` | 지원되는 다른 교환 파일 형식으로 저장한다. | +| 전송 | 전자 전송 `ETRANSMIT` | 도면과 참조·글꼴·플롯 설정을 하나의 전달 패키지로 묶는다. | + +## 10. 공동작업·검토 + +| 그룹(패널) | 기능/명령 | 짧은 설명 | +|---|---|---| +| 비교 | DWG 비교 `COMPARE` | 두 DWG의 추가·삭제·변경 객체를 색으로 구분한다. | +| 비교 | 외부 참조 비교 | 부착된 외부 참조의 변경 사항을 현재 도면과 비교한다. | +| 검토 | 추적 `TRACE` | 원도면을 바꾸지 않는 검토용 투명 오버레이를 만든다. | +| 검토 | 추적에서 복사 `COPYFROMTRACE` | 추적에 있는 객체를 원도면의 같은 위치로 복사한다. | +| 검토 | 표식 가져오기 `MARKUPIMPORT` | PDF·JPG·PNG 표식을 새 추적으로 가져와 원도면 위에 겹친다. | +| 검토 | 표식 도우미 `MARKUPASSIST` | 표식의 문자·구름·지시 등을 인식해 도면 반영을 돕는다. | +| 공유 | 뷰 공유 `SHAREDVIEWS` | 도면의 온라인 검토용 뷰를 생성하고 의견을 교환한다. | +| 공유 | 도면 공유 `SHARE` | 현재 도면 복사본을 웹·모바일에서 보거나 편집하는 링크를 만든다. | +| 이력 | 활동 정보 `ACTIVITYINSIGHTSOPEN` | 도면 열기·저장·편집 등 작업 이벤트를 추적한다. | +| 버전 | 버전 이력 `DWGHISTORY` | 지원되는 클라우드에 저장된 도면의 이전 버전을 조회·비교한다. | + +## 11. 리본 밖의 핵심 제도 기능 + +| 영역 | 기능 | 짧은 설명 | +|---|---|---| +| 선택 | 창 선택 | 왼쪽→오른쪽 사각형 안에 완전히 포함된 객체를 선택한다. | +| 선택 | 교차 선택 | 오른쪽→왼쪽 영역에 포함되거나 걸치는 객체를 선택한다. | +| 선택 | 올가미 선택 | 자유형 경계로 객체를 포함 또는 교차 선택한다. | +| 선택 | 선택 순환 | 겹친 객체 목록에서 원하는 객체를 골라 선택한다. | +| 직접 편집 | 그립 편집 | 선택 객체의 정점·중간점·반지름 등을 직접 끌어 수정한다. | +| 직접 편집 | 다기능 그립 | 폴리선 정점 추가·제거, 호 전환 등 상황별 작업을 제공한다. | +| 정확도 | 객체 스냅 `OSNAP` | 끝점·중간점·중심·교차점·접점 등 정확한 점을 포착한다. | +| 정확도 | 객체 스냅 추적 `(F11)` | 포착한 점에서 임시 정렬 경로를 추적한다. | +| 정확도 | 극좌표 추적 `(F10)` | 지정 각도 증분을 따라 커서 이동을 안내한다. | +| 정확도 | 직교 모드 `(F8)` | 커서 이동을 현재 UCS의 수평·수직 방향으로 제한한다. | +| 정확도 | 그리드·스냅 | 화면 격자를 표시하고 커서 이동 간격을 제한한다. | +| 정확도 | 동적 입력 `(F12, DYNMODE 시스템 변수)` | 커서 근처에서 좌표·거리·각도 및 명령 옵션을 입력한다. | +| 정확도 | 직접 거리 입력 | 방향을 지정한 뒤 키보드로 정확한 거리를 입력한다. | +| 좌표 | 절대·상대·극좌표 | 전역 좌표, 이전 점 기준 좌표, 거리·각도로 점을 입력한다. | +| 작업 흐름 | 명령 직접 입력 | 명령 이름, 별칭, 옵션, 숫자와 좌표를 명령행에 연속 입력한다. | +| 작업 흐름 | 명령 자동완성 | 입력 중인 명령·시스템 변수·콘텐츠를 검색해 제안한다. | +| 작업 흐름 | 실행 취소·다시 실행 | 명령 단위로 작업 이력을 되돌리거나 복원한다. | +| 작업 흐름 | 반복·최근 명령 | 직전 또는 최근 사용 명령을 다시 실행한다. | +| 표시 | 선가중치 표시 | 실제 출력 선 굵기의 화면 표시를 전환한다. | +| 표시 | 투명도 표시 | 객체·도면층 투명도의 화면 표시를 전환한다. | +| 표시 | 주석 가시성 | 현재 축척을 지원하지 않는 주석의 표시 여부를 전환한다. | +| 공간 | 모형·배치 | 실제 크기 설계 공간과 용지 출력 공간을 전환한다. | +| 공간 | 뷰포트 축척 | 배치에서 모형 표시 축척을 정밀하게 지정한다. | +| 파일 | 새 도면·열기·저장·다른 이름 저장 | DWG 작업 파일의 기본 수명주기를 관리한다. | +| 파일 | 자동 저장·복구 | 비정상 종료 시 사용할 임시 저장과 복구 정보를 관리한다. | +| 파일 | 감사 `AUDIT` | 응용프로그램 메뉴에서 열린 도면의 오류를 검사·수정한다. | +| 파일 | 복구 `RECOVER` | 응용프로그램 메뉴에서 손상된 도면을 복구하면서 연다. | +| 호환 | DWG 변환 `DWGCONVERT` | 여러 도면을 지정 DWG 버전으로 일괄 변환한다. | +| 호환 | 단위 `UNITS` | 길이·각도 형식, 정밀도 및 삽입 축척 단위를 설정한다. | + +## 12. 2D 상황별 리본 탭 + +> 해당 객체를 선택하거나 편집 명령을 실행할 때만 나타나는 리본이다. + +| 상황별 탭 | 기능 | 짧은 설명 | +|---|---|---| +| 해치 작성/편집 | 원점 설정 | 해치 패턴이 시작되는 기준점을 지정한다. | +| 해치 작성/편집 | 연관·주석 | 경계 연동과 주석 축척 적용 여부를 설정한다. | +| 해치 작성/편집 | 경계 재작성 | 해치에서 폴리선 또는 영역 경계를 다시 만든다. | +| 문자 편집기 | 굵게·기울임·밑줄·색상 | 선택 문자 범위의 서식을 변경한다. | +| 문자 편집기 | 정렬·들여쓰기·줄 간격 | 문단 배치와 간격을 조정한다. | +| 문자 편집기 | 열 | 여러 줄 문자를 동적·정적 열로 나눈다. | +| 블록 편집기 | 블록 테스트 | 동적 블록의 그립과 동작을 시험한다. | +| 블록 편집기 | 매개변수·동작 | 동적 블록에 점·선형·회전 등의 매개변수와 동작을 지정한다. | +| 블록 편집기 | 가시성 상태 | 하나의 블록에서 표시할 객체 조합을 관리한다. | +| 배열 편집 | 항목 수·간격 | 배열 항목의 개수와 행·열 또는 경로 간격을 수정한다. | +| 배열 편집 | 항목 교체·편집 | 배열의 원본 항목을 편집하거나 다른 객체로 바꾼다. | +| 배열 편집 | 원본 편집·재설정 | 원본 객체를 편집하고 배열 재지정을 초기화한다. | +| 외부 참조·언더레이 | 페이드·대비 | 외부 참조와 PDF·DWF·DGN 언더레이의 화면 특성을 조정한다. | +| 외부 참조 | 참조 도면층 | 참조에 포함된 도면층 표시를 관리한다. | +| 이미지 | 페이드·대비·밝기 | 래스터 이미지의 화면 특성을 조정한다. | +| PDF/DWF/DGN 언더레이 | 단색·언더레이 도면층 | 단색 표시와 원본 도면층 가시성을 관리한다. | +| 배치 | 명명된 뷰 삽입 | 저장된 모형 뷰를 배치 뷰포트로 배치한다. | +| 테이블 셀 | 행·열 삽입·삭제 | 선택 셀 주변의 표 구조를 편집한다. | +| 테이블 셀 | 병합·병합 해제 | 여러 셀을 합치거나 원래 셀로 되돌린다. | +| 테이블 셀 | 셀 형식·정렬·테두리 | 데이터 형식과 표시 서식을 조정한다. | + +## 13. 조사 근거 및 재검증 자료 + +| 구분 | 자료 | 확인 내용 | +|---|---|---| +| Autodesk 도움말 | [AutoCAD 2024 한국어 도움말](https://help.autodesk.com/view/ACD/2024/KOR/) | 기본 AutoCAD 명령·작업 절차·UI 기능 | +| Autodesk 공식 PDF | [AutoCAD 2024 Shortcuts Guide](https://damassets.autodesk.net/content/dam/autodesk/www/shortcuts/autocad/autocad-shortcut-guide-en.pdf) | 그리기·수정·주석·도면층·조회 등 주요 명령 교차 확인 | +| Autodesk 공식 블로그 | [Essential Keyboard Shortcuts and Commands](https://www.autodesk.com/blogs/autocad/work-faster-in-autocad-essential-keyboard-shortcuts-and-commands/) | 대표 명령의 작업별 분류와 짧은 기능 설명 교차 확인 | +| Autodesk 도움말 | [블록 삽입 정보](https://help.autodesk.com/cloudhelp/2024/KOR/AutoCAD-Core/files/GUID-BC0FD3C1-3BFC-4C5D-AB9A-BF480D5084BE.htm) | 홈 탭 블록 패널, 블록 갤러리와 팔레트 | +| Autodesk 도움말 | [외부 참조 경로로 작업하려면](https://help.autodesk.com/cloudhelp/2024/KOR/AutoCAD-Core/files/GUID-97F90017-E17A-4209-A6BD-1E205BF892E1.htm) | 뷰 탭 팔레트 패널과 외부 참조 관리 | +| Autodesk 도움말 | [도면 비교를 통해 작업하려면](https://help.autodesk.com/cloudhelp/2024/KOR/AutoCAD-Core/files/GUID-744D8A49-D582-4C06-80B1-6B0313E92CA2.htm) | 뷰·공동작업 탭의 DWG 비교 기능 | +| Autodesk 도움말 | [리본 사용자화 FAQ](https://help.autodesk.com/cloudhelp/2024/KOR/AutoCAD-Core/files/GUID-7711CB8D-C865-42C1-9F21-7179FA7BBE63.htm) | 리본 탭·패널·명령의 사용자화 가능 범위 | +| Autodesk 도움말 | [명령 매크로 정보](https://help.autodesk.com/cloudhelp/2024/KOR/AutoCAD-Core/files/GUID-6B27C0A2-6446-4C75-9853-6834F4FFFF89.htm) | 뷰·자동화 탭의 명령 매크로 팔레트 | +| Autodesk 도움말 | [점에서 끊기](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DidYouKnow/files/GUID-73B7DEA8-309E-4823-9F65-DB0A2E1FBC75.htm) | `BREAKATPOINT`의 지원 버전과 홈 탭 위치 | +| Autodesk 도움말 | [파라메트릭 도면과 구속조건](https://help.autodesk.com/cloudhelp/2023/ENU/AutoCAD-Core/files/GUID-899E008D-B422-4DF2-AC8D-1A4F5701ED4E.htm) | 기하·치수 구속의 범위와 동작 | +| Autodesk 도움말 | [Markup Import와 Markup Assist](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Platform/files/GUID-0CD6B2CE-EAF0-478B-B39B-F4821F9DC91D.htm) | 표식 가져오기·인식·반영 기능과 제약 | +| Autodesk 도움말 | [Activity Insights](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-WhatsNew/files/GUID-074F77C1-496D-49D2-8D64-FD545753CAB7.htm) | 2024 작업 이벤트 기록 기능과 리본 위치 | +| Autodesk 도움말 | [Drawing History 팔레트](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Core/files/GUID-6916CA9F-0BD1-49ED-9DA4-2FA3615998DD.htm) | 지원 클라우드 도면의 버전 조회·비교 기능 | +| Autodesk 도움말 | [Shared Views 명령](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Core/files/GUID-C9055F88-D079-441F-938B-37A7E0E390D9.htm) | 온라인 검토 뷰와 의견 교환 기능 | +| Autodesk 도움말 | [Action Macro 기록](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Customization/files/GUID-FEAD3614-CD33-4B60-BC00-4CBC98D8CBCB.htm) | 동작 기록기의 명령·입력 기록 및 재생 기능 | +| Autodesk 도움말 | [Purge 대화상자](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Core/files/GUID-C6B62BBD-C363-4DC7-AF43-036A7B287473.htm) | 미사용 명명 객체와 고아 데이터 정리 기능 | +| Autodesk 도움말 | [AutoCAD 2024 새 명령·시스템 변수](https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Core/files/GUID-B93A458E-1A7F-4090-A8CF-87A31C24E404.htm) | 2024의 Activity Insights·Smart Blocks·Trace·Markup 명령 대조 | +| Autodesk 도움말 | [AutoCAD 2024 변경 명령·시스템 변수](https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Core/files/GUID-4FEBA606-95E0-4DC4-A116-257ED86DCD58.htm) | `INSERT`·`COUNT`·`TRACE`·`SHARE` 변경 내용 대조 | +| Autodesk 도움말 | [COUNT 명령](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Core/files/GUID-3A0C3460-6ABC-4D13-BF1F-D2BFCD399851.htm) | 동일 객체·블록 개수 계산 기능 | +| Autodesk 도움말 | [객체 개수로 작업](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-27873872-0DB9-4EE1-97F5-43F91F9C3785.htm) | 뷰 탭 팔레트 패널의 Count 위치와 검토 기능 | +| Autodesk 도움말 | [OVERKILL 명령](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-44B9ECFC-752C-4CC5-9DA3-84DBF3B17CA6.htm) | 중복·겹침 객체 제거 및 결합 기능 | +| Autodesk 도움말 | [Smart Blocks 교체](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-WhatsNew/files/GUID-4F8E11D6-917A-43F3-B596-C8F296D32519.htm) | `BREPLACE`의 제안·교체와 특성 유지 동작 | +| Autodesk 도움말 | [Trace 업데이트](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-WhatsNew/files/GUID-F9C11306-0F53-4F1A-92C7-8D77EA42823A.htm) | `COPYFROMTRACE`와 추적 화면표시 설정 | +| Autodesk 도움말 | [상태막대 빠른 참조](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-E3B34B0A-EA98-45E9-937A-BF5FEF4152DB.htm) | 스냅·직교·극좌표·동적 입력 등 제도 토글의 기능 | +| Autodesk 도움말 | [열린 도면 복구](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-LT/files/GUID-7AFE414B-BCC6-470C-B0E9-8228A252EE7C.htm) | 응용프로그램 메뉴의 `AUDIT` 위치와 `RECOVER` 구분 | -- 2.54.0 From 487539bba18af840af9b84ec9aa0bef45209e80d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 29 Aug 2026 23:30:04 +0900 Subject: [PATCH 03/66] auto: 2026-08-29 23:30 (ESD_LAPTOP) --- resources/임시_AutoCAD_리본_기능_조사.md | 583 ++++++++++++----------- 1 file changed, 292 insertions(+), 291 deletions(-) diff --git a/resources/임시_AutoCAD_리본_기능_조사.md b/resources/임시_AutoCAD_리본_기능_조사.md index ed079bf3..9377d6a1 100644 --- a/resources/임시_AutoCAD_리본_기능_조사.md +++ b/resources/임시_AutoCAD_리본_기능_조사.md @@ -4,336 +4,337 @@ - 조사일: 2026-08-29 - 추가검증: 2026-08-29, Autodesk 공식 명령 참조·2024 신규/변경 명령·상태막대 참조와 재대조 - 용도: B07 상세설계 웹 CAD 기능 검토용 임시 참고자료 +- 단축키 기준: AutoCAD 2024 기본 명령 별칭과 키 조합. `—`는 공식 기본 단축키를 확인하지 못했거나 리본·상황별 조작만 있는 기능이다. 명령 별칭은 `acad.pgp`, 키 조합은 CUI에서 사용자화할 수 있다. - 주의: AutoCAD 리본은 제품, 작업공간, 화면 폭, 설치 확장 및 CUI 사용자화에 따라 탭·패널·아이콘의 표시 여부와 위치가 달라진다. 아래 표는 기본 2D 제도 리본의 대표 기능을 작업 기준으로 합친 목록이며, 모든 AutoCAD 명령의 전수 목록은 아니다. 같은 기능이 여러 탭·패널에 나타나도 최초 한 번만 기재한다. 3D 모델링, 전문화 도구 세트, Express Tools 및 타사 애드인은 제외했다. ## 1. 홈 탭 — 그리기 -| 그룹(패널) | 기능/명령 | 짧은 설명 | -|---|---|---| -| 그리기 | 선 `LINE` | 두 점 사이에 직선 세그먼트를 작성한다. | -| 그리기 | 폴리선 `PLINE` | 연결된 선·호를 하나의 객체로 작성한다. | -| 그리기 | 원 `CIRCLE` | 중심·반지름 등 여러 조건으로 원을 작성한다. | -| 그리기 | 호 `ARC` | 3점, 중심, 시작·끝점 등의 조건으로 호를 작성한다. | -| 그리기 | 직사각형 `RECTANG` | 직사각형 형태의 닫힌 폴리선을 작성한다. | -| 그리기 | 다각형 `POLYGON` | 지정한 변 수의 정다각형을 작성한다. | -| 그리기 | 타원 `ELLIPSE` | 중심 또는 축 길이로 타원·타원호를 작성한다. | -| 그리기 | 스플라인 `SPLINE` | 맞춤점 또는 조정 정점으로 부드러운 곡선을 작성한다. | -| 그리기 | 다중선 `MLINE` | 여러 평행선으로 구성된 다중선 객체를 작성한다. | -| 그리기 | 다중선 스타일 `MLSTYLE` | 다중선의 요소 수·간격·선종류와 끝막음을 관리한다. | -| 그리기 | 구성선 `XLINE` | 양방향으로 무한한 기준선을 작성한다. | -| 그리기 | 광선 `RAY` | 한 방향으로 무한한 기준선을 작성한다. | -| 그리기 | 점 `POINT` | 점 객체를 작성한다. | -| 그리기 | 도넛 `DONUT` | 채워진 원 또는 링 모양 폴리선을 작성한다. | -| 그리기 | 등분 `DIVIDE` | 객체를 자르지 않고 동일 간격의 점 또는 블록을 지정 개수만큼 배치한다. | -| 그리기 | 길이분할 `MEASURE` | 객체를 자르지 않고 지정 거리마다 점 또는 블록을 배치한다. | -| 그리기 | 해치 `HATCH` | 닫힌 경계에 패턴·솔리드·그라데이션 채움을 작성한다. | -| 그리기 | 그라데이션 `GRADIENT` | 닫힌 영역에 색상 그라데이션을 적용한다. | -| 그리기 | 경계 `BOUNDARY` | 닫힌 영역에서 폴리선 또는 영역 객체를 만든다. | -| 그리기 | 영역 `REGION` | 닫힌 평면 객체를 영역 객체로 변환한다. | -| 그리기 | 와이프아웃 `WIPEOUT` | 뒤쪽 객체를 가리는 마스크 영역을 작성한다. | +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | +|---|---|---|---| +| 그리기 | 선 `LINE` | L | 두 점 사이에 직선 세그먼트를 작성한다. | +| 그리기 | 폴리선 `PLINE` | PL | 연결된 선·호를 하나의 객체로 작성한다. | +| 그리기 | 원 `CIRCLE` | C | 중심·반지름 등 여러 조건으로 원을 작성한다. | +| 그리기 | 호 `ARC` | A | 3점, 중심, 시작·끝점 등의 조건으로 호를 작성한다. | +| 그리기 | 직사각형 `RECTANG` | REC | 직사각형 형태의 닫힌 폴리선을 작성한다. | +| 그리기 | 다각형 `POLYGON` | POL | 지정한 변 수의 정다각형을 작성한다. | +| 그리기 | 타원 `ELLIPSE` | EL | 중심 또는 축 길이로 타원·타원호를 작성한다. | +| 그리기 | 스플라인 `SPLINE` | SPL | 맞춤점 또는 조정 정점으로 부드러운 곡선을 작성한다. | +| 그리기 | 다중선 `MLINE` | ML | 여러 평행선으로 구성된 다중선 객체를 작성한다. | +| 그리기 | 다중선 스타일 `MLSTYLE` | — | 다중선의 요소 수·간격·선종류와 끝막음을 관리한다. | +| 그리기 | 구성선 `XLINE` | XL | 양방향으로 무한한 기준선을 작성한다. | +| 그리기 | 광선 `RAY` | — | 한 방향으로 무한한 기준선을 작성한다. | +| 그리기 | 점 `POINT` | PO | 점 객체를 작성한다. | +| 그리기 | 도넛 `DONUT` | DO | 채워진 원 또는 링 모양 폴리선을 작성한다. | +| 그리기 | 등분 `DIVIDE` | DIV | 객체를 자르지 않고 동일 간격의 점 또는 블록을 지정 개수만큼 배치한다. | +| 그리기 | 길이분할 `MEASURE` | ME | 객체를 자르지 않고 지정 거리마다 점 또는 블록을 배치한다. | +| 그리기 | 해치 `HATCH` | H / BH | 닫힌 경계에 패턴·솔리드·그라데이션 채움을 작성한다. | +| 그리기 | 그라데이션 `GRADIENT` | GD | 닫힌 영역에 색상 그라데이션을 적용한다. | +| 그리기 | 경계 `BOUNDARY` | BO | 닫힌 영역에서 폴리선 또는 영역 객체를 만든다. | +| 그리기 | 영역 `REGION` | REG | 닫힌 평면 객체를 영역 객체로 변환한다. | +| 그리기 | 와이프아웃 `WIPEOUT` | — | 뒤쪽 객체를 가리는 마스크 영역을 작성한다. | ## 2. 홈 탭 — 수정 -| 그룹(패널) | 기능/명령 | 짧은 설명 | -|---|---|---| -| 수정 | 이동 `MOVE` | 선택 객체를 기준점에서 새 위치로 이동한다. | -| 수정 | 복사 `COPY` | 선택 객체를 하나 이상의 위치에 복제한다. | -| 수정 | 회전 `ROTATE` | 기준점을 중심으로 객체를 회전한다. | -| 수정 | 축척 `SCALE` | 기준점과 비율 또는 참조 길이로 크기를 변경한다. | -| 수정 | 정렬 `ALIGN` | 원본점과 대상점 쌍으로 객체를 이동·회전하고 선택적으로 축척한다. | -| 수정 | 대칭 `MIRROR` | 대칭축을 기준으로 객체를 반사 복사한다. | -| 수정 | 간격띄우기 `OFFSET` | 평행선·동심원·등거리 곡선을 작성한다. | -| 수정 | 배열 `ARRAY` | 직사각형·경로·원형 패턴으로 객체를 반복 배치한다. | -| 수정 | 자르기 `TRIM` | 경계를 기준으로 불필요한 객체 부분을 자른다. | -| 수정 | 연장 `EXTEND` | 객체 끝을 지정 경계까지 연장한다. | -| 수정 | 신축 `STRETCH` | 교차 선택한 정점과 객체 일부를 늘이거나 이동한다. | -| 수정 | 모깎기 `FILLET` | 두 객체를 지정 반지름의 호로 연결한다. | -| 수정 | 모따기 `CHAMFER` | 두 객체를 직선 모따기로 연결한다. | -| 수정 | 곡선 혼합 `BLEND` | 두 곡선을 부드러운 스플라인으로 연결한다. | -| 수정 | 끊기 `BREAK` | 객체의 두 점 사이를 제거하거나 한 점에서 나눈다. | -| 수정 | 점에서 끊기 `BREAKATPOINT` | 객체를 지정점에서 두 객체로 나눈다. | -| 수정 | 결합 `JOIN` | 끝이 맞는 선·호·폴리선 등을 하나로 결합한다. | -| 수정 | 분해 `EXPLODE` | 블록·폴리선 등 복합 객체를 구성요소로 분해한다. | -| 수정 | 지우기 `ERASE` | 선택 객체를 삭제한다. | -| 수정 | 길이조정 `LENGTHEN` | 선과 호의 길이 또는 끼인각을 변경한다. | -| 수정 | 폴리선 편집 `PEDIT` | 폴리선 결합, 폭, 정점, 곡선 맞춤 등을 편집한다. | -| 수정 | 해치 편집 `HATCHEDIT` | 기존 해치의 패턴, 축척, 각도, 경계를 수정한다. | -| 수정 | 그리기 순서 `DRAWORDER` | 객체의 앞뒤 표시 순서를 변경한다. | -| 수정 | 특성 일치 `MATCHPROP` | 원본 객체의 표시 특성을 다른 객체에 복사한다. | -| 수정 | 중복 객체 삭제 `OVERKILL` | 중복·겹침 형상을 제거하고 이어진 선·호를 결합한다. | -| 수정 | 방향 반전 `REVERSE` | 선·폴리선·스플라인 등의 시작점과 끝점 방향을 뒤집는다. | -| 수정 | 특성을 ByLayer로 `SETBYLAYER` | 선택 객체의 특성 재지정을 ByLayer 값으로 변경한다. | +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | +|---|---|---|---| +| 수정 | 이동 `MOVE` | M | 선택 객체를 기준점에서 새 위치로 이동한다. | +| 수정 | 복사 `COPY` | CO | 선택 객체를 하나 이상의 위치에 복제한다. | +| 수정 | 회전 `ROTATE` | RO | 기준점을 중심으로 객체를 회전한다. | +| 수정 | 축척 `SCALE` | SC | 기준점과 비율 또는 참조 길이로 크기를 변경한다. | +| 수정 | 정렬 `ALIGN` | AL | 원본점과 대상점 쌍으로 객체를 이동·회전하고 선택적으로 축척한다. | +| 수정 | 대칭 `MIRROR` | MI | 대칭축을 기준으로 객체를 반사 복사한다. | +| 수정 | 간격띄우기 `OFFSET` | O | 평행선·동심원·등거리 곡선을 작성한다. | +| 수정 | 배열 `ARRAY` | AR | 직사각형·경로·원형 패턴으로 객체를 반복 배치한다. | +| 수정 | 자르기 `TRIM` | TR | 경계를 기준으로 불필요한 객체 부분을 자른다. | +| 수정 | 연장 `EXTEND` | EX | 객체 끝을 지정 경계까지 연장한다. | +| 수정 | 신축 `STRETCH` | S | 교차 선택한 정점과 객체 일부를 늘이거나 이동한다. | +| 수정 | 모깎기 `FILLET` | F | 두 객체를 지정 반지름의 호로 연결한다. | +| 수정 | 모따기 `CHAMFER` | CHA | 두 객체를 직선 모따기로 연결한다. | +| 수정 | 곡선 혼합 `BLEND` | — | 두 곡선을 부드러운 스플라인으로 연결한다. | +| 수정 | 끊기 `BREAK` | BR | 객체의 두 점 사이를 제거하거나 한 점에서 나눈다. | +| 수정 | 점에서 끊기 `BREAKATPOINT` | — | 객체를 지정점에서 두 객체로 나눈다. | +| 수정 | 결합 `JOIN` | J | 끝이 맞는 선·호·폴리선 등을 하나로 결합한다. | +| 수정 | 분해 `EXPLODE` | X | 블록·폴리선 등 복합 객체를 구성요소로 분해한다. | +| 수정 | 지우기 `ERASE` | E | 선택 객체를 삭제한다. | +| 수정 | 길이조정 `LENGTHEN` | LEN | 선과 호의 길이 또는 끼인각을 변경한다. | +| 수정 | 폴리선 편집 `PEDIT` | PE | 폴리선 결합, 폭, 정점, 곡선 맞춤 등을 편집한다. | +| 수정 | 해치 편집 `HATCHEDIT` | HE | 기존 해치의 패턴, 축척, 각도, 경계를 수정한다. | +| 수정 | 그리기 순서 `DRAWORDER` | DR | 객체의 앞뒤 표시 순서를 변경한다. | +| 수정 | 특성 일치 `MATCHPROP` | MA | 원본 객체의 표시 특성을 다른 객체에 복사한다. | +| 수정 | 중복 객체 삭제 `OVERKILL` | — | 중복·겹침 형상을 제거하고 이어진 선·호를 결합한다. | +| 수정 | 방향 반전 `REVERSE` | — | 선·폴리선·스플라인 등의 시작점과 끝점 방향을 뒤집는다. | +| 수정 | 특성을 ByLayer로 `SETBYLAYER` | — | 선택 객체의 특성 재지정을 ByLayer 값으로 변경한다. | ## 3. 홈 탭 — 도면층·특성·그룹·유틸리티 -| 그룹(패널) | 기능/명령 | 짧은 설명 | -|---|---|---| -| 도면층 | 도면층 특성 `LAYER` | 도면층을 만들고 이름·색상·선종류·출력 여부를 관리한다. | -| 도면층 | 현재 도면층 설정 | 선택한 도면층을 새 객체의 기본 도면층으로 지정한다. | -| 도면층 | 켜기/끄기 | 도면층 객체의 표시를 켜거나 끈다. | -| 도면층 | 동결/동결해제 | 도면층을 재생성 및 표시 대상에서 제외하거나 복원한다. | -| 도면층 | 잠금/잠금해제 | 도면층 객체의 편집 가능 여부를 전환한다. | -| 도면층 | 분리 `LAYISO` | 선택 객체의 도면층만 남기고 나머지를 숨기거나 잠근다. | -| 도면층 | 분리 해제 `LAYUNISO` | 도면층 분리 이전 상태를 복원한다. | -| 도면층 | 이전 상태 `LAYERP` | 직전 도면층 설정 변경을 복원한다. | -| 도면층 | 도면층 상태 | 도면층 설정 조합을 이름으로 저장하고 복원한다. | -| 도면층 | 도면층 일치 `LAYMCH` | 선택 객체를 대상 객체의 도면층으로 이동한다. | -| 도면층 | 현재 도면층으로 `LAYCUR` | 선택 객체를 현재 도면층으로 이동한다. | -| 도면층 | 도면층 병합 `LAYMRG` | 한 도면층의 객체를 다른 도면층으로 옮기고 원래 도면층을 제거한다. | -| 도면층 | 도면층 삭제 `LAYDEL` | 선택한 도면층과 그 객체를 삭제한다. | -| 도면층 | 도면층 탐색 `LAYWALK` | 도면층을 선택적으로 표시하며 포함 객체를 확인한다. | -| 특성 | 색상 | 객체 색상을 직접 또는 `ByLayer`·`ByBlock`으로 지정한다. | -| 특성 | 선종류 | 실선·점선·중심선 등 객체 선종류를 지정한다. | -| 특성 | 선가중치 | 화면 및 출력에 사용할 선 굵기를 지정한다. | -| 특성 | 투명도 | 객체 또는 도면층의 투명도를 지정한다. | -| 특성 | 특성 팔레트 `PROPERTIES` | 선택 객체의 형상·표시·데이터 특성을 조회하고 수정한다. | -| 특성 | 빠른 특성 `QUICKPROPERTIES` | 선택 객체 주변에 주요 특성만 간단히 표시한다. | -| 그룹 | 그룹 `GROUP` | 여러 객체를 함께 선택할 수 있는 명명 그룹으로 묶는다. | -| 그룹 | 그룹 해제 `UNGROUP` | 객체 그룹을 해제한다. | -| 유틸리티 | 거리 `DIST` | 두 점 사이의 거리와 각도 차이를 측정한다. | -| 유틸리티 | 반지름 | 원 또는 호의 반지름을 측정한다. | -| 유틸리티 | 각도 | 선·호 또는 지정점 사이의 각도를 측정한다. | -| 유틸리티 | 면적 `AREA` | 객체 또는 지정 경계의 면적과 둘레를 계산한다. | -| 유틸리티 | 빠른 측정 `MEASUREGEOM` | 커서 주변의 치수·거리·각도와 닫힌 영역의 면적을 동적으로 표시한다. | -| 유틸리티 | 점 좌표 `ID` | 지정점의 X·Y·Z 좌표를 표시한다. | -| 유틸리티 | 리스트 `LIST` | 선택 객체의 상세 데이터와 기하 정보를 표시한다. | -| 유틸리티 | 빠른 선택 `QSELECT` | 객체 유형과 특성 조건으로 선택 집합을 만든다. | -| 유틸리티 | 계산기 `QUICKCALC` | 수식, 단위 변환 및 도면 값을 계산한다. | -| 유틸리티 | 유사 선택 `SELECTSIMILAR` | 선택 객체와 지정 특성이 같은 객체를 모두 선택한다. | -| 유틸리티 | 객체 분리 `ISOLATEOBJECTS` | 선택 객체만 표시하고 나머지 객체를 숨긴다. | -| 유틸리티 | 객체 숨기기 `HIDEOBJECTS` | 선택 객체를 일시적으로 숨긴다. | -| 유틸리티 | 객체 분리 종료 `UNISOLATEOBJECTS` | 숨기거나 분리한 객체를 다시 표시한다. | -| 클립보드 | 잘라내기·복사·붙여넣기 | 객체를 시스템 클립보드로 이동·복제한다. | -| 클립보드 | 기준점과 함께 복사 | 지정 기준점을 포함해 객체를 클립보드에 복사한다. | -| 클립보드 | 원래 좌표로 붙여넣기 | 복사한 객체를 원본 도면 좌표에 배치한다. | -| 클립보드 | 블록으로 붙여넣기 | 클립보드 객체를 새 블록으로 삽입한다. | +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | +|---|---|---|---| +| 도면층 | 도면층 특성 `LAYER` | LA | 도면층을 만들고 이름·색상·선종류·출력 여부를 관리한다. | +| 도면층 | 현재 도면층 설정 | — | 선택한 도면층을 새 객체의 기본 도면층으로 지정한다. | +| 도면층 | 켜기/끄기 | — | 도면층 객체의 표시를 켜거나 끈다. | +| 도면층 | 동결/동결해제 | — | 도면층을 재생성 및 표시 대상에서 제외하거나 복원한다. | +| 도면층 | 잠금/잠금해제 | — | 도면층 객체의 편집 가능 여부를 전환한다. | +| 도면층 | 분리 `LAYISO` | — | 선택 객체의 도면층만 남기고 나머지를 숨기거나 잠근다. | +| 도면층 | 분리 해제 `LAYUNISO` | — | 도면층 분리 이전 상태를 복원한다. | +| 도면층 | 이전 상태 `LAYERP` | — | 직전 도면층 설정 변경을 복원한다. | +| 도면층 | 도면층 상태 | — | 도면층 설정 조합을 이름으로 저장하고 복원한다. | +| 도면층 | 도면층 일치 `LAYMCH` | — | 선택 객체를 대상 객체의 도면층으로 이동한다. | +| 도면층 | 현재 도면층으로 `LAYCUR` | — | 선택 객체를 현재 도면층으로 이동한다. | +| 도면층 | 도면층 병합 `LAYMRG` | — | 한 도면층의 객체를 다른 도면층으로 옮기고 원래 도면층을 제거한다. | +| 도면층 | 도면층 삭제 `LAYDEL` | — | 선택한 도면층과 그 객체를 삭제한다. | +| 도면층 | 도면층 탐색 `LAYWALK` | — | 도면층을 선택적으로 표시하며 포함 객체를 확인한다. | +| 특성 | 색상 | — | 객체 색상을 직접 또는 `ByLayer`·`ByBlock`으로 지정한다. | +| 특성 | 선종류 | — | 실선·점선·중심선 등 객체 선종류를 지정한다. | +| 특성 | 선가중치 | — | 화면 및 출력에 사용할 선 굵기를 지정한다. | +| 특성 | 투명도 | — | 객체 또는 도면층의 투명도를 지정한다. | +| 특성 | 특성 팔레트 `PROPERTIES` | Ctrl+1 / CH / MO / PR | 선택 객체의 형상·표시·데이터 특성을 조회하고 수정한다. | +| 특성 | 빠른 특성 `QUICKPROPERTIES` | QP | 선택 객체 주변에 주요 특성만 간단히 표시한다. | +| 그룹 | 그룹 `GROUP` | G | 여러 객체를 함께 선택할 수 있는 명명 그룹으로 묶는다. | +| 그룹 | 그룹 해제 `UNGROUP` | — | 객체 그룹을 해제한다. | +| 유틸리티 | 거리 `DIST` | DI | 두 점 사이의 거리와 각도 차이를 측정한다. | +| 유틸리티 | 반지름 | — | 원 또는 호의 반지름을 측정한다. | +| 유틸리티 | 각도 | — | 선·호 또는 지정점 사이의 각도를 측정한다. | +| 유틸리티 | 면적 `AREA` | AA | 객체 또는 지정 경계의 면적과 둘레를 계산한다. | +| 유틸리티 | 빠른 측정 `MEASUREGEOM` | MEA | 커서 주변의 치수·거리·각도와 닫힌 영역의 면적을 동적으로 표시한다. | +| 유틸리티 | 점 좌표 `ID` | ID | 지정점의 X·Y·Z 좌표를 표시한다. | +| 유틸리티 | 리스트 `LIST` | LI | 선택 객체의 상세 데이터와 기하 정보를 표시한다. | +| 유틸리티 | 빠른 선택 `QSELECT` | — | 객체 유형과 특성 조건으로 선택 집합을 만든다. | +| 유틸리티 | 계산기 `QUICKCALC` | QC / Ctrl+8 | 수식, 단위 변환 및 도면 값을 계산한다. | +| 유틸리티 | 유사 선택 `SELECTSIMILAR` | — | 선택 객체와 지정 특성이 같은 객체를 모두 선택한다. | +| 유틸리티 | 객체 분리 `ISOLATEOBJECTS` | — | 선택 객체만 표시하고 나머지 객체를 숨긴다. | +| 유틸리티 | 객체 숨기기 `HIDEOBJECTS` | — | 선택 객체를 일시적으로 숨긴다. | +| 유틸리티 | 객체 분리 종료 `UNISOLATEOBJECTS` | UNHIDE / UNISOLATE | 숨기거나 분리한 객체를 다시 표시한다. | +| 클립보드 | 잘라내기·복사·붙여넣기 | Ctrl+X / Ctrl+C / Ctrl+V | 객체를 시스템 클립보드로 이동·복제한다. | +| 클립보드 | 기준점과 함께 복사 | Ctrl+Shift+C | 지정 기준점을 포함해 객체를 클립보드에 복사한다. | +| 클립보드 | 원래 좌표로 붙여넣기 | — | 복사한 객체를 원본 도면 좌표에 배치한다. | +| 클립보드 | 블록으로 붙여넣기 | Ctrl+Shift+V | 클립보드 객체를 새 블록으로 삽입한다. | ## 4. 삽입 탭 — 블록·참조·데이터 -| 그룹(패널) | 기능/명령 | 짧은 설명 | -|---|---|---| -| 블록 | 블록 삽입 `INSERT` | 현재 도면·최근 항목·라이브러리의 블록을 배치한다. | -| 블록 | 스마트 블록 배치 | 블록 팔레트로 삽입할 때 기존 동일 블록을 바탕으로 위치·회전·축척을 제안한다. | -| 블록 | 블록 교체 `BREPLACE` | 선택 블록을 제안 목록 또는 지정한 다른 블록으로 교체한다. | -| 블록 정의 | 블록 작성 `BLOCK` | 선택 객체와 기준점으로 블록 정의를 만든다. | -| 블록 정의 | 블록 편집기 `BEDIT` | 블록 정의와 동적 블록 동작을 편집한다. | -| 블록 정의 | 블록 쓰기 `WBLOCK` | 선택 객체 또는 블록을 별도 DWG로 저장한다. | -| 블록 정의 | 속성 정의 `ATTDEF` | 블록에 포함할 문자 데이터 필드를 정의한다. | -| 블록 정의 | 속성 관리 `BATTMAN` | 블록 속성의 순서·표시·기본값을 관리한다. | -| 블록 정의 | 속성 동기화 `ATTSYNC` | 변경된 속성 정의를 기존 블록 참조에 반영한다. | -| 블록 정의 | 향상된 속성 편집 `EATTEDIT` | 삽입된 블록 참조의 속성 값과 문자·표시 특성을 편집한다. | -| 참조 | DWG 부착 `XATTACH` | 다른 DWG를 외부 참조로 연결한다. | -| 참조 | 이미지 부착 `IMAGEATTACH` | 래스터 이미지를 참조로 연결한다. | -| 참조 | PDF 부착 `PDFATTACH` | PDF 페이지를 언더레이로 연결한다. | -| 참조 | DWF/DGN 부착 | DWF 또는 DGN을 언더레이로 연결한다. | -| 참조 | 점 구름 부착 | 스캔된 점 구름 데이터를 도면에 연결한다. | -| 참조 | 외부 참조 팔레트 `XREF` | 참조의 로드·언로드·재로드·경로·결합을 관리한다. | -| 참조 | 참조 내부 편집 `REFEDIT` | 블록 또는 외부 참조를 현재 도면 안에서 직접 편집한다. | -| 참조 | 외부 참조 자르기 `XCLIP` | DWG 외부 참조 또는 블록의 표시 경계를 지정한다. | -| 참조 | 이미지 자르기 `IMAGECLIP` | 부착 이미지의 표시 경계를 지정한다. | -| 참조 | PDF 자르기 `PDFCLIP` | PDF 언더레이의 표시 경계를 지정한다. | -| 가져오기 | PDF 가져오기 `PDFIMPORT` | PDF의 벡터 형상·문자·채움을 도면 객체로 변환한다. | -| 가져오기 | 가져오기 `IMPORT` | 지원되는 다른 형식의 데이터를 현재 도면으로 가져온다. | -| 링크 및 추출 | 데이터 링크 `DATALINK` | Excel 등 외부 표 데이터와 도면 테이블을 연결한다. | -| 링크 및 추출 | 데이터 추출 `DATAEXTRACTION` | 객체·블록·속성 정보를 표 또는 외부 파일로 추출한다. | -| 링크 및 추출 | 하이퍼링크 `HYPERLINK` | 객체에 웹·파일·도면 뷰 링크를 연결한다. | -| 링크 및 추출 | 필드 `FIELD` | 날짜·도면·객체 특성 등 갱신 가능한 문자 값을 삽입한다. | -| 위치 | 지리적 위치 `GEOGRAPHICLOCATION` | 좌표계와 지도상의 도면 위치를 지정한다. | +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | +|---|---|---|---| +| 블록 | 블록 삽입 `INSERT` | I | 현재 도면·최근 항목·라이브러리의 블록을 배치한다. | +| 블록 | 스마트 블록 배치 | — | 블록 팔레트로 삽입할 때 기존 동일 블록을 바탕으로 위치·회전·축척을 제안한다. | +| 블록 | 블록 교체 `BREPLACE` | — | 선택 블록을 제안 목록 또는 지정한 다른 블록으로 교체한다. | +| 블록 정의 | 블록 작성 `BLOCK` | B | 선택 객체와 기준점으로 블록 정의를 만든다. | +| 블록 정의 | 블록 편집기 `BEDIT` | BE | 블록 정의와 동적 블록 동작을 편집한다. | +| 블록 정의 | 블록 쓰기 `WBLOCK` | W | 선택 객체 또는 블록을 별도 DWG로 저장한다. | +| 블록 정의 | 속성 정의 `ATTDEF` | ATT | 블록에 포함할 문자 데이터 필드를 정의한다. | +| 블록 정의 | 속성 관리 `BATTMAN` | — | 블록 속성의 순서·표시·기본값을 관리한다. | +| 블록 정의 | 속성 동기화 `ATTSYNC` | — | 변경된 속성 정의를 기존 블록 참조에 반영한다. | +| 블록 정의 | 향상된 속성 편집 `EATTEDIT` | — | 삽입된 블록 참조의 속성 값과 문자·표시 특성을 편집한다. | +| 참조 | DWG 부착 `XATTACH` | XA | 다른 DWG를 외부 참조로 연결한다. | +| 참조 | 이미지 부착 `IMAGEATTACH` | IAT | 래스터 이미지를 참조로 연결한다. | +| 참조 | PDF 부착 `PDFATTACH` | — | PDF 페이지를 언더레이로 연결한다. | +| 참조 | DWF/DGN 부착 | — | DWF 또는 DGN을 언더레이로 연결한다. | +| 참조 | 점 구름 부착 | — | 스캔된 점 구름 데이터를 도면에 연결한다. | +| 참조 | 외부 참조 팔레트 `XREF` | XR | 참조의 로드·언로드·재로드·경로·결합을 관리한다. | +| 참조 | 참조 내부 편집 `REFEDIT` | — | 블록 또는 외부 참조를 현재 도면 안에서 직접 편집한다. | +| 참조 | 외부 참조 자르기 `XCLIP` | XC | DWG 외부 참조 또는 블록의 표시 경계를 지정한다. | +| 참조 | 이미지 자르기 `IMAGECLIP` | ICL | 부착 이미지의 표시 경계를 지정한다. | +| 참조 | PDF 자르기 `PDFCLIP` | — | PDF 언더레이의 표시 경계를 지정한다. | +| 가져오기 | PDF 가져오기 `PDFIMPORT` | — | PDF의 벡터 형상·문자·채움을 도면 객체로 변환한다. | +| 가져오기 | 가져오기 `IMPORT` | IMP | 지원되는 다른 형식의 데이터를 현재 도면으로 가져온다. | +| 링크 및 추출 | 데이터 링크 `DATALINK` | DL | Excel 등 외부 표 데이터와 도면 테이블을 연결한다. | +| 링크 및 추출 | 데이터 추출 `DATAEXTRACTION` | DX | 객체·블록·속성 정보를 표 또는 외부 파일로 추출한다. | +| 링크 및 추출 | 하이퍼링크 `HYPERLINK` | — | 객체에 웹·파일·도면 뷰 링크를 연결한다. | +| 링크 및 추출 | 필드 `FIELD` | — | 날짜·도면·객체 특성 등 갱신 가능한 문자 값을 삽입한다. | +| 위치 | 지리적 위치 `GEOGRAPHICLOCATION` | GEO / NORTH | 좌표계와 지도상의 도면 위치를 지정한다. | ## 5. 주석 탭 — 문자·치수·지시선·표·표식 -| 그룹(패널) | 기능/명령 | 짧은 설명 | -|---|---|---| -| 문자 | 여러 줄 문자 `MTEXT` | 서식과 줄바꿈을 지원하는 문단 문자를 작성한다. | -| 문자 | 단일 행 문자 `TEXT` | 각 행이 독립 객체인 간단한 문자를 작성한다. | -| 문자 | 문자 스타일 `STYLE` | 글꼴·높이·폭 비율·기울기 등의 스타일을 관리한다. | -| 문자 | 문자 편집 `TEXTEDIT` | 기존 문자·치수 문자·속성 문자를 수정한다. | -| 문자 | 찾기·대치 `FIND` | 도면의 문자 내용을 검색하고 필요한 문자열을 대치한다. | -| 문자 | 맞춤법 검사 `SPELL` | 도면 문자와 속성 값의 철자를 검사한다. | -| 치수 | 빠른 치수 `QDIM` | 선택 객체에 여러 치수를 한 번에 작성한다. | -| 치수 | 치수 `DIM` | 선택한 객체와 지정점에 적합한 치수 유형을 작성한다. | -| 치수 | 선형 치수 `DIMLINEAR` | 수평·수직·회전 선형 치수를 작성한다. | -| 치수 | 정렬 치수 `DIMALIGNED` | 두 점과 평행한 실제 길이 치수를 작성한다. | -| 치수 | 각도 치수 `DIMANGULAR` | 두 선 또는 호의 각도를 기입한다. | -| 치수 | 호 길이 치수 `DIMARC` | 호의 곡선 길이를 기입한다. | -| 치수 | 반지름·지름 치수 | 원과 호의 반지름 또는 지름을 기입한다. | -| 치수 | 꺾기 치수 | 큰 반지름의 중심 위치를 축약해 표시한다. | -| 치수 | 세로좌표 치수 | 기준 원점에 대한 X 또는 Y 좌표를 기입한다. | -| 치수 | 기준선·연속 치수 | 공통 기준 또는 앞 치수 끝점에서 연속 기입한다. | -| 치수 | 중심 표식 `CENTERMARK` | 원 또는 호 중심에 연관 중심 표식을 작성한다. | -| 치수 | 중심선 `CENTERLINE` | 두 선 또는 폴리선 세그먼트 사이에 연관 중심선을 작성한다. | -| 치수 | 치수 스타일 `DIMSTYLE` | 치수선·화살표·문자·단위·공차 형식을 관리한다. | -| 치수 | 치수 업데이트 | 변경한 치수 스타일을 기존 치수에 적용한다. | -| 치수 | 치수 끊기 `DIMBREAK` | 교차 객체나 지정점에서 치수선·연장선을 끊어 표시한다. | -| 치수 | 치수 간격 `DIMSPACE` | 평행 또는 동심 치수 사이의 간격을 조정한다. | -| 치수 | 치수 재연관 `DIMREASSOCIATE` | 끊어진 치수를 실제 형상점과 다시 연관시킨다. | -| 치수 | 검사 치수 `DIMINSPECT` | 검사 빈도·값 정보를 포함한 검사 치수를 작성한다. | -| 기호 | 기하공차 `TOLERANCE` | 형상·자세·위치 공차 기호 프레임을 작성한다. | -| 지시선 | 다중 지시선 `MLEADER` | 화살표와 문자 또는 블록 주석을 연결한다. | -| 지시선 | 다중 지시선 스타일 `MLEADERSTYLE` | 다중 지시선의 형식과 기본 동작을 작성·수정한다. | -| 지시선 | 지시선 추가·제거·정렬 | 기존 다중 지시선의 가지와 배치를 편집한다. | -| 지시선 | 지시선 수집 `MLEADERCOLLECT` | 블록 내용의 여러 다중 지시선을 행 또는 열로 정렬한다. | -| 표 | 테이블 `TABLE` | 행·열과 셀 서식을 가진 도면 표를 작성한다. | -| 표 | 테이블 스타일 `TABLESTYLE` | 표의 제목·머리글·데이터 셀 형식을 관리한다. | -| 표식 | 구름형 리비전 `REVCLOUD` | 변경 또는 검토 범위를 구름형 선으로 표시한다. | -| 주석 축척 | 주석 객체·축척 | 여러 뷰포트 축척에서 일정한 출력 크기를 유지한다. | -| 주석 축척 | 축척 리스트 편집 `SCALELISTEDIT` | 도면에서 사용할 주석·뷰포트 축척 목록을 관리한다. | +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | +|---|---|---|---| +| 문자 | 여러 줄 문자 `MTEXT` | T / MT | 서식과 줄바꿈을 지원하는 문단 문자를 작성한다. | +| 문자 | 단일 행 문자 `TEXT` | DT | 각 행이 독립 객체인 간단한 문자를 작성한다. | +| 문자 | 문자 스타일 `STYLE` | ST | 글꼴·높이·폭 비율·기울기 등의 스타일을 관리한다. | +| 문자 | 문자 편집 `TEXTEDIT` | TEDIT | 기존 문자·치수 문자·속성 문자를 수정한다. | +| 문자 | 찾기·대치 `FIND` | — | 도면의 문자 내용을 검색하고 필요한 문자열을 대치한다. | +| 문자 | 맞춤법 검사 `SPELL` | SP | 도면 문자와 속성 값의 철자를 검사한다. | +| 치수 | 빠른 치수 `QDIM` | — | 선택 객체에 여러 치수를 한 번에 작성한다. | +| 치수 | 치수 `DIM` | — | 선택한 객체와 지정점에 적합한 치수 유형을 작성한다. | +| 치수 | 선형 치수 `DIMLINEAR` | — | 수평·수직·회전 선형 치수를 작성한다. | +| 치수 | 정렬 치수 `DIMALIGNED` | — | 두 점과 평행한 실제 길이 치수를 작성한다. | +| 치수 | 각도 치수 `DIMANGULAR` | DAN | 두 선 또는 호의 각도를 기입한다. | +| 치수 | 호 길이 치수 `DIMARC` | DAR | 호의 곡선 길이를 기입한다. | +| 치수 | 반지름·지름 치수 | — | 원과 호의 반지름 또는 지름을 기입한다. | +| 치수 | 꺾기 치수 | — | 큰 반지름의 중심 위치를 축약해 표시한다. | +| 치수 | 세로좌표 치수 | — | 기준 원점에 대한 X 또는 Y 좌표를 기입한다. | +| 치수 | 기준선·연속 치수 | — | 공통 기준 또는 앞 치수 끝점에서 연속 기입한다. | +| 치수 | 중심 표식 `CENTERMARK` | — | 원 또는 호 중심에 연관 중심 표식을 작성한다. | +| 치수 | 중심선 `CENTERLINE` | — | 두 선 또는 폴리선 세그먼트 사이에 연관 중심선을 작성한다. | +| 치수 | 치수 스타일 `DIMSTYLE` | D | 치수선·화살표·문자·단위·공차 형식을 관리한다. | +| 치수 | 치수 업데이트 | — | 변경한 치수 스타일을 기존 치수에 적용한다. | +| 치수 | 치수 끊기 `DIMBREAK` | — | 교차 객체나 지정점에서 치수선·연장선을 끊어 표시한다. | +| 치수 | 치수 간격 `DIMSPACE` | — | 평행 또는 동심 치수 사이의 간격을 조정한다. | +| 치수 | 치수 재연관 `DIMREASSOCIATE` | DRE | 끊어진 치수를 실제 형상점과 다시 연관시킨다. | +| 치수 | 검사 치수 `DIMINSPECT` | — | 검사 빈도·값 정보를 포함한 검사 치수를 작성한다. | +| 기호 | 기하공차 `TOLERANCE` | TOL | 형상·자세·위치 공차 기호 프레임을 작성한다. | +| 지시선 | 다중 지시선 `MLEADER` | MLD | 화살표와 문자 또는 블록 주석을 연결한다. | +| 지시선 | 다중 지시선 스타일 `MLEADERSTYLE` | MLS | 다중 지시선의 형식과 기본 동작을 작성·수정한다. | +| 지시선 | 지시선 추가·제거·정렬 | — | 기존 다중 지시선의 가지와 배치를 편집한다. | +| 지시선 | 지시선 수집 `MLEADERCOLLECT` | MLC | 블록 내용의 여러 다중 지시선을 행 또는 열로 정렬한다. | +| 표 | 테이블 `TABLE` | TB | 행·열과 셀 서식을 가진 도면 표를 작성한다. | +| 표 | 테이블 스타일 `TABLESTYLE` | TS | 표의 제목·머리글·데이터 셀 형식을 관리한다. | +| 표식 | 구름형 리비전 `REVCLOUD` | — | 변경 또는 검토 범위를 구름형 선으로 표시한다. | +| 주석 축척 | 주석 객체·축척 | — | 여러 뷰포트 축척에서 일정한 출력 크기를 유지한다. | +| 주석 축척 | 축척 리스트 편집 `SCALELISTEDIT` | — | 도면에서 사용할 주석·뷰포트 축척 목록을 관리한다. | ## 6. 파라메트릭 탭 — 구속조건 -| 그룹(패널) | 기능/명령 | 짧은 설명 | -|---|---|---| -| 기하 구속 | 자동 구속 `AUTOCONSTRAIN` | 객체 관계를 분석해 여러 기하 구속을 자동 적용한다. | -| 기하 구속 | 일치 | 두 점 또는 점과 객체를 같은 위치에 구속한다. | -| 기하 구속 | 동일선상 | 객체들이 같은 무한선 위에 있도록 구속한다. | -| 기하 구속 | 동심 | 원·호·타원이 같은 중심을 갖도록 구속한다. | -| 기하 구속 | 고정 | 점 또는 객체의 위치·방향을 고정한다. | -| 기하 구속 | 평행·직교 | 두 선을 평행 또는 직각으로 유지한다. | -| 기하 구속 | 수평·수직 | 선 또는 두 점을 수평·수직으로 유지한다. | -| 기하 구속 | 접선 | 두 곡선이 서로 접하도록 구속한다. | -| 기하 구속 | 매끄럽게 | 스플라인과 다른 곡선을 부드럽게 연결한다. | -| 기하 구속 | 대칭 | 객체를 지정 대칭축에 대해 대칭으로 유지한다. | -| 기하 구속 | 같음 | 객체의 길이 또는 반지름을 같게 유지한다. | -| 치수 구속 | 선형·정렬·수평·수직 | 거리 값을 매개변수로 지정해 형상을 제어한다. | -| 치수 구속 | 반지름·지름·각도 | 크기와 각도를 매개변수로 지정한다. | -| 치수 구속 | 변환 | 일반 치수를 연관 치수 구속으로 변환한다. | -| 관리 | 매개변수 관리자 | 이름 있는 변수, 값 및 수식을 관리한다. | -| 관리 | 구속 표시·숨기기·삭제 | 구속 막대와 치수 구속의 표시 및 제거를 관리한다. | +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | +|---|---|---|---| +| 기하 구속 | 자동 구속 `AUTOCONSTRAIN` | — | 객체 관계를 분석해 여러 기하 구속을 자동 적용한다. | +| 기하 구속 | 일치 | — | 두 점 또는 점과 객체를 같은 위치에 구속한다. | +| 기하 구속 | 동일선상 | — | 객체들이 같은 무한선 위에 있도록 구속한다. | +| 기하 구속 | 동심 | — | 원·호·타원이 같은 중심을 갖도록 구속한다. | +| 기하 구속 | 고정 | — | 점 또는 객체의 위치·방향을 고정한다. | +| 기하 구속 | 평행·직교 | — | 두 선을 평행 또는 직각으로 유지한다. | +| 기하 구속 | 수평·수직 | — | 선 또는 두 점을 수평·수직으로 유지한다. | +| 기하 구속 | 접선 | — | 두 곡선이 서로 접하도록 구속한다. | +| 기하 구속 | 매끄럽게 | — | 스플라인과 다른 곡선을 부드럽게 연결한다. | +| 기하 구속 | 대칭 | — | 객체를 지정 대칭축에 대해 대칭으로 유지한다. | +| 기하 구속 | 같음 | — | 객체의 길이 또는 반지름을 같게 유지한다. | +| 치수 구속 | 선형·정렬·수평·수직 | — | 거리 값을 매개변수로 지정해 형상을 제어한다. | +| 치수 구속 | 반지름·지름·각도 | — | 크기와 각도를 매개변수로 지정한다. | +| 치수 구속 | 변환 | — | 일반 치수를 연관 치수 구속으로 변환한다. | +| 관리 | 매개변수 관리자 | — | 이름 있는 변수, 값 및 수식을 관리한다. | +| 관리 | 구속 표시·숨기기·삭제 | — | 구속 막대와 치수 구속의 표시 및 제거를 관리한다. | ## 7. 뷰 탭 — 탐색·뷰·뷰포트·팔레트 -| 그룹(패널) | 기능/명령 | 짧은 설명 | -|---|---|---| -| 탐색 | 줌 `ZOOM` | 확대·축소, 범위 전체, 윈도우 등의 방법으로 뷰를 변경한다. | -| 탐색 | 초점이동 `PAN` | 배율을 유지한 채 화면의 관찰 위치를 이동한다. | -| 탐색 | 탐색 막대 | 줌·초점이동 등 화면 탐색 도구를 모아 제공한다. | -| 뷰 | 명명된 뷰 `VIEW` | 현재 화면·도면층·UCS 상태를 이름으로 저장하고 복원한다. | -| 뷰 | 새 명명 뷰 `NEWVIEW` | 현재 화면 또는 지정 영역을 새 이름의 뷰로 저장한다. | -| 뷰 | 재생성 `REGEN` | 현재 뷰포트의 객체 표시를 다시 계산한다. | -| 뷰 | 전체 재생성 `REGENALL` | 모든 뷰포트의 객체 표시를 다시 계산한다. | -| 뷰포트 | 모형 공간 뷰포트 | 도면 영역을 여러 독립 관찰 창으로 나눈다. | -| 뷰포트 | 배치 뷰포트 `MVIEW` | 종이 공간에 모형을 표시하는 출력 창을 작성한다. | -| 뷰포트 | 뷰포트 잠금 | 배치 뷰포트의 축척과 관찰 상태 변경을 막는다. | -| 좌표 | UCS `UCS` | 사용자 좌표계의 원점과 축 방향을 설정한다. | -| 좌표 | 월드 UCS | 좌표계를 기본 WCS로 복원한다. | -| 팔레트 | 도구 팔레트 | 자주 쓰는 블록·해치·명령 도구 모음을 표시한다. | -| 팔레트 | 개수 `COUNT` | 선택 객체·블록의 인스턴스를 세고 결과를 강조·검토한다. | -| 팔레트 | DesignCenter | 다른 도면의 블록·도면층·스타일을 탐색해 가져온다. | -| 팔레트 | 시트 세트 관리자 | 여러 도면 시트의 집합·번호·출력을 관리한다. | -| 팔레트 | 명령행 | 명령 입력, 옵션 선택 및 작업 기록을 표시한다. | -| 팔레트 | 명령 매크로 | 반복 명령 시퀀스를 검토·저장·실행한다. | -| 인터페이스 | 파일 탭 | 열려 있는 여러 도면 사이를 전환한다. | -| 인터페이스 | 타일·계단식 배열 | 여러 열린 도면 창의 배치를 조정한다. | -| 인터페이스 | 전체 화면 정리 `CLEANSCREENON` | 도구막대와 팔레트를 숨겨 도면 영역을 최대화한다. | +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | +|---|---|---|---| +| 탐색 | 줌 `ZOOM` | Z | 확대·축소, 범위 전체, 윈도우 등의 방법으로 뷰를 변경한다. | +| 탐색 | 초점이동 `PAN` | P | 배율을 유지한 채 화면의 관찰 위치를 이동한다. | +| 탐색 | 탐색 막대 | — | 줌·초점이동 등 화면 탐색 도구를 모아 제공한다. | +| 뷰 | 명명된 뷰 `VIEW` | V | 현재 화면·도면층·UCS 상태를 이름으로 저장하고 복원한다. | +| 뷰 | 새 명명 뷰 `NEWVIEW` | NVIEW | 현재 화면 또는 지정 영역을 새 이름의 뷰로 저장한다. | +| 뷰 | 재생성 `REGEN` | RE | 현재 뷰포트의 객체 표시를 다시 계산한다. | +| 뷰 | 전체 재생성 `REGENALL` | REA | 모든 뷰포트의 객체 표시를 다시 계산한다. | +| 뷰포트 | 모형 공간 뷰포트 | — | 도면 영역을 여러 독립 관찰 창으로 나눈다. | +| 뷰포트 | 배치 뷰포트 `MVIEW` | MV | 종이 공간에 모형을 표시하는 출력 창을 작성한다. | +| 뷰포트 | 뷰포트 잠금 | — | 배치 뷰포트의 축척과 관찰 상태 변경을 막는다. | +| 좌표 | UCS `UCS` | — | 사용자 좌표계의 원점과 축 방향을 설정한다. | +| 좌표 | 월드 UCS | — | 좌표계를 기본 WCS로 복원한다. | +| 팔레트 | 도구 팔레트 | Ctrl+3 | 자주 쓰는 블록·해치·명령 도구 모음을 표시한다. | +| 팔레트 | 개수 `COUNT` | — | 선택 객체·블록의 인스턴스를 세고 결과를 강조·검토한다. | +| 팔레트 | DesignCenter | Ctrl+2 | 다른 도면의 블록·도면층·스타일을 탐색해 가져온다. | +| 팔레트 | 시트 세트 관리자 | Ctrl+4 / SSM | 여러 도면 시트의 집합·번호·출력을 관리한다. | +| 팔레트 | 명령행 | Ctrl+9 / CLI | 명령 입력, 옵션 선택 및 작업 기록을 표시한다. | +| 팔레트 | 명령 매크로 | — | 반복 명령 시퀀스를 검토·저장·실행한다. | +| 인터페이스 | 파일 탭 | Ctrl+Tab | 열려 있는 여러 도면 사이를 전환한다. | +| 인터페이스 | 타일·계단식 배열 | — | 여러 열린 도면 창의 배치를 조정한다. | +| 인터페이스 | 전체 화면 정리 `CLEANSCREENON` | Ctrl+0 | 도구막대와 팔레트를 숨겨 도면 영역을 최대화한다. | ## 8. 관리 탭 — 표준·사용자화·자동화 -| 그룹(패널) | 기능/명령 | 짧은 설명 | -|---|---|---| -| 작업 기록 | 동작 기록기 | 사용자 명령과 입력을 기록해 반복 재생한다. | -| 사용자화 | 사용자 인터페이스 `CUI` | 리본·도구막대·메뉴·단축키·마우스 동작을 사용자화한다. | -| 사용자화 | 별칭 편집 | 명령행에서 쓰는 짧은 명령 별칭을 관리한다. | -| 사용자화 | 작업공간 | 리본·팔레트·도구막대 배치를 저장하고 전환한다. | -| CAD 표준 | 표준 구성 `STANDARDS` | 도면층·문자·치수·선종류 표준 파일을 연결한다. | -| CAD 표준 | 표준 검사 `CHECKSTANDARDS` | 현재 도면이 연결된 CAD 표준과 다른 항목을 찾는다. | -| CAD 표준 | 도면층 변환 `LAYTRANS` | 현재 도면층을 지정 표준 도면층 이름과 특성으로 매핑한다. | -| 응용프로그램 | 응용프로그램 로드 `APPLOAD` | AutoLISP·ObjectARX 등 확장 프로그램을 로드한다. | -| 응용프로그램 | Visual LISP | AutoLISP 코드를 작성·검사하는 개발 환경을 연다. | -| 스크립트 | 스크립트 실행 `SCRIPT` | 텍스트 파일에 기록된 명령 시퀀스를 실행한다. | -| 정리 | 소거 `PURGE` | 사용하지 않는 블록·도면층·스타일 정의를 제거한다. | +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | +|---|---|---|---| +| 작업 기록 | 동작 기록기 | — | 사용자 명령과 입력을 기록해 반복 재생한다. | +| 사용자화 | 사용자 인터페이스 `CUI` | — | 리본·도구막대·메뉴·단축키·마우스 동작을 사용자화한다. | +| 사용자화 | 별칭 편집 | — | 명령행에서 쓰는 짧은 명령 별칭을 관리한다. | +| 사용자화 | 작업공간 | — | 리본·팔레트·도구막대 배치를 저장하고 전환한다. | +| CAD 표준 | 표준 구성 `STANDARDS` | STA | 도면층·문자·치수·선종류 표준 파일을 연결한다. | +| CAD 표준 | 표준 검사 `CHECKSTANDARDS` | CHK | 현재 도면이 연결된 CAD 표준과 다른 항목을 찾는다. | +| CAD 표준 | 도면층 변환 `LAYTRANS` | — | 현재 도면층을 지정 표준 도면층 이름과 특성으로 매핑한다. | +| 응용프로그램 | 응용프로그램 로드 `APPLOAD` | AP | AutoLISP·ObjectARX 등 확장 프로그램을 로드한다. | +| 응용프로그램 | Visual LISP | — | AutoLISP 코드를 작성·검사하는 개발 환경을 연다. | +| 스크립트 | 스크립트 실행 `SCRIPT` | SCR | 텍스트 파일에 기록된 명령 시퀀스를 실행한다. | +| 정리 | 소거 `PURGE` | PU | 사용하지 않는 블록·도면층·스타일 정의를 제거한다. | ## 9. 출력 탭 — 배치·플롯·게시·내보내기 -| 그룹(패널) | 기능/명령 | 짧은 설명 | -|---|---|---| -| 플롯 | 플롯 `PLOT` | 프린터·용지·영역·축척·스타일을 설정해 출력한다. | -| 플롯 | 미리보기 `PREVIEW` | 실제 출력 전 용지 결과를 확인한다. | -| 플롯 | 페이지 설정 `PAGESETUP` | 배치별 출력 장치와 용지 설정을 저장한다. | -| 플롯 | 플롯 스타일 | 색상·선가중치 등의 CTB/STB 출력 규칙을 관리한다. | -| 플롯 | 플로터 관리자 `PLOTTERMANAGER` | 플로터 구성 파일과 장치 설정을 관리한다. | -| 플롯 | 플롯 스타일 관리자 `STYLESMANAGER` | CTB·STB 플롯 스타일 테이블 파일을 관리한다. | -| 게시 | 게시 `PUBLISH` | 여러 도면·배치를 한 번에 DWF·PDF·프린터로 출력한다. | -| 게시 | 배치 플롯 | 시트 목록을 구성해 일괄 출력한다. | -| 내보내기 | PDF 내보내기 `EXPORTPDF` | 도면 또는 배치를 PDF 파일로 만든다. | -| 내보내기 | DWF/DWFx 내보내기 | 검토·배포용 Autodesk 형식으로 내보낸다. | -| 내보내기 | 기타 형식 `EXPORT` | 지원되는 다른 교환 파일 형식으로 저장한다. | -| 전송 | 전자 전송 `ETRANSMIT` | 도면과 참조·글꼴·플롯 설정을 하나의 전달 패키지로 묶는다. | +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | +|---|---|---|---| +| 플롯 | 플롯 `PLOT` | PRINT / Ctrl+P | 프린터·용지·영역·축척·스타일을 설정해 출력한다. | +| 플롯 | 미리보기 `PREVIEW` | PRE | 실제 출력 전 용지 결과를 확인한다. | +| 플롯 | 페이지 설정 `PAGESETUP` | — | 배치별 출력 장치와 용지 설정을 저장한다. | +| 플롯 | 플롯 스타일 | — | 색상·선가중치 등의 CTB/STB 출력 규칙을 관리한다. | +| 플롯 | 플로터 관리자 `PLOTTERMANAGER` | — | 플로터 구성 파일과 장치 설정을 관리한다. | +| 플롯 | 플롯 스타일 관리자 `STYLESMANAGER` | — | CTB·STB 플롯 스타일 테이블 파일을 관리한다. | +| 게시 | 게시 `PUBLISH` | — | 여러 도면·배치를 한 번에 DWF·PDF·프린터로 출력한다. | +| 게시 | 배치 플롯 | — | 시트 목록을 구성해 일괄 출력한다. | +| 내보내기 | PDF 내보내기 `EXPORTPDF` | EPDF | 도면 또는 배치를 PDF 파일로 만든다. | +| 내보내기 | DWF/DWFx 내보내기 | — | 검토·배포용 Autodesk 형식으로 내보낸다. | +| 내보내기 | 기타 형식 `EXPORT` | EXP | 지원되는 다른 교환 파일 형식으로 저장한다. | +| 전송 | 전자 전송 `ETRANSMIT` | ZIP | 도면과 참조·글꼴·플롯 설정을 하나의 전달 패키지로 묶는다. | ## 10. 공동작업·검토 -| 그룹(패널) | 기능/명령 | 짧은 설명 | -|---|---|---| -| 비교 | DWG 비교 `COMPARE` | 두 DWG의 추가·삭제·변경 객체를 색으로 구분한다. | -| 비교 | 외부 참조 비교 | 부착된 외부 참조의 변경 사항을 현재 도면과 비교한다. | -| 검토 | 추적 `TRACE` | 원도면을 바꾸지 않는 검토용 투명 오버레이를 만든다. | -| 검토 | 추적에서 복사 `COPYFROMTRACE` | 추적에 있는 객체를 원도면의 같은 위치로 복사한다. | -| 검토 | 표식 가져오기 `MARKUPIMPORT` | PDF·JPG·PNG 표식을 새 추적으로 가져와 원도면 위에 겹친다. | -| 검토 | 표식 도우미 `MARKUPASSIST` | 표식의 문자·구름·지시 등을 인식해 도면 반영을 돕는다. | -| 공유 | 뷰 공유 `SHAREDVIEWS` | 도면의 온라인 검토용 뷰를 생성하고 의견을 교환한다. | -| 공유 | 도면 공유 `SHARE` | 현재 도면 복사본을 웹·모바일에서 보거나 편집하는 링크를 만든다. | -| 이력 | 활동 정보 `ACTIVITYINSIGHTSOPEN` | 도면 열기·저장·편집 등 작업 이벤트를 추적한다. | -| 버전 | 버전 이력 `DWGHISTORY` | 지원되는 클라우드에 저장된 도면의 이전 버전을 조회·비교한다. | +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | +|---|---|---|---| +| 비교 | DWG 비교 `COMPARE` | — | 두 DWG의 추가·삭제·변경 객체를 색으로 구분한다. | +| 비교 | 외부 참조 비교 | — | 부착된 외부 참조의 변경 사항을 현재 도면과 비교한다. | +| 검토 | 추적 `TRACE` | — | 원도면을 바꾸지 않는 검토용 투명 오버레이를 만든다. | +| 검토 | 추적에서 복사 `COPYFROMTRACE` | — | 추적에 있는 객체를 원도면의 같은 위치로 복사한다. | +| 검토 | 표식 가져오기 `MARKUPIMPORT` | — | PDF·JPG·PNG 표식을 새 추적으로 가져와 원도면 위에 겹친다. | +| 검토 | 표식 도우미 `MARKUPASSIST` | — | 표식의 문자·구름·지시 등을 인식해 도면 반영을 돕는다. | +| 공유 | 뷰 공유 `SHAREDVIEWS` | — | 도면의 온라인 검토용 뷰를 생성하고 의견을 교환한다. | +| 공유 | 도면 공유 `SHARE` | — | 현재 도면 복사본을 웹·모바일에서 보거나 편집하는 링크를 만든다. | +| 이력 | 활동 정보 `ACTIVITYINSIGHTSOPEN` | AI OPEN | 도면 열기·저장·편집 등 작업 이벤트를 추적한다. | +| 버전 | 버전 이력 `DWGHISTORY` | — | 지원되는 클라우드에 저장된 도면의 이전 버전을 조회·비교한다. | ## 11. 리본 밖의 핵심 제도 기능 -| 영역 | 기능 | 짧은 설명 | -|---|---|---| -| 선택 | 창 선택 | 왼쪽→오른쪽 사각형 안에 완전히 포함된 객체를 선택한다. | -| 선택 | 교차 선택 | 오른쪽→왼쪽 영역에 포함되거나 걸치는 객체를 선택한다. | -| 선택 | 올가미 선택 | 자유형 경계로 객체를 포함 또는 교차 선택한다. | -| 선택 | 선택 순환 | 겹친 객체 목록에서 원하는 객체를 골라 선택한다. | -| 직접 편집 | 그립 편집 | 선택 객체의 정점·중간점·반지름 등을 직접 끌어 수정한다. | -| 직접 편집 | 다기능 그립 | 폴리선 정점 추가·제거, 호 전환 등 상황별 작업을 제공한다. | -| 정확도 | 객체 스냅 `OSNAP` | 끝점·중간점·중심·교차점·접점 등 정확한 점을 포착한다. | -| 정확도 | 객체 스냅 추적 `(F11)` | 포착한 점에서 임시 정렬 경로를 추적한다. | -| 정확도 | 극좌표 추적 `(F10)` | 지정 각도 증분을 따라 커서 이동을 안내한다. | -| 정확도 | 직교 모드 `(F8)` | 커서 이동을 현재 UCS의 수평·수직 방향으로 제한한다. | -| 정확도 | 그리드·스냅 | 화면 격자를 표시하고 커서 이동 간격을 제한한다. | -| 정확도 | 동적 입력 `(F12, DYNMODE 시스템 변수)` | 커서 근처에서 좌표·거리·각도 및 명령 옵션을 입력한다. | -| 정확도 | 직접 거리 입력 | 방향을 지정한 뒤 키보드로 정확한 거리를 입력한다. | -| 좌표 | 절대·상대·극좌표 | 전역 좌표, 이전 점 기준 좌표, 거리·각도로 점을 입력한다. | -| 작업 흐름 | 명령 직접 입력 | 명령 이름, 별칭, 옵션, 숫자와 좌표를 명령행에 연속 입력한다. | -| 작업 흐름 | 명령 자동완성 | 입력 중인 명령·시스템 변수·콘텐츠를 검색해 제안한다. | -| 작업 흐름 | 실행 취소·다시 실행 | 명령 단위로 작업 이력을 되돌리거나 복원한다. | -| 작업 흐름 | 반복·최근 명령 | 직전 또는 최근 사용 명령을 다시 실행한다. | -| 표시 | 선가중치 표시 | 실제 출력 선 굵기의 화면 표시를 전환한다. | -| 표시 | 투명도 표시 | 객체·도면층 투명도의 화면 표시를 전환한다. | -| 표시 | 주석 가시성 | 현재 축척을 지원하지 않는 주석의 표시 여부를 전환한다. | -| 공간 | 모형·배치 | 실제 크기 설계 공간과 용지 출력 공간을 전환한다. | -| 공간 | 뷰포트 축척 | 배치에서 모형 표시 축척을 정밀하게 지정한다. | -| 파일 | 새 도면·열기·저장·다른 이름 저장 | DWG 작업 파일의 기본 수명주기를 관리한다. | -| 파일 | 자동 저장·복구 | 비정상 종료 시 사용할 임시 저장과 복구 정보를 관리한다. | -| 파일 | 감사 `AUDIT` | 응용프로그램 메뉴에서 열린 도면의 오류를 검사·수정한다. | -| 파일 | 복구 `RECOVER` | 응용프로그램 메뉴에서 손상된 도면을 복구하면서 연다. | -| 호환 | DWG 변환 `DWGCONVERT` | 여러 도면을 지정 DWG 버전으로 일괄 변환한다. | -| 호환 | 단위 `UNITS` | 길이·각도 형식, 정밀도 및 삽입 축척 단위를 설정한다. | +| 영역 | 기능 | 단축키 | 짧은 설명 | +|---|---|---|---| +| 선택 | 창 선택 | — | 왼쪽→오른쪽 사각형 안에 완전히 포함된 객체를 선택한다. | +| 선택 | 교차 선택 | — | 오른쪽→왼쪽 영역에 포함되거나 걸치는 객체를 선택한다. | +| 선택 | 올가미 선택 | — | 자유형 경계로 객체를 포함 또는 교차 선택한다. | +| 선택 | 선택 순환 | — | 겹친 객체 목록에서 원하는 객체를 골라 선택한다. | +| 직접 편집 | 그립 편집 | — | 선택 객체의 정점·중간점·반지름 등을 직접 끌어 수정한다. | +| 직접 편집 | 다기능 그립 | — | 폴리선 정점 추가·제거, 호 전환 등 상황별 작업을 제공한다. | +| 정확도 | 객체 스냅 `OSNAP` | OS / F3 | 끝점·중간점·중심·교차점·접점 등 정확한 점을 포착한다. | +| 정확도 | 객체 스냅 추적 `(F11)` | F11 | 포착한 점에서 임시 정렬 경로를 추적한다. | +| 정확도 | 극좌표 추적 `(F10)` | F10 | 지정 각도 증분을 따라 커서 이동을 안내한다. | +| 정확도 | 직교 모드 `(F8)` | F8 | 커서 이동을 현재 UCS의 수평·수직 방향으로 제한한다. | +| 정확도 | 그리드·스냅 | F7 / F9 | 화면 격자를 표시하고 커서 이동 간격을 제한한다. | +| 정확도 | 동적 입력 `(F12, DYNMODE 시스템 변수)` | F12 | 커서 근처에서 좌표·거리·각도 및 명령 옵션을 입력한다. | +| 정확도 | 직접 거리 입력 | — | 방향을 지정한 뒤 키보드로 정확한 거리를 입력한다. | +| 좌표 | 절대·상대·극좌표 | — | 전역 좌표, 이전 점 기준 좌표, 거리·각도로 점을 입력한다. | +| 작업 흐름 | 명령 직접 입력 | — | 명령 이름, 별칭, 옵션, 숫자와 좌표를 명령행에 연속 입력한다. | +| 작업 흐름 | 명령 자동완성 | — | 입력 중인 명령·시스템 변수·콘텐츠를 검색해 제안한다. | +| 작업 흐름 | 실행 취소·다시 실행 | Ctrl+Z / Ctrl+Y | 명령 단위로 작업 이력을 되돌리거나 복원한다. | +| 작업 흐름 | 반복·최근 명령 | — | 직전 또는 최근 사용 명령을 다시 실행한다. | +| 표시 | 선가중치 표시 | — | 실제 출력 선 굵기의 화면 표시를 전환한다. | +| 표시 | 투명도 표시 | — | 객체·도면층 투명도의 화면 표시를 전환한다. | +| 표시 | 주석 가시성 | — | 현재 축척을 지원하지 않는 주석의 표시 여부를 전환한다. | +| 공간 | 모형·배치 | — | 실제 크기 설계 공간과 용지 출력 공간을 전환한다. | +| 공간 | 뷰포트 축척 | — | 배치에서 모형 표시 축척을 정밀하게 지정한다. | +| 파일 | 새 도면·열기·저장·다른 이름 저장 | Ctrl+N / Ctrl+O / Ctrl+S / Ctrl+Shift+S | DWG 작업 파일의 기본 수명주기를 관리한다. | +| 파일 | 자동 저장·복구 | — | 비정상 종료 시 사용할 임시 저장과 복구 정보를 관리한다. | +| 파일 | 감사 `AUDIT` | — | 응용프로그램 메뉴에서 열린 도면의 오류를 검사·수정한다. | +| 파일 | 복구 `RECOVER` | — | 응용프로그램 메뉴에서 손상된 도면을 복구하면서 연다. | +| 호환 | DWG 변환 `DWGCONVERT` | — | 여러 도면을 지정 DWG 버전으로 일괄 변환한다. | +| 호환 | 단위 `UNITS` | UN | 길이·각도 형식, 정밀도 및 삽입 축척 단위를 설정한다. | ## 12. 2D 상황별 리본 탭 > 해당 객체를 선택하거나 편집 명령을 실행할 때만 나타나는 리본이다. -| 상황별 탭 | 기능 | 짧은 설명 | -|---|---|---| -| 해치 작성/편집 | 원점 설정 | 해치 패턴이 시작되는 기준점을 지정한다. | -| 해치 작성/편집 | 연관·주석 | 경계 연동과 주석 축척 적용 여부를 설정한다. | -| 해치 작성/편집 | 경계 재작성 | 해치에서 폴리선 또는 영역 경계를 다시 만든다. | -| 문자 편집기 | 굵게·기울임·밑줄·색상 | 선택 문자 범위의 서식을 변경한다. | -| 문자 편집기 | 정렬·들여쓰기·줄 간격 | 문단 배치와 간격을 조정한다. | -| 문자 편집기 | 열 | 여러 줄 문자를 동적·정적 열로 나눈다. | -| 블록 편집기 | 블록 테스트 | 동적 블록의 그립과 동작을 시험한다. | -| 블록 편집기 | 매개변수·동작 | 동적 블록에 점·선형·회전 등의 매개변수와 동작을 지정한다. | -| 블록 편집기 | 가시성 상태 | 하나의 블록에서 표시할 객체 조합을 관리한다. | -| 배열 편집 | 항목 수·간격 | 배열 항목의 개수와 행·열 또는 경로 간격을 수정한다. | -| 배열 편집 | 항목 교체·편집 | 배열의 원본 항목을 편집하거나 다른 객체로 바꾼다. | -| 배열 편집 | 원본 편집·재설정 | 원본 객체를 편집하고 배열 재지정을 초기화한다. | -| 외부 참조·언더레이 | 페이드·대비 | 외부 참조와 PDF·DWF·DGN 언더레이의 화면 특성을 조정한다. | -| 외부 참조 | 참조 도면층 | 참조에 포함된 도면층 표시를 관리한다. | -| 이미지 | 페이드·대비·밝기 | 래스터 이미지의 화면 특성을 조정한다. | -| PDF/DWF/DGN 언더레이 | 단색·언더레이 도면층 | 단색 표시와 원본 도면층 가시성을 관리한다. | -| 배치 | 명명된 뷰 삽입 | 저장된 모형 뷰를 배치 뷰포트로 배치한다. | -| 테이블 셀 | 행·열 삽입·삭제 | 선택 셀 주변의 표 구조를 편집한다. | -| 테이블 셀 | 병합·병합 해제 | 여러 셀을 합치거나 원래 셀로 되돌린다. | -| 테이블 셀 | 셀 형식·정렬·테두리 | 데이터 형식과 표시 서식을 조정한다. | +| 상황별 탭 | 기능 | 단축키 | 짧은 설명 | +|---|---|---|---| +| 해치 작성/편집 | 원점 설정 | — | 해치 패턴이 시작되는 기준점을 지정한다. | +| 해치 작성/편집 | 연관·주석 | — | 경계 연동과 주석 축척 적용 여부를 설정한다. | +| 해치 작성/편집 | 경계 재작성 | — | 해치에서 폴리선 또는 영역 경계를 다시 만든다. | +| 문자 편집기 | 굵게·기울임·밑줄·색상 | — | 선택 문자 범위의 서식을 변경한다. | +| 문자 편집기 | 정렬·들여쓰기·줄 간격 | — | 문단 배치와 간격을 조정한다. | +| 문자 편집기 | 열 | — | 여러 줄 문자를 동적·정적 열로 나눈다. | +| 블록 편집기 | 블록 테스트 | — | 동적 블록의 그립과 동작을 시험한다. | +| 블록 편집기 | 매개변수·동작 | — | 동적 블록에 점·선형·회전 등의 매개변수와 동작을 지정한다. | +| 블록 편집기 | 가시성 상태 | — | 하나의 블록에서 표시할 객체 조합을 관리한다. | +| 배열 편집 | 항목 수·간격 | — | 배열 항목의 개수와 행·열 또는 경로 간격을 수정한다. | +| 배열 편집 | 항목 교체·편집 | — | 배열의 원본 항목을 편집하거나 다른 객체로 바꾼다. | +| 배열 편집 | 원본 편집·재설정 | — | 원본 객체를 편집하고 배열 재지정을 초기화한다. | +| 외부 참조·언더레이 | 페이드·대비 | — | 외부 참조와 PDF·DWF·DGN 언더레이의 화면 특성을 조정한다. | +| 외부 참조 | 참조 도면층 | — | 참조에 포함된 도면층 표시를 관리한다. | +| 이미지 | 페이드·대비·밝기 | — | 래스터 이미지의 화면 특성을 조정한다. | +| PDF/DWF/DGN 언더레이 | 단색·언더레이 도면층 | — | 단색 표시와 원본 도면층 가시성을 관리한다. | +| 배치 | 명명된 뷰 삽입 | — | 저장된 모형 뷰를 배치 뷰포트로 배치한다. | +| 테이블 셀 | 행·열 삽입·삭제 | — | 선택 셀 주변의 표 구조를 편집한다. | +| 테이블 셀 | 병합·병합 해제 | — | 여러 셀을 합치거나 원래 셀로 되돌린다. | +| 테이블 셀 | 셀 형식·정렬·테두리 | — | 데이터 형식과 표시 서식을 조정한다. | ## 13. 조사 근거 및 재검증 자료 -- 2.54.0 From c8b5a8bf38e06f8950a5060f2f055a44cd114c05 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 00:15:53 +0900 Subject: [PATCH 04/66] =?UTF-8?q?fix(B05):=20=EC=A2=85=EB=8B=A8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20=EB=B9=88=20=EC=B9=B8=EC=9D=98=20=EC=84=B8=EB=A1=9C?= =?UTF-8?q?=EC=84=A0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구조물 측점(세월교·BOX암거 등) 빈 칸의 border-left가 이웃 규칙 측점 값 한가운데를 지나고, 곡선 L/R 칸 경계와 x가 정확히 겹쳐 절토고~곡선 R를 관통하는 한 줄로 보였다. 값 행에서만 선을 지우고 곡선 행 구분선은 유지한다. Co-Authored-By: Claude Opus 5 (1M context) --- B05_Profile/B05_Profile_UI_Style.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/B05_Profile/B05_Profile_UI_Style.css b/B05_Profile/B05_Profile_UI_Style.css index cef484d2..665e004c 100644 --- a/B05_Profile/B05_Profile_UI_Style.css +++ b/B05_Profile/B05_Profile_UI_Style.css @@ -636,6 +636,13 @@ border-right: 1px solid var(--color-border); } +/* 구조물 측점 빈 칸의 세로선은 이웃 규칙 측점의 값 한가운데를 지나고, 곡선 L/R 칸 + 경계와 x가 겹쳐 곡선 행 구분선이 위 행들까지 이어진 것처럼 보인다. 곡선 행에만 + 남기고 값 행에서는 지운다(2026-08-30 사용자 지시). */ +.b05-profile-table__cell.is-structure { + border-left-color: transparent; +} + .b05-profile-table__row.is-cut .b05-profile-table__cell { color: rgb(220 38 38); } -- 2.54.0 From c8ce7ae02a53e24ccab91d407ca2d5099e31ce04 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 00:29:43 +0900 Subject: [PATCH 05/66] =?UTF-8?q?fix(B06):=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EA=B8=B0=EC=8A=AD=EB=A7=89=EC=9D=B4=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EC=8B=9C=20=EB=B6=80=EB=AA=A8=20=EC=B9=B8=EC=9D=98=20=EB=8B=A8?= =?UTF-8?q?=20=EC=88=98=C2=B7=EC=98=B5=EC=85=98=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 단 수·옵션 행은 조정창 사본에서 DOM째 빌려온 행이라, 다단 벽을 고르면 clearSideSlots()가 되돌린 뒤 다시 채우지 못해 유입구·유출구 칸이 비었다. 다단 벽에서도 tierChannel을 부모 채널로 열어 두 행을 살리고(revetWall에 isExtra 포함), 이식은 부모 칸으로 보낸다. 추가 기슭막이 칸에는 형태· 높이·구간값만 남긴다. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_UI_Adjust_Dock.ts | 40 +++++++++++-------- .../B06_Section_UI_Cross_Structure_Panel.ts | 13 +++++- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/B06_Section/B06_Section_UI_Adjust_Dock.ts b/B06_Section/B06_Section_UI_Adjust_Dock.ts index 52ddbb81..18a83ce5 100644 --- a/B06_Section/B06_Section_UI_Adjust_Dock.ts +++ b/B06_Section/B06_Section_UI_Adjust_Dock.ts @@ -30,13 +30,12 @@ export function setAdjustSideSlots(next: SideSlots): void { sideSlots = next; } -/** 다단 벽에서 옮겨 오는 행 — 폼에 자기 칸이 없어 형태·높이·구간값까지 전부 온다. */ +/** 다단 벽에서 옮겨 오는 행 — 폼에 자기 칸이 없어 형태·높이·구간값이 온다. + * 단 수·옵션은 부모 벽의 값이라 여기 오지 않고 부모 칸에 남는다(2026-08-30 사용자). */ const EXTRA_ROWS = [ ".b06-structure-panel__struct--form", ".b06-structure-panel__struct--height", ".b06-structure-panel__struct--span", - ".b06-structure-panel__struct--tier", - ".b06-structure-panel__struct--options", ].join(", "); /** 유입구·유출구는 폼이 이미 가진 항목 말고 단 수·옵션만 받는다. */ @@ -97,6 +96,18 @@ function clearSideSlots(): void { } } +/** 행 DOM을 제자리 표식과 함께 옮긴다 — 숨은 행은 그대로 둔다. */ +function moveRows(panelRoot: HTMLElement, selector: string, target: HTMLElement): void { + panelRoot.querySelectorAll(selector).forEach((row) => { + if (row.classList.contains("is-hidden")) return; + // 제자리에 표식을 남기고 옮긴다 — 선택이 바뀌면 이 자리로 되돌린다. + const anchor = document.createComment("adjust-row"); + row.parentNode?.insertBefore(anchor, row); + movedRows.push({ row, anchor }); + target.append(row); + }); +} + /** * 조정창 사본의 단 수·옵션 행을 고른 벽 쪽(유입/유출) 그룹으로 옮긴다. 행 DOM을 * 그대로 옮기므로 배선·값 동기는 손대지 않는다. 자리가 없으면 사본에 그대로 둔다. @@ -111,21 +122,18 @@ function relocateSideRows(panelRoot: HTMLElement): void { if (side === "extra") { const group = target.closest("fieldset"); if (!group) return; - const owner = slotOf(panelRoot.dataset.extraSide === "inlet" ? "inlet" : "outlet").closest( - "fieldset", - ); + const ownerSlot = slotOf(panelRoot.dataset.extraSide === "inlet" ? "inlet" : "outlet"); + const owner = ownerSlot.closest("fieldset"); if (owner && owner !== group) owner.after(group); group.hidden = false; - } else if (!slotUsable(target)) return; - const rows = panelRoot.querySelectorAll(side === "extra" ? EXTRA_ROWS : SIDE_ROWS); - rows.forEach((row) => { - if (row.classList.contains("is-hidden")) return; - // 제자리에 표식을 남기고 옮긴다 — 선택이 바뀌면 이 자리로 되돌린다. - const anchor = document.createComment("adjust-row"); - row.parentNode?.insertBefore(anchor, row); - movedRows.push({ row, anchor }); - target.append(row); - }); + moveRows(panelRoot, EXTRA_ROWS, target); + // 단 수·옵션은 그 단이 딸린 부모 벽의 값이라 부모 칸에 그대로 세운다 — 다단을 + // 고를 때 그 두 행이 사라지던 것을 막는다(2026-08-30 사용자). + if (slotUsable(ownerSlot)) moveRows(panelRoot, SIDE_ROWS, ownerSlot); + } else { + if (!slotUsable(target)) return; + moveRows(panelRoot, SIDE_ROWS, target); + } // 고른 구조물이 어느 쪽인지 그룹 테두리로 알린다(2026-08-29 사용자 지시 3). const group = target.closest("fieldset"); if (!group) return; diff --git a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts index 3349554f..b360e64d 100644 --- a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts +++ b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts @@ -569,7 +569,15 @@ export function buildStructurePanel( // 유출 벽과 독립 기슭막이 유입 벽에서는 **항상** 보인다(2026-08-22 사용자 ⑤ — // 상태 따라 나타났다 사라지면 레이아웃이 널뛴다). 집수정 계류측은 둘 자리가 // 있을 때만 뜬다(배관 유입 기슭막이는 다단 채널 자체가 없다). - tierChannel = key === "outlet" ? "outlet" : key === "inlet" ? "basin" : null; + // 다단 벽을 고른 동안에도 단 수는 **그 단이 딸린 부모 벽**의 값이다 — 채널을 + // 부모 쪽으로 열어 두고 행은 부모 칸에 남긴다(2026-08-30 사용자: 추가 기슭막이를 + // 고르면 단 수·옵션이 사라지던 것). + tierChannel = + key === "outlet" || key.startsWith("extra") + ? "outlet" + : key === "inlet" || key.startsWith("bextra") + ? "basin" + : null; // 좌측 [횡단 조정] 사본의 단 수·옵션 행이 어느 그룹(유입구·유출구)으로 옮겨 // 붙을지 알린다(2026-08-29 지시 4). 다단 벽은 자기 채널 쪽으로 간다. // 다단(추가) 벽은 유입·유출 어느 칸에도 자기 자리가 없다 — 유입구·유출구 아래 @@ -596,7 +604,8 @@ export function buildStructurePanel( // 기슭막이 기준벽(배관 유입·유출, 독립 좌·우)에서는 **항상** 보인다(2026-08-30 // 사용자). 링크 카드에서 만져도 제어기가 소유 측점 값을 고치므로 안전하다. // 집수정 유입은 계류측 단이 설 자리가 있을 때만 — 벽이 아니라 구체다. - const revetWall = (key === "inlet" || key === "outlet") && !isBasin; + // 다단 벽도 포함한다 — 행은 부모 칸에 서 있고, 그 값(부모 단 수·옵션)을 그대로 쓴다. + const revetWall = ((key === "inlet" || key === "outlet") && !isBasin) || isExtra; extraRow.classList.toggle( "is-hidden", !revetWall && !(isBasin && tierChannel === "basin" && (tiers.canAdd || tiers.count > 0)), -- 2.54.0 From ea9d9685bed92c2877116a561006e6a64148d1ff Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 01:34:13 +0900 Subject: [PATCH 06/66] =?UTF-8?q?feat(B07):=20CAD=EB=A5=BC=20AutoCAD=20?= =?UTF-8?q?=EB=AA=85=EB=A0=B9=20=EC=B2=B4=EA=B3=84=EB=A1=9C=20=EC=9E=AC?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=ED=95=9C=EB=8B=A4=20(=EC=A1=B0=EC=82=AC?= =?UTF-8?q?=ED=91=9C=201=C2=B72=C2=B73=C2=B75=EC=A0=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 저장소 사고로 잃은 4개 커밋(38eb7cd4·b8a11756·281528bf·f60b488d)의 작업물을 하나로 다시 담았다. 내용은 동일하다. ■ 구조 - commands/: 명령 정의(id·AutoCAD 별칭·글리프·도구/즉시실행)를 단일 소스로 두고 리본·명령행·단축키가 모두 이 레지스트리를 읽는다. tools/tool.consts.ts 폐지. - ribbon/: 탭→패널→명령 데이터(ribbon.config.ts)와 범용 렌더러 분리. AutoCAD 배치(홈·삽입·주석·뷰·출력)와 패널 확장(▾)을 따른다. - tools/factories/sequence-tool.ts: 점·숫자·문자·객체·선택 단계를 선언하면 xstate 머신을 만들어 주는 공장. 명령당 170줄 보일러플레이트 제거. - helpers/geometry/: 3점 호·정다각형·타원·스플라인·구름형·평행이동·해치 스캔선· 점렬 샘플링 등 상태 없는 순수 함수. - Toolbar 483줄을 QuickAccessBar/Ribbon/InspectorPanel/StatusBar/CommandLine/ ViewControls/PropertiesEditor/QuickProperties로 분해. ■ 명령 (조사표 기준) - 1절 그리기 21건, 2절 수정 27건, 3절 도면층·특성·그룹·유틸리티 39건 전부 반영. - 5절 주석 34건 중 26건 반영(문자·치수 16종·지시선·표·구름형·주석 축척). - HatchEntity 추가(solid·pattern·cross·gradient) + JSON 왕복, Layer에 색·선가중치· 선종류·동결·투명도 필드 추가, 그리기 루프가 동결·숨김·투명도를 반영. ■ 화면 실측에서 고친 결함 - 명령행 포커스 상태에서 ENTER가 도구로 가지 않던 문제 - 명령행 문자 입력이 접두사가 같은 명령으로 실행되던 문제 - 명령이 끝나도 입력을 계속 먹던 문제(점 입력 명령만 반복, 나머지는 선택 도구 복귀) - 시퀀스 단계 인덱스 오사용 9건 + 단계 값 종류 검사 추가 - 해치 내부를 클릭해도 선택되지 않던 문제 미반영 8건(맞춤법 검사·꺾기 치수·치수 끊기/재연관/검사 치수·기하공차·지시선 수집· 축척 리스트 편집)은 조사표 `반영` 열과 PLAN.md에 사유를 적었다. --- B07_DesignDetail/openwebcad/src/App.css | 314 +++++++++-- B07_DesignDetail/openwebcad/src/App.types.ts | 8 + .../openwebcad/src/commands/command.types.ts | 29 + .../src/commands/commands.annotate.ts | 299 ++++++++++ .../openwebcad/src/commands/commands.draw.ts | 221 ++++++++ .../openwebcad/src/commands/commands.file.ts | 70 +++ .../src/commands/commands.insert.ts | 16 + .../openwebcad/src/commands/commands.layer.ts | 166 ++++++ .../src/commands/commands.modify.ts | 340 ++++++++++++ .../src/commands/commands.utility.ts | 270 ++++++++++ .../openwebcad/src/commands/commands.view.ts | 71 +++ .../openwebcad/src/commands/dim-settings.ts | 40 ++ .../openwebcad/src/commands/draw-settings.ts | 36 ++ .../openwebcad/src/commands/registry.ts | 95 ++++ .../openwebcad/src/commands/run-command.ts | 61 +++ .../openwebcad/src/components/CommandLine.tsx | 98 ++++ .../src/components/InspectorPanel.tsx | 68 +++ .../src/components/LayerManager.tsx | 12 +- .../src/components/PropertiesEditor.tsx | 154 ++++++ .../src/components/QuickAccessBar.tsx | 34 ++ .../src/components/QuickProperties.tsx | 21 + .../src/components/RibbonWidgets.tsx | 191 +++++++ .../openwebcad/src/components/StatusBar.tsx | 67 +++ .../openwebcad/src/components/Toolbar.tsx | 510 ++---------------- .../src/components/ViewControls.tsx | 28 + .../openwebcad/src/components/ui-state.ts | 40 ++ .../src/components/use-cad-refresh.ts | 18 + .../src/drawControllers/DrawController.ts | 2 + .../screenCanvas.drawController.ts | 5 + .../src/drawControllers/svg.drawController.ts | 3 + .../openwebcad/src/entities/ArcEntity.ts | 4 + .../src/entities/ArrowHeadEntity.ts | 4 + .../openwebcad/src/entities/CircleEntity.ts | 4 + .../openwebcad/src/entities/Entity.ts | 7 + .../openwebcad/src/entities/HatchEntity.ts | 261 +++++++++ .../openwebcad/src/entities/ImageEntity.ts | 4 + .../openwebcad/src/entities/LineEntity.ts | 4 + .../src/entities/MeasurementEntity.ts | 38 +- .../openwebcad/src/entities/PointEntity.ts | 4 + .../openwebcad/src/entities/PolyLineEntity.ts | 9 + .../src/entities/RectangleEntity.ts | 4 + .../openwebcad/src/entities/TextEntity.ts | 4 + .../openwebcad/src/helpers/cad-clipboard.ts | 53 ++ .../openwebcad/src/helpers/debug-hook.ts | 32 ++ .../openwebcad/src/helpers/draw-functions.ts | 10 +- .../openwebcad/src/helpers/entity-groups.ts | 22 + .../src/helpers/geometry/entity-loop.ts | 50 ++ .../src/helpers/geometry/hatch-lines.ts | 67 +++ .../src/helpers/geometry/sample-entity.ts | 125 +++++ .../src/helpers/geometry/shape-points.ts | 207 +++++++ .../import-entities-from-json.ts | 3 + .../openwebcad/src/helpers/layer-history.ts | 32 ++ .../openwebcad/src/helpers/visibility.ts | 34 ++ .../src/inputController/input-controller.ts | 48 +- B07_DesignDetail/openwebcad/src/main.tsx | 4 +- .../openwebcad/src/ribbon/Ribbon.tsx | 148 +++++ .../openwebcad/src/ribbon/ribbon.config.ts | 203 +++++++ B07_DesignDetail/openwebcad/src/tools.ts | 103 +++- .../src/tools/annotate/annotation.helpers.ts | 66 +++ .../tools/annotate/dimension-radial-tools.ts | 256 +++++++++ .../src/tools/annotate/dimension-tools.ts | 163 ++++++ .../src/tools/annotate/leader-table-tools.ts | 209 +++++++ .../src/tools/annotate/text-tools.ts | 103 ++++ .../src/tools/draw/basic-draw-tools.ts | 149 +++++ .../src/tools/draw/construction-tools.ts | 142 +++++ .../openwebcad/src/tools/draw/divide-tools.ts | 41 ++ .../openwebcad/src/tools/draw/fill-tools.ts | 113 ++++ .../src/tools/factories/entity-factory.ts | 64 +++ .../src/tools/factories/sequence-tool.ts | 349 ++++++++++++ .../openwebcad/src/tools/measurement-tool.ts | 221 -------- .../src/tools/modify/corner-tools.ts | 113 ++++ .../src/tools/modify/corner.helpers.ts | 268 +++++++++ .../src/tools/modify/modify.helpers.ts | 229 ++++++++ .../src/tools/modify/property-tools.ts | 154 ++++++ .../src/tools/modify/structure-tools.ts | 81 +++ .../src/tools/modify/transform-tools.ts | 144 +++++ .../src/tools/select-tool.helpers.ts | 14 +- .../openwebcad/src/tools/tool.consts.ts | 75 --- .../src/tools/utility/clipboard-tools.ts | 91 ++++ .../src/tools/utility/inquiry-tools.ts | 161 ++++++ .../src/tools/utility/layer-tools.ts | 265 +++++++++ .../src/tools/utility/property-tools.ts | 125 +++++ .../src/tools/utility/selection-tools.ts | 122 +++++ .../test/helpers/init-application.ts | 2 +- .../test/helpers/replay-recording.ts | 2 +- .../test/helpers/set-active-tool.ts | 2 +- .../openwebcad/vitest.tmp.config.ts | 20 + resources/임시_AutoCAD_리본_기능_조사.md | 383 +++++++++++++ 88 files changed, 8052 insertions(+), 850 deletions(-) create mode 100644 B07_DesignDetail/openwebcad/src/commands/command.types.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/commands.annotate.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/commands.draw.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/commands.file.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/commands.insert.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/commands.layer.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/commands.modify.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/commands.utility.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/commands.view.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/dim-settings.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/draw-settings.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/registry.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/run-command.ts create mode 100644 B07_DesignDetail/openwebcad/src/components/CommandLine.tsx create mode 100644 B07_DesignDetail/openwebcad/src/components/InspectorPanel.tsx create mode 100644 B07_DesignDetail/openwebcad/src/components/PropertiesEditor.tsx create mode 100644 B07_DesignDetail/openwebcad/src/components/QuickAccessBar.tsx create mode 100644 B07_DesignDetail/openwebcad/src/components/QuickProperties.tsx create mode 100644 B07_DesignDetail/openwebcad/src/components/RibbonWidgets.tsx create mode 100644 B07_DesignDetail/openwebcad/src/components/StatusBar.tsx create mode 100644 B07_DesignDetail/openwebcad/src/components/ViewControls.tsx create mode 100644 B07_DesignDetail/openwebcad/src/components/ui-state.ts create mode 100644 B07_DesignDetail/openwebcad/src/components/use-cad-refresh.ts create mode 100644 B07_DesignDetail/openwebcad/src/entities/HatchEntity.ts create mode 100644 B07_DesignDetail/openwebcad/src/helpers/cad-clipboard.ts create mode 100644 B07_DesignDetail/openwebcad/src/helpers/debug-hook.ts create mode 100644 B07_DesignDetail/openwebcad/src/helpers/entity-groups.ts create mode 100644 B07_DesignDetail/openwebcad/src/helpers/geometry/entity-loop.ts create mode 100644 B07_DesignDetail/openwebcad/src/helpers/geometry/hatch-lines.ts create mode 100644 B07_DesignDetail/openwebcad/src/helpers/geometry/sample-entity.ts create mode 100644 B07_DesignDetail/openwebcad/src/helpers/geometry/shape-points.ts create mode 100644 B07_DesignDetail/openwebcad/src/helpers/layer-history.ts create mode 100644 B07_DesignDetail/openwebcad/src/helpers/visibility.ts create mode 100644 B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx create mode 100644 B07_DesignDetail/openwebcad/src/ribbon/ribbon.config.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/annotate/annotation.helpers.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/annotate/dimension-radial-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/annotate/dimension-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/annotate/leader-table-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/annotate/text-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/draw/basic-draw-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/draw/construction-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/draw/divide-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/draw/fill-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/factories/entity-factory.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool.ts delete mode 100644 B07_DesignDetail/openwebcad/src/tools/measurement-tool.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/modify/corner-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/modify/corner.helpers.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/modify/modify.helpers.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/modify/property-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/modify/structure-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/modify/transform-tools.ts delete mode 100644 B07_DesignDetail/openwebcad/src/tools/tool.consts.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/utility/clipboard-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/utility/inquiry-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/utility/layer-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/utility/property-tools.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/utility/selection-tools.ts create mode 100644 B07_DesignDetail/openwebcad/vitest.tmp.config.ts create mode 100644 resources/임시_AutoCAD_리본_기능_조사.md diff --git a/B07_DesignDetail/openwebcad/src/App.css b/B07_DesignDetail/openwebcad/src/App.css index 92bf0607..6c7cc9b3 100644 --- a/B07_DesignDetail/openwebcad/src/App.css +++ b/B07_DesignDetail/openwebcad/src/App.css @@ -4,9 +4,9 @@ :root { --cad-title-height: 42px; - --cad-ribbon-height: 82px; - /* 하단 명령어 입력창 제거 → 높이 0으로 캔버스가 공간을 회수 (추후 사용성 개선 예정) */ - --cad-command-height: 0px; + --cad-ribbon-height: 116px; + /* 명령행 높이 — 상태막대의 [명령행] 토글이 0px로 바꾼다 */ + --cad-command-height: 86px; --cad-status-height: 28px; --cad-panel-width: 248px; font-family: var(--font-body); @@ -139,13 +139,44 @@ body > canvas[data-id="canvas"] { left: 0; height: var(--cad-ribbon-height); display: flex; - align-items: stretch; - padding: 5px 8px 3px; - overflow-x: auto; + flex-direction: column; background: var(--cad-chrome-raised); border-bottom: 1px solid var(--cad-line); } +.cad-ribbon-tabs { + display: flex; + gap: 2px; + height: 26px; + padding: 0 8px; + border-bottom: 1px solid var(--cad-line); +} +.cad-ribbon-tabs button { + padding: 0 14px; + border: 1px solid transparent; + border-bottom: 0; + border-radius: 4px 4px 0 0; + background: transparent; + color: var(--cad-text-dim); + font-size: 12px; +} +.cad-ribbon-tabs button:hover { + background: var(--cad-hover); +} +.cad-ribbon-tabs button[data-active="true"] { + background: var(--cad-chrome); + border-color: var(--cad-line); + color: var(--color-text); +} +.cad-ribbon-panels { + display: flex; + flex: 1; + align-items: stretch; + padding: 4px 8px 2px; + overflow-x: auto; + overflow-y: visible; +} .cad-ribbon-group { + position: relative; display: flex; flex-direction: column; min-width: max-content; @@ -154,54 +185,125 @@ body > canvas[data-id="canvas"] { } .cad-ribbon-tools { display: flex; - gap: 2px; - height: 57px; + gap: 3px; + align-items: flex-start; + height: 66px; +} +.cad-ribbon-tools__grid { + display: grid; + grid-template-rows: 1fr 1fr; + grid-auto-flow: column; + gap: 1px; } .cad-ribbon-group__label { margin-top: auto; + padding: 0; + border: 0; + background: transparent; color: var(--cad-text-muted); font-size: 10px; text-align: center; } -.cad-ribbon-props { +.cad-ribbon-group__label[data-expandable="true"] { + cursor: pointer; +} +.cad-ribbon-group__label[data-expandable="true"]:hover { + color: var(--cad-accent); +} +.cad-ribbon-overflow { + position: absolute; + z-index: 10; + top: 100%; + left: 4px; + display: flex; + flex-direction: column; + min-width: 170px; + padding: 4px; + background: var(--cad-chrome-raised); + border: 1px solid var(--cad-line); + border-radius: 4px; + box-shadow: var(--shadow-lg); +} +.cad-ribbon-overflow button { + display: flex; gap: 8px; align-items: center; + padding: 5px 8px; + border: 0; + border-radius: 3px; + background: transparent; + font-size: 11px; + text-align: left; } -.cad-prop { +.cad-ribbon-overflow button:hover { + background: var(--cad-hover); +} +.cad-quick-access { + display: flex; + gap: 2px; + margin-left: 14px; +} +.cad-quick-access button { + width: 26px; + height: 24px; + border: 1px solid transparent; + border-radius: 3px; + background: transparent; + font-size: 13px; +} +.cad-quick-access button:hover { + border-color: var(--cad-line); + background: var(--cad-hover); +} +.cad-ribbon-props { display: flex; flex-direction: column; gap: 3px; align-items: stretch; + justify-content: flex-start; +} +/* 위젯 패널은 한 줄에 라벨+입력을 놓고 세로로 쌓는다 (리본 높이 안에 들어가야 한다) */ +.cad-prop { + display: flex; + flex-direction: row; + gap: 6px; + align-items: center; font-size: 10px; color: var(--cad-text-muted); } .cad-prop > span { - text-align: center; + width: 52px; + text-align: left; } .cad-prop select, .cad-prop input[type="number"] { - height: 24px; - min-width: 64px; - padding: 0 4px; + height: 19px; + min-width: 78px; + padding: 0 3px; color: var(--cad-text); background: var(--cad-chrome-sunken); border: 1px solid var(--cad-line); - border-radius: 4px; + border-radius: 3px; font-size: 11px; } .cad-prop input[type="number"] { - min-width: 48px; - width: 48px; + min-width: 52px; + width: 52px; } .cad-prop input[type="color"] { - height: 24px; - width: 40px; + height: 19px; + width: 44px; padding: 1px; background: var(--cad-chrome-sunken); border: 1px solid var(--cad-line); - border-radius: 4px; + border-radius: 3px; cursor: pointer; } +/* 위젯 패널 안의 작은 명령 버튼은 한 줄 높이에 맞춘다 */ +.cad-ribbon-props .cad-tool[data-size="small"] { + height: 22px; + min-width: 0; +} .cad-tool { display: flex; flex-direction: column; @@ -214,6 +316,29 @@ body > canvas[data-id="canvas"] { border-radius: 3px; background: transparent; font-size: 10px; + white-space: nowrap; +} +.cad-tool[data-size="big"] { + min-width: 58px; + height: 66px; +} +.cad-tool[data-size="big"] .cad-tool__glyph { + height: 32px; + font-size: 26px; + line-height: 32px; +} +.cad-tool[data-size="small"] { + flex-direction: row; + justify-content: flex-start; + gap: 4px; + min-width: 78px; + height: 32px; + padding: 0 5px; +} +.cad-tool[data-size="small"] .cad-tool__glyph { + height: auto; + font-size: 15px; + line-height: 1; } .cad-tool:hover:not(:disabled), .cad-tool[data-active="true"] { @@ -376,23 +501,62 @@ body > canvas[data-id="canvas"] { /* 하단 명령어 입력창은 숨김 처리 (마우스 커서 단축키 입력으로 대체) */ .cad-command-area { - display: none; + position: fixed; + z-index: 3; + right: 0; + bottom: var(--cad-status-height); + left: var(--cad-panel-width); + height: var(--cad-command-height); + display: flex; + flex-direction: column; + gap: 4px; + padding: 6px 10px; + background: var(--cad-chrome); + border-top: 1px solid var(--cad-line); +} +.cad-command-history { + display: flex; + flex-direction: column; + flex: 1; + justify-content: flex-end; + overflow: hidden; + color: var(--cad-text-muted); + font: 11px Consolas, monospace; +} +.cad-command-suggestions { + position: absolute; + z-index: 6; + bottom: 100%; + left: 10px; + display: flex; + flex-direction: column; + min-width: 220px; + padding: 4px; + background: var(--cad-chrome-raised); + border: 1px solid var(--cad-line); + border-radius: 4px; + box-shadow: var(--shadow-lg); +} +.cad-command-suggestions button { + padding: 4px 8px; + border: 0; + border-radius: 3px; + background: transparent; + font: 11px Consolas, monospace; + text-align: left; +} +.cad-command-suggestions button:hover { + background: var(--cad-hover); +} +.cad-prop--wide select { + min-width: 132px; } .cad-command-prompt { - display: flex; - gap: 12px; - height: 18px; - overflow: hidden; - font-size: 10px; - white-space: nowrap; -} -.cad-command-prompt span { - color: var(--cad-accent); -} -.cad-command-prompt strong { + max-width: 42%; overflow: hidden; color: var(--cad-text-dim); - font-weight: 400; + font-size: 11px; + white-space: nowrap; text-overflow: ellipsis; } .cad-command-area form { @@ -605,3 +769,87 @@ body > canvas[data-id="canvas"] { min-width: auto; } } + +/* 특성 팔레트 — 값을 바로 고치는 입력들 */ +.cad-properties-editor { + display: flex; + flex-direction: column; + gap: 7px; +} +.cad-properties-editor__title { + color: var(--color-text); + font-size: 13px; + font-weight: 600; +} +.cad-properties-editor label { + display: grid; + grid-template-columns: 72px 1fr; + align-items: center; + gap: 8px; + font-size: 11px; + color: var(--cad-text-dim); +} +.cad-properties-editor select, +.cad-properties-editor input { + height: 24px; + min-width: 0; + padding: 0 4px; + border: 1px solid var(--cad-line); + border-radius: 3px; + background: var(--cad-chrome-sunken); + color: var(--cad-text); + font-size: 11px; +} +.cad-properties-editor__readout { + margin: 6px 0 0; + border-top: 1px solid var(--cad-line); +} +.cad-properties-editor__readout div { + display: grid; + grid-template-columns: 72px 1fr; + padding: 5px 0; + border-bottom: 1px solid var(--cad-line); + font-size: 11px; +} +.cad-properties-editor__readout dt { + color: var(--cad-text-dim); +} +.cad-properties-editor__readout dd { + margin: 0; + overflow: hidden; + color: var(--cad-text); + text-overflow: ellipsis; +} +.cad-properties__empty { + margin: 0; + color: var(--cad-text-muted); + font-size: 11px; +} + +/* 빠른 특성 — 화면 위에 뜨는 작은 상자 */ +.cad-quick-properties { + position: fixed; + z-index: 5; + top: calc(var(--cad-title-height) + var(--cad-ribbon-height) + 12px); + right: 14px; + width: 240px; + padding: 8px 10px 10px; + background: var(--cad-chrome-raised); + border: 1px solid var(--cad-line); + border-radius: 5px; + box-shadow: var(--shadow-lg); +} +.cad-quick-properties__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; + color: var(--cad-text-dim); + font-size: 11px; +} +.cad-quick-properties__header button { + border: 0; + background: transparent; + color: var(--cad-text-muted); + font-size: 12px; +} diff --git a/B07_DesignDetail/openwebcad/src/App.types.ts b/B07_DesignDetail/openwebcad/src/App.types.ts index 0cbb3e57..fb521756 100644 --- a/B07_DesignDetail/openwebcad/src/App.types.ts +++ b/B07_DesignDetail/openwebcad/src/App.types.ts @@ -62,6 +62,14 @@ export interface Layer { name: string; isVisible: boolean; isLocked: boolean; + /** 동결 — 표시에서 빼고 선택 대상에서도 제외한다 */ + isFrozen?: boolean; + /** 도면층 기본 특성 (SETBYLAYER가 객체에 입힌다) */ + color?: string; + lineWidth?: number; + lineDash?: number[]; + /** 0~1. 1이면 불투명 */ + opacity?: number; } export enum LOCAL_STORAGE_KEY { diff --git a/B07_DesignDetail/openwebcad/src/commands/command.types.ts b/B07_DesignDetail/openwebcad/src/commands/command.types.ts new file mode 100644 index 00000000..e7d3a807 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/command.types.ts @@ -0,0 +1,29 @@ +/** + * 명령 정의 — 리본 버튼·명령행 입력·단축키가 모두 이 한 곳을 읽는다. + * AutoCAD 명령 이름(id)과 기본 별칭(aliases)을 그대로 쓴다. + */ +import type { Tool } from '../tools'; + +// biome-ignore lint/suspicious/noExplicitAny: xstate 머신 타입 인자가 14개라 그대로 받는다 +export type AnyToolMachine = any; + +export interface CadCommand { + /** AutoCAD 명령 이름 (대문자). 명령행에서 그대로 입력한다 */ + id: string; + /** 리본·툴팁에 쓰는 한글 이름 */ + label: string; + /** AutoCAD 기본 별칭 (대문자) */ + aliases?: string[]; + /** 리본 버튼 글리프 */ + glyph: string; + /** 툴팁 보조 설명 */ + hint?: string; + /** 도구형 명령이 활성화할 도구 */ + tool?: Tool; + /** 도구형 명령의 xstate 머신 */ + machine?: AnyToolMachine; + /** 즉시 실행 명령. 반환 문자열은 명령행 기록에 남는다 (없으면 라벨을 쓴다) */ + run?: () => string | undefined; + /** 선택 객체가 있어야 동작하는 즉시 실행 명령 */ + needsSelection?: boolean; +} diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.annotate.ts b/B07_DesignDetail/openwebcad/src/commands/commands.annotate.ts new file mode 100644 index 00000000..631aadf5 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/commands.annotate.ts @@ -0,0 +1,299 @@ +/** 주석 탭 명령 (조사표 5절 전 항목) */ +import { toast } from 'react-toastify'; +import type { CadCommand } from './command.types'; +import { setActiveRibbonTab } from '../components/ui-state'; +import { Tool } from '../tools'; +import { + centerLineToolStateMachine, + centerMarkToolStateMachine, + dimAngularToolStateMachine, + dimArcToolStateMachine, + dimDiameterToolStateMachine, + dimOrdinateToolStateMachine, + dimRadiusToolStateMachine, + dimSpaceToolStateMachine, +} from '../tools/annotate/dimension-radial-tools'; +import { + dimAlignedToolStateMachine, + dimAutoToolStateMachine, + dimBaselineToolStateMachine, + dimContinueToolStateMachine, + dimLinearToolStateMachine, + qDimToolStateMachine, +} from '../tools/annotate/dimension-tools'; +import { + annotationScaleToolStateMachine, + dimStyleToolStateMachine, + mleaderAlignToolStateMachine, + mleaderStyleToolStateMachine, + mleaderToolStateMachine, + revCloudToolStateMachine, + tableStyleToolStateMachine, + tableToolStateMachine, + updateDimensions, +} from '../tools/annotate/leader-table-tools'; +import { + findToolStateMachine, + mtextToolStateMachine, + textEditToolStateMachine, + textToolStateMachine, +} from '../tools/annotate/text-tools'; + +export const TEXT_COMMANDS: CadCommand[] = [ + { + id: 'MTEXT', + label: '여러 줄 문자', + aliases: ['T', 'MT'], + glyph: '¶', + hint: '여러 줄 문자를 작성한다 (줄바꿈은 \\P)', + tool: Tool.MTEXT, + machine: mtextToolStateMachine, + }, + { + id: 'TEXT', + label: '단일 행 문자', + aliases: ['DT'], + glyph: 'A', + hint: '한 줄짜리 문자를 작성한다', + tool: Tool.TEXT, + machine: textToolStateMachine, + }, + { + id: 'STYLE', + label: '문자 스타일', + aliases: ['ST'], + glyph: '🅰', + hint: '리본 문자 패널에서 글꼴·크기·색을 지정한다', + run: () => { + setActiveRibbonTab('annotate'); + toast.info('주석 탭 문자 패널에서 글꼴과 크기를 지정하십시오.'); + return '문자 스타일'; + }, + }, + { + id: 'TEXTEDIT', + label: '문자 편집', + aliases: ['TEDIT'], + glyph: '✎', + hint: '기존 문자의 내용을 고친다', + tool: Tool.TEXTEDIT, + machine: textEditToolStateMachine, + }, + { + id: 'FIND', + label: '찾기·대치', + glyph: '🔍', + hint: '도면 문자를 찾고 바꾼다', + tool: Tool.FIND, + machine: findToolStateMachine, + }, +]; + +export const DIMENSION_COMMANDS: CadCommand[] = [ + { + id: 'DIM', + label: '치수', + glyph: '⟺', + hint: '선택 객체에 맞는 치수를 자동으로 넣는다', + tool: Tool.DIM, + machine: dimAutoToolStateMachine, + }, + { + id: 'DIMLINEAR', + label: '선형 치수', + aliases: ['DLI'], + glyph: '↔', + hint: '수평 또는 수직 거리를 기입한다', + tool: Tool.DIMLINEAR, + machine: dimLinearToolStateMachine, + }, + { + id: 'DIMALIGNED', + label: '정렬 치수', + aliases: ['DAL'], + glyph: '⤢', + hint: '두 점과 평행한 실제 길이를 기입한다', + tool: Tool.DIMALIGNED, + machine: dimAlignedToolStateMachine, + }, + { + id: 'DIMANGULAR', + label: '각도 치수', + aliases: ['DAN'], + glyph: '∠', + hint: '세 점이 이루는 각도를 기입한다', + tool: Tool.DIMANGULAR, + machine: dimAngularToolStateMachine, + }, + { + id: 'DIMARC', + label: '호 길이 치수', + aliases: ['DAR'], + glyph: '⌒', + hint: '호의 곡선 길이를 기입한다', + tool: Tool.DIMARC, + machine: dimArcToolStateMachine, + }, + { + id: 'DIMRADIUS', + label: '반지름 치수', + aliases: ['DRA'], + glyph: 'R', + hint: '원·호의 반지름을 기입한다', + tool: Tool.DIMRADIUS, + machine: dimRadiusToolStateMachine, + }, + { + id: 'DIMDIAMETER', + label: '지름 치수', + aliases: ['DDI'], + glyph: 'Ø', + hint: '원·호의 지름을 기입한다', + tool: Tool.DIMDIAMETER, + machine: dimDiameterToolStateMachine, + }, + { + id: 'DIMORDINATE', + label: '세로좌표 치수', + aliases: ['DOR'], + glyph: '⌐', + hint: '기준 원점에 대한 X 또는 Y 좌표를 기입한다', + tool: Tool.DIMORDINATE, + machine: dimOrdinateToolStateMachine, + }, + { + id: 'DIMBASELINE', + label: '기준선 치수', + aliases: ['DBA'], + glyph: '⊞', + hint: '직전 치수의 시작점을 기준으로 이어 기입한다', + tool: Tool.DIMBASELINE, + machine: dimBaselineToolStateMachine, + }, + { + id: 'DIMCONTINUE', + label: '연속 치수', + aliases: ['DCO'], + glyph: '⋯', + hint: '직전 치수의 끝점에서 이어 기입한다', + tool: Tool.DIMCONTINUE, + machine: dimContinueToolStateMachine, + }, + { + id: 'QDIM', + label: '빠른 치수', + glyph: '⚡', + hint: '선택 객체에 치수를 한 번에 넣는다', + tool: Tool.QDIM, + machine: qDimToolStateMachine, + }, + { + id: 'CENTERMARK', + label: '중심 표식', + glyph: '✛', + hint: '원·호 중심에 표식을 넣는다', + tool: Tool.CENTERMARK, + machine: centerMarkToolStateMachine, + }, + { + id: 'CENTERLINE', + label: '중심선', + glyph: '┈', + hint: '두 선 사이에 중심선을 넣는다', + tool: Tool.CENTERLINE, + machine: centerLineToolStateMachine, + }, + { + id: 'DIMSTYLE', + label: '치수 스타일', + aliases: ['D'], + glyph: '⚙', + hint: '치수 문자 높이·화살표 크기·소수 자릿수를 정한다', + tool: Tool.DIMSTYLE, + machine: dimStyleToolStateMachine, + }, + { + id: 'DIMUPDATE', + label: '치수 업데이트', + glyph: '⟳', + hint: '바꾼 치수 스타일을 기존 치수에 반영한다', + run: updateDimensions, + }, + { + id: 'DIMSPACE', + label: '치수 간격', + glyph: '⇕', + hint: '평행한 치수선의 간격을 고르게 맞춘다', + tool: Tool.DIMSPACE, + machine: dimSpaceToolStateMachine, + }, +]; + +export const LEADER_COMMANDS: CadCommand[] = [ + { + id: 'MLEADER', + label: '다중 지시선', + aliases: ['MLD'], + glyph: '➤', + hint: '화살표와 문자를 잇는 지시선을 작성한다', + tool: Tool.MLEADER, + machine: mleaderToolStateMachine, + }, + { + id: 'MLEADERSTYLE', + label: '지시선 스타일', + aliases: ['MLS'], + glyph: '⚙', + hint: '지시선 문자 높이와 화살표 크기를 정한다', + tool: Tool.MLEADERSTYLE, + machine: mleaderStyleToolStateMachine, + }, + { + id: 'MLEADERALIGN', + label: '지시선 정렬', + glyph: '≡', + hint: '선택한 지시선 문자의 위치를 맞춘다', + tool: Tool.MLEADERALIGN, + machine: mleaderAlignToolStateMachine, + }, +]; + +export const TABLE_COMMANDS: CadCommand[] = [ + { + id: 'TABLE', + label: '테이블', + aliases: ['TB'], + glyph: '▦', + hint: '행·열 격자를 도면에 배치한다', + tool: Tool.TABLE, + machine: tableToolStateMachine, + }, + { + id: 'TABLESTYLE', + label: '테이블 스타일', + aliases: ['TS'], + glyph: '⚙', + hint: '표의 열 너비와 행 높이를 정한다', + tool: Tool.TABLESTYLE, + machine: tableStyleToolStateMachine, + }, +]; + +export const MARKUP_COMMANDS: CadCommand[] = [ + { + id: 'REVCLOUD', + label: '구름형 리비전', + glyph: '☁', + hint: '검토 범위를 구름형 선으로 표시한다', + tool: Tool.REVCLOUD, + machine: revCloudToolStateMachine, + }, + { + id: 'ANNOSCALE', + label: '주석 축척', + glyph: '⚖', + hint: '치수·문자·화살표 크기에 곱할 축척을 정한다', + tool: Tool.ANNOSCALE, + machine: annotationScaleToolStateMachine, + }, +]; diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.draw.ts b/B07_DesignDetail/openwebcad/src/commands/commands.draw.ts new file mode 100644 index 00000000..2bd50c85 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/commands.draw.ts @@ -0,0 +1,221 @@ +/** 홈 탭 — 그리기 패널 명령 (조사표 1절 전 항목) */ +import type { CadCommand } from './command.types'; +import { Tool } from '../tools'; +import { circleToolStateMachine } from '../tools/circle-tool'; +import { + arcToolStateMachine, + donutToolStateMachine, + ellipseToolStateMachine, + plineToolStateMachine, + pointToolStateMachine, + polygonToolStateMachine, + splineToolStateMachine, +} from '../tools/draw/basic-draw-tools'; +import { + mlineToolStateMachine, + mlstyleToolStateMachine, + rayToolStateMachine, + wipeoutToolStateMachine, + xlineToolStateMachine, +} from '../tools/draw/construction-tools'; +import { + divideToolStateMachine, + measureLengthToolStateMachine, +} from '../tools/draw/divide-tools'; +import { + boundaryToolStateMachine, + gradientToolStateMachine, + hatchToolStateMachine, + regionToolStateMachine, +} from '../tools/draw/fill-tools'; +import { lineToolStateMachine } from '../tools/line-tool'; +import { rectangleToolStateMachine } from '../tools/rectangle-tool'; + +export const DRAW_COMMANDS: CadCommand[] = [ + { + id: 'LINE', + label: '선', + aliases: ['L'], + glyph: '╱', + hint: '두 점 사이에 직선 세그먼트를 작성한다', + tool: Tool.LINE, + machine: lineToolStateMachine, + }, + { + id: 'PLINE', + label: '폴리선', + aliases: ['PL'], + glyph: '⌁', + hint: '연결된 선을 하나의 객체로 작성한다', + tool: Tool.PLINE, + machine: plineToolStateMachine, + }, + { + id: 'CIRCLE', + label: '원', + aliases: ['C'], + glyph: '○', + hint: '중심과 반지름으로 원을 작성한다', + tool: Tool.CIRCLE, + machine: circleToolStateMachine, + }, + { + id: 'ARC', + label: '호', + aliases: ['A'], + glyph: '◜', + hint: '시작점·통과점·끝점 세 점으로 호를 작성한다', + tool: Tool.ARC, + machine: arcToolStateMachine, + }, + { + id: 'RECTANG', + label: '직사각형', + aliases: ['REC'], + glyph: '▭', + hint: '두 대각점으로 직사각형을 작성한다', + tool: Tool.RECTANGLE, + machine: rectangleToolStateMachine, + }, + { + id: 'POLYGON', + label: '다각형', + aliases: ['POL'], + glyph: '⬠', + hint: '지정한 변 수의 정다각형을 작성한다', + tool: Tool.POLYGON, + machine: polygonToolStateMachine, + }, + { + id: 'ELLIPSE', + label: '타원', + aliases: ['EL'], + glyph: '⬭', + hint: '중심과 두 축으로 타원을 작성한다', + tool: Tool.ELLIPSE, + machine: ellipseToolStateMachine, + }, + { + id: 'SPLINE', + label: '스플라인', + aliases: ['SPL'], + glyph: '∿', + hint: '지정한 점을 지나는 부드러운 곡선을 작성한다', + tool: Tool.SPLINE, + machine: splineToolStateMachine, + }, + { + id: 'MLINE', + label: '다중선', + aliases: ['ML'], + glyph: '⋕', + hint: '평행한 여러 선을 한 번에 작성한다', + tool: Tool.MLINE, + machine: mlineToolStateMachine, + }, + { + id: 'MLSTYLE', + label: '다중선 스타일', + glyph: '⚙', + hint: '다중선의 요소 수와 간격을 정한다', + tool: Tool.MLSTYLE, + machine: mlstyleToolStateMachine, + }, + { + id: 'XLINE', + label: '구성선', + aliases: ['XL'], + glyph: '⟷', + hint: '양방향으로 길게 뻗는 기준선을 작성한다', + tool: Tool.XLINE, + machine: xlineToolStateMachine, + }, + { + id: 'RAY', + label: '광선', + glyph: '⟶', + hint: '한 방향으로 길게 뻗는 기준선을 작성한다', + tool: Tool.RAY, + machine: rayToolStateMachine, + }, + { + id: 'POINT', + label: '점', + aliases: ['PO'], + glyph: '·', + hint: '점 객체를 작성한다', + tool: Tool.POINT, + machine: pointToolStateMachine, + }, + { + id: 'DONUT', + label: '도넛', + aliases: ['DO'], + glyph: '◎', + hint: '내부·외부 지름으로 링을 작성한다', + tool: Tool.DONUT, + machine: donutToolStateMachine, + }, + { + id: 'DIVIDE', + label: '등분', + aliases: ['DIV'], + glyph: '⋯', + hint: '객체를 자르지 않고 같은 간격의 점을 배치한다', + tool: Tool.DIVIDE, + machine: divideToolStateMachine, + }, + { + id: 'MEASURE', + label: '길이분할', + aliases: ['ME'], + glyph: '⋮', + hint: '지정 거리마다 점을 배치한다', + tool: Tool.MEASURE_LENGTH, + machine: measureLengthToolStateMachine, + }, + { + id: 'HATCH', + label: '해치', + aliases: ['H', 'BH'], + glyph: '▨', + hint: '닫힌 경계를 패턴으로 채운다', + tool: Tool.HATCH, + machine: hatchToolStateMachine, + }, + { + id: 'GRADIENT', + label: '그라데이션', + aliases: ['GD'], + glyph: '◪', + hint: '닫힌 경계를 색 변화로 채운다', + tool: Tool.GRADIENT, + machine: gradientToolStateMachine, + }, + { + id: 'BOUNDARY', + label: '경계', + aliases: ['BO'], + glyph: '⬡', + hint: '선택 객체에서 닫힌 폴리선 경계를 만든다', + tool: Tool.BOUNDARY, + machine: boundaryToolStateMachine, + }, + { + id: 'REGION', + label: '영역', + aliases: ['REG'], + glyph: '⬢', + hint: '닫힌 객체를 영역(닫힌 폴리선)으로 만든다', + tool: Tool.REGION, + machine: regionToolStateMachine, + }, + { + id: 'WIPEOUT', + label: '와이프아웃', + glyph: '▩', + hint: '뒤쪽 객체를 가리는 마스크를 작성한다', + tool: Tool.WIPEOUT, + machine: wipeoutToolStateMachine, + }, +]; diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.file.ts b/B07_DesignDetail/openwebcad/src/commands/commands.file.ts new file mode 100644 index 00000000..71c1c3a8 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/commands.file.ts @@ -0,0 +1,70 @@ +/** 빠른 실행 도구막대 · 출력 탭 명령 (파일 수명주기) */ +import { toast } from 'react-toastify'; +import type { CadCommand } from './command.types'; +import { exportEntitiesToJsonFile } from '../helpers/import-export-handlers/export-entities-to-json'; +import { exportEntitiesToLocalStorage } from '../helpers/import-export-handlers/export-entities-to-local-storage'; +import { exportEntitiesToPngFile } from '../helpers/import-export-handlers/export-entities-to-png'; +import { exportEntitiesToSvgFile } from '../helpers/import-export-handlers/export-entities-to-svg'; +import { redo, undo } from '../state'; + +export const FILE_COMMANDS: CadCommand[] = [ + { + id: 'QSAVE', + label: '저장', + aliases: ['SAVE'], + glyph: '💾', + hint: '현재 도면을 브라우저에 저장한다', + run: () => { + void exportEntitiesToLocalStorage().then(() => toast.success('도면을 저장했습니다.')); + return '도면 저장'; + }, + }, + { + id: 'EXPORT', + label: 'JSON 내보내기', + aliases: ['EXP'], + glyph: '⭳', + hint: '도면을 JSON 파일로 내보낸다', + run: () => { + void exportEntitiesToJsonFile(); + return 'JSON 내보내기'; + }, + }, + { + id: 'EXPORTSVG', + label: 'SVG 내보내기', + glyph: '🖼', + run: () => { + exportEntitiesToSvgFile(); + return 'SVG 내보내기'; + }, + }, + { + id: 'EXPORTPNG', + label: 'PNG 내보내기', + glyph: '🏞', + run: () => { + void exportEntitiesToPngFile(); + return 'PNG 내보내기'; + }, + }, + { + id: 'UNDO', + label: '실행 취소', + aliases: ['U'], + glyph: '↶', + run: () => { + undo(); + return '실행 취소'; + }, + }, + { + id: 'REDO', + label: '다시 실행', + glyph: '↷', + run: () => { + redo(); + return '다시 실행'; + }, + }, +]; diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.insert.ts b/B07_DesignDetail/openwebcad/src/commands/commands.insert.ts new file mode 100644 index 00000000..5e64ba67 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/commands.insert.ts @@ -0,0 +1,16 @@ +/** 삽입 탭 명령 (조사표 4절 중 구현분) */ +import type { CadCommand } from './command.types'; +import { Tool } from '../tools'; +import { imageImportToolStateMachine } from '../tools/image-import-tool'; + +export const INSERT_COMMANDS: CadCommand[] = [ + { + id: 'IMAGEATTACH', + label: '이미지 부착', + aliases: ['IAT'], + glyph: '🖼', + hint: '래스터 이미지를 도면에 배치한다', + tool: Tool.IMAGE_IMPORT, + machine: imageImportToolStateMachine, + }, +]; diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.layer.ts b/B07_DesignDetail/openwebcad/src/commands/commands.layer.ts new file mode 100644 index 00000000..d5ea2b0f --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/commands.layer.ts @@ -0,0 +1,166 @@ +/** 홈 탭 — 도면층 패널 명령 (조사표 3절) */ +import type { CadCommand } from './command.types'; +import { Tool } from '../tools'; +import { + layerCurrentToolStateMachine, + layerDeleteToolStateMachine, + layerFreezeToolStateMachine, + layerIsolateToolStateMachine, + layerLockToolStateMachine, + layerMatchToolStateMachine, + layerMergeToolStateMachine, + layerOffToolStateMachine, + layerStateRestoreToolStateMachine, + layerStateSaveToolStateMachine, + layerToCurrentToolStateMachine, + layerUnlockToolStateMachine, + openLayerManager, + restorePreviousLayers, + thawAllLayers, + turnAllLayersOn, + unisolateLayers, + walkLayers, +} from '../tools/utility/layer-tools'; + +export const LAYER_COMMANDS: CadCommand[] = [ + { + id: 'LAYER', + label: '도면층 특성', + aliases: ['LA'], + glyph: '▤', + hint: '도면층을 만들고 이름·표시·잠금을 관리한다', + run: openLayerManager, + }, + { + id: 'LAYCURSET', + label: '현재 도면층 설정', + glyph: '◉', + hint: '선택한 객체의 도면층을 현재 도면층으로 지정한다', + tool: Tool.LAYER_CURRENT, + machine: layerCurrentToolStateMachine, + }, + { + id: 'LAYOFF', + label: '도면층 끄기', + glyph: '◐', + hint: '선택 객체가 속한 도면층을 숨긴다', + tool: Tool.LAYOFF, + machine: layerOffToolStateMachine, + }, + { + id: 'LAYON', + label: '모두 켜기', + glyph: '◉', + hint: '모든 도면층을 표시한다', + run: turnAllLayersOn, + }, + { + id: 'LAYFRZ', + label: '동결', + glyph: '❄', + hint: '도면층을 표시·재생성 대상에서 뺀다', + tool: Tool.LAYFRZ, + machine: layerFreezeToolStateMachine, + }, + { + id: 'LAYTHW', + label: '동결 해제', + glyph: '☀', + hint: '모든 도면층의 동결을 푼다', + run: thawAllLayers, + }, + { + id: 'LAYLCK', + label: '잠금', + glyph: '🔒', + hint: '도면층 객체를 편집할 수 없게 한다', + tool: Tool.LAYLCK, + machine: layerLockToolStateMachine, + }, + { + id: 'LAYULK', + label: '잠금 해제', + glyph: '🔓', + hint: '도면층 잠금을 푼다', + tool: Tool.LAYULK, + machine: layerUnlockToolStateMachine, + }, + { + id: 'LAYISO', + label: '분리', + glyph: '◫', + hint: '선택 객체의 도면층만 남기고 나머지를 숨긴다', + tool: Tool.LAYISO, + machine: layerIsolateToolStateMachine, + }, + { + id: 'LAYUNISO', + label: '분리 해제', + glyph: '◻', + hint: '도면층 분리 이전 상태로 되돌린다', + run: unisolateLayers, + }, + { + id: 'LAYERP', + label: '이전 상태', + glyph: '↺', + hint: '직전 도면층 설정으로 되돌린다', + run: restorePreviousLayers, + }, + { + id: 'LAYERSTATE', + label: '도면층 상태 저장', + aliases: ['LAS'], + glyph: '💾', + hint: '현재 도면층 설정을 이름으로 저장한다', + tool: Tool.LAYERSTATE, + machine: layerStateSaveToolStateMachine, + }, + { + id: 'LAYERSTATERESTORE', + label: '도면층 상태 복원', + glyph: '⭯', + hint: '저장한 도면층 설정을 되돌린다', + tool: Tool.LAYERSTATE_RESTORE, + machine: layerStateRestoreToolStateMachine, + }, + { + id: 'LAYMCH', + label: '도면층 일치', + glyph: '⇄', + hint: '선택 객체를 대상 객체의 도면층으로 옮긴다', + tool: Tool.LAYMCH, + machine: layerMatchToolStateMachine, + }, + { + id: 'LAYCUR', + label: '현재 도면층으로', + glyph: '⇥', + hint: '선택 객체를 현재 도면층으로 옮긴다', + tool: Tool.LAYCUR, + machine: layerToCurrentToolStateMachine, + }, + { + id: 'LAYMRG', + label: '도면층 병합', + glyph: '⊎', + hint: '한 도면층의 객체를 다른 도면층으로 옮기고 원래 도면층을 지운다', + tool: Tool.LAYMRG, + machine: layerMergeToolStateMachine, + }, + { + id: 'LAYDEL', + label: '도면층 삭제', + glyph: '🗑', + hint: '선택한 도면층과 그 객체를 삭제한다', + tool: Tool.LAYDEL, + machine: layerDeleteToolStateMachine, + }, + { + id: 'LAYWALK', + label: '도면층 탐색', + glyph: '👣', + hint: '부를 때마다 다음 도면층만 표시한다', + run: walkLayers, + }, +]; diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.modify.ts b/B07_DesignDetail/openwebcad/src/commands/commands.modify.ts new file mode 100644 index 00000000..14e078a6 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/commands.modify.ts @@ -0,0 +1,340 @@ +/** 홈 탭 — 수정 패널 명령 (조사표 2절 전 항목) */ +import type { CadCommand } from './command.types'; +import { Tool } from '../tools'; +import { alignBottomToolStateMachine } from '../tools/align-bottom-tool'; +import { alignCenterHorizontalToolStateMachine } from '../tools/align-center-horizontal-tool'; +import { alignLeftToolStateMachine } from '../tools/align-left-tool'; +import { alignCenterVerticalToolStateMachine } from '../tools/align-middle-vertical-tool'; +import { alignRightToolStateMachine } from '../tools/align-right-tool'; +import { alignTopToolStateMachine } from '../tools/align-top-tool'; +import { arrayToolStateMachine } from '../tools/array-tool'; +import { copyToolStateMachine } from '../tools/copy-tool'; +import { eraserToolStateMachine } from '../tools/eraser-tool'; +import { + blendToolStateMachine, + breakAtPointToolStateMachine, + breakToolStateMachine, + chamferToolStateMachine, + filletToolStateMachine, +} from '../tools/modify/corner-tools'; +import { + drawOrderBackToolStateMachine, + drawOrderFrontToolStateMachine, + hatchEditToolStateMachine, + matchPropToolStateMachine, + overkillToolStateMachine, + reverseToolStateMachine, + setByLayerToolStateMachine, +} from '../tools/modify/property-tools'; +import { + eraseToolStateMachine, + explodeToolStateMachine, + extendToolStateMachine, + joinToolStateMachine, +} from '../tools/modify/structure-tools'; +import { + alignToolStateMachine, + lengthenToolStateMachine, + mirrorToolStateMachine, + offsetToolStateMachine, + stretchToolStateMachine, +} from '../tools/modify/transform-tools'; +import { moveToolStateMachine } from '../tools/move-tool'; +import { peditToolStateMachine } from '../tools/pedit-tool'; +import { rotateToolStateMachine } from '../tools/rotate-tool'; +import { scaleToolStateMachine } from '../tools/scale-tool'; + +export const MODIFY_COMMANDS: CadCommand[] = [ + { + id: 'MOVE', + label: '이동', + aliases: ['M'], + glyph: '✥', + hint: '선택 객체를 기준점에서 새 위치로 이동한다', + tool: Tool.MOVE, + machine: moveToolStateMachine, + }, + { + id: 'COPY', + label: '복사', + aliases: ['CO', 'CP'], + glyph: '⧉', + hint: '선택 객체를 하나 이상의 위치에 복제한다', + tool: Tool.COPY, + machine: copyToolStateMachine, + }, + { + id: 'ROTATE', + label: '회전', + aliases: ['RO'], + glyph: '↻', + hint: '기준점을 중심으로 객체를 회전한다', + tool: Tool.ROTATE, + machine: rotateToolStateMachine, + }, + { + id: 'SCALE', + label: '축척', + aliases: ['SC'], + glyph: '⤢', + hint: '기준점과 비율로 크기를 변경한다', + tool: Tool.SCALE, + machine: scaleToolStateMachine, + }, + { + id: 'ALIGN', + label: '정렬', + aliases: ['AL'], + glyph: '⇲', + hint: '원본점·대상점 두 쌍으로 객체를 이동·회전한다', + tool: Tool.ALIGN, + machine: alignToolStateMachine, + }, + { + id: 'MIRROR', + label: '대칭', + aliases: ['MI'], + glyph: '⇋', + hint: '대칭축을 기준으로 객체를 반사 복사한다', + tool: Tool.MIRROR, + machine: mirrorToolStateMachine, + }, + { + id: 'OFFSET', + label: '간격띄우기', + aliases: ['O'], + glyph: '⇶', + hint: '평행선·동심원을 지정 거리만큼 띄워 만든다', + tool: Tool.OFFSET, + machine: offsetToolStateMachine, + }, + { + id: 'ARRAY', + label: '배열', + aliases: ['AR'], + glyph: '⋮⋮', + hint: '직사각형 패턴으로 객체를 반복 배치한다', + tool: Tool.ARRAY, + machine: arrayToolStateMachine, + }, + { + id: 'TRIM', + label: '자르기', + aliases: ['TR'], + glyph: '⌦', + hint: '교차점 사이의 필요 없는 부분을 잘라낸다', + tool: Tool.ERASER, + machine: eraserToolStateMachine, + }, + { + id: 'EXTEND', + label: '연장', + aliases: ['EX'], + glyph: '⇥', + hint: '선의 끝을 지정한 경계까지 늘린다', + tool: Tool.EXTEND, + machine: extendToolStateMachine, + }, + { + id: 'STRETCH', + label: '신축', + aliases: ['S'], + glyph: '↔', + hint: '기준점에 가까운 끝점을 끌어 늘린다', + tool: Tool.STRETCH, + machine: stretchToolStateMachine, + }, + { + id: 'FILLET', + label: '모깎기', + aliases: ['F'], + glyph: '◟', + hint: '두 선을 지정 반지름의 호로 연결한다', + tool: Tool.FILLET, + machine: filletToolStateMachine, + }, + { + id: 'CHAMFER', + label: '모따기', + aliases: ['CHA'], + glyph: '◺', + hint: '두 선을 직선 모따기로 연결한다', + tool: Tool.CHAMFER, + machine: chamferToolStateMachine, + }, + { + id: 'BLEND', + label: '곡선 혼합', + glyph: '∿', + hint: '두 곡선의 끝을 부드러운 곡선으로 잇는다', + tool: Tool.BLEND, + machine: blendToolStateMachine, + }, + { + id: 'BREAK', + label: '끊기', + aliases: ['BR'], + glyph: '⊣⊢', + hint: '두 점 사이를 제거해 객체를 끊는다', + tool: Tool.BREAK, + machine: breakToolStateMachine, + }, + { + id: 'BREAKATPOINT', + label: '점에서 끊기', + glyph: '⊥', + hint: '지정한 점에서 객체를 둘로 나눈다', + tool: Tool.BREAK_AT_POINT, + machine: breakAtPointToolStateMachine, + }, + { + id: 'JOIN', + label: '결합', + aliases: ['J'], + glyph: '⋈', + hint: '끝이 맞는 객체를 하나의 폴리선으로 결합한다', + tool: Tool.JOIN, + machine: joinToolStateMachine, + }, + { + id: 'EXPLODE', + label: '분해', + aliases: ['X'], + glyph: '✳', + hint: '폴리선·사각형·해치를 구성요소로 분해한다', + tool: Tool.EXPLODE, + machine: explodeToolStateMachine, + }, + { + id: 'ERASE', + label: '지우기', + aliases: ['E'], + glyph: '⌫', + hint: '선택한 객체를 삭제한다', + tool: Tool.ERASE, + machine: eraseToolStateMachine, + }, + { + id: 'LENGTHEN', + label: '길이조정', + aliases: ['LEN'], + glyph: '↦', + hint: '선의 길이를 증분만큼 늘리거나 줄인다', + tool: Tool.LENGTHEN, + machine: lengthenToolStateMachine, + }, + { + id: 'PEDIT', + label: '폴리선 편집', + aliases: ['PE'], + glyph: '⌁', + hint: '연결된 선을 폴리선으로 묶거나 편집한다', + tool: Tool.PEDIT, + machine: peditToolStateMachine, + }, + { + id: 'HATCHEDIT', + label: '해치 편집', + aliases: ['HE'], + glyph: '▨', + hint: '기존 해치의 패턴을 바꾼다', + tool: Tool.HATCHEDIT, + machine: hatchEditToolStateMachine, + }, + { + id: 'DRAWORDER', + label: '맨 앞으로', + aliases: ['DR'], + glyph: '⤒', + hint: '선택 객체를 가장 위에 그린다', + tool: Tool.DRAWORDER_FRONT, + machine: drawOrderFrontToolStateMachine, + }, + { + id: 'DRAWORDERBACK', + label: '맨 뒤로', + glyph: '⤓', + hint: '선택 객체를 가장 아래에 그린다', + tool: Tool.DRAWORDER_BACK, + machine: drawOrderBackToolStateMachine, + }, + { + id: 'MATCHPROP', + label: '특성 일치', + aliases: ['MA'], + glyph: '🖌', + hint: '원본 객체의 색·굵기·선종류를 대상에 복사한다', + tool: Tool.MATCHPROP, + machine: matchPropToolStateMachine, + }, + { + id: 'OVERKILL', + label: '중복 객체 삭제', + glyph: '⧉', + hint: '같은 자리에 겹친 중복 객체를 지운다', + tool: Tool.OVERKILL, + machine: overkillToolStateMachine, + }, + { + id: 'REVERSE', + label: '방향 반전', + glyph: '⇄', + hint: '선·폴리선의 시작점과 끝점을 뒤집는다', + tool: Tool.REVERSE, + machine: reverseToolStateMachine, + }, + { + id: 'SETBYLAYER', + label: 'ByLayer로', + glyph: '▤', + hint: '선택 객체의 특성을 도면층 값으로 되돌린다', + tool: Tool.SETBYLAYER, + machine: setByLayerToolStateMachine, + }, +]; + +/** 원본 openwebcad의 정렬 도구 — AutoCAD 리본에는 없지만 기존 기능이라 유지한다 */ +export const ALIGN_COMMANDS: CadCommand[] = [ + { + id: 'ALIGNLEFT', + label: '왼쪽', + glyph: '⇤', + tool: Tool.ALIGN_LEFT, + machine: alignLeftToolStateMachine, + }, + { + id: 'ALIGNCENTERH', + label: '가로 중앙', + glyph: '↔', + tool: Tool.ALIGN_CENTER_HORIZONTAL, + machine: alignCenterHorizontalToolStateMachine, + }, + { + id: 'ALIGNRIGHT', + label: '오른쪽', + glyph: '⇥', + tool: Tool.ALIGN_RIGHT, + machine: alignRightToolStateMachine, + }, + { + id: 'ALIGNTOP', + label: '위', + glyph: '⤒', + tool: Tool.ALIGN_TOP, + machine: alignTopToolStateMachine, + }, + { + id: 'ALIGNCENTERV', + label: '세로 중앙', + glyph: '↕', + tool: Tool.ALIGN_CENTER_VERTICAL, + machine: alignCenterVerticalToolStateMachine, + }, + { + id: 'ALIGNBOTTOM', + label: '아래', + glyph: '⤓', + tool: Tool.ALIGN_BOTTOM, + machine: alignBottomToolStateMachine, + }, +]; diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.utility.ts b/B07_DesignDetail/openwebcad/src/commands/commands.utility.ts new file mode 100644 index 00000000..038e923e --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/commands.utility.ts @@ -0,0 +1,270 @@ +/** 홈 탭 — 특성·그룹·유틸리티·클립보드 명령 (조사표 3절) */ +import type { CadCommand } from './command.types'; +import { Tool } from '../tools'; +import { + copyBaseToolStateMachine, + copySelectionToClipboard, + cutSelectionToClipboard, + pasteAsGroup, + pasteAtOriginalCoordinates, + pasteToolStateMachine, +} from '../tools/utility/clipboard-tools'; +import { + angleToolStateMachine, + areaToolStateMachine, + distanceToolStateMachine, + idPointToolStateMachine, + listSelectedEntities, + measureGeomToolStateMachine, + quickCalcToolStateMachine, + radiusToolStateMachine, +} from '../tools/utility/inquiry-tools'; +import { + colorToolStateMachine, + lineTypeToolStateMachine, + lineWeightToolStateMachine, + openPropertiesPalette, + toggleQuickProperties, + transparencyToolStateMachine, +} from '../tools/utility/property-tools'; +import { + groupToolStateMachine, + hideObjectsToolStateMachine, + isolateObjectsToolStateMachine, + qSelectToolStateMachine, + selectSimilarToolStateMachine, + ungroupToolStateMachine, + unhideAllObjects, +} from '../tools/utility/selection-tools'; + +export const PROPERTY_COMMANDS: CadCommand[] = [ + { + id: 'COLOR', + label: '색상', + aliases: ['COL'], + glyph: '🎨', + hint: '선택 객체의 색을 지정한다', + tool: Tool.COLOR, + machine: colorToolStateMachine, + }, + { + id: 'LINETYPE', + label: '선종류', + aliases: ['LT'], + glyph: '┄', + hint: '실선·파선·1점쇄선·점선을 지정한다', + tool: Tool.LINETYPE, + machine: lineTypeToolStateMachine, + }, + { + id: 'LWEIGHT', + label: '선가중치', + aliases: ['LW'], + glyph: '▬', + hint: '선 굵기를 지정한다', + tool: Tool.LWEIGHT, + machine: lineWeightToolStateMachine, + }, + { + id: 'PROPERTIES', + label: '특성', + aliases: ['CH', 'MO', 'PR'], + glyph: '☰', + hint: '선택 객체의 특성을 보고 고친다', + run: openPropertiesPalette, + }, + { + id: 'QUICKPROPERTIES', + label: '빠른 특성', + aliases: ['QP'], + glyph: '⌗', + hint: '선택 객체의 주요 특성만 화면에 띄운다', + run: toggleQuickProperties, + }, + { + id: 'TRANSPARENCY', + label: '투명도', + glyph: '◍', + hint: '선택 객체의 투명도를 지정한다', + tool: Tool.TRANSPARENCY, + machine: transparencyToolStateMachine, + }, +]; + +export const GROUP_COMMANDS: CadCommand[] = [ + { + id: 'GROUP', + label: '그룹', + aliases: ['G'], + glyph: '⧉', + hint: '여러 객체를 함께 선택되는 그룹으로 묶는다', + tool: Tool.GROUP, + machine: groupToolStateMachine, + }, + { + id: 'UNGROUP', + label: '그룹 해제', + glyph: '⧅', + hint: '그룹 묶음을 푼다', + tool: Tool.UNGROUP, + machine: ungroupToolStateMachine, + }, +]; + +export const INQUIRY_COMMANDS: CadCommand[] = [ + { + id: 'DIST', + label: '거리', + aliases: ['DI'], + glyph: '↔', + hint: '두 점 사이의 거리와 각도를 잰다', + tool: Tool.DIST, + machine: distanceToolStateMachine, + }, + { + id: 'RADIUS', + label: '반지름', + glyph: '◜', + hint: '원 또는 호의 반지름을 잰다', + tool: Tool.RADIUS_INQUIRY, + machine: radiusToolStateMachine, + }, + { + id: 'ANGLE', + label: '각도', + glyph: '∠', + hint: '세 점이 이루는 각도를 잰다', + tool: Tool.ANGLE_INQUIRY, + machine: angleToolStateMachine, + }, + { + id: 'AREA', + label: '면적', + aliases: ['AA'], + glyph: '▦', + hint: '닫힌 경계의 면적과 둘레를 계산한다', + tool: Tool.AREA, + machine: areaToolStateMachine, + }, + { + id: 'MEASUREGEOM', + label: '빠른 측정', + aliases: ['MEA'], + glyph: '📏', + hint: '거리·반지름·각도·면적 중 하나를 골라 잰다', + tool: Tool.MEASUREGEOM, + machine: measureGeomToolStateMachine, + }, + { + id: 'ID', + label: '점 좌표', + glyph: '⌖', + hint: '지정한 점의 좌표를 표시한다', + tool: Tool.ID_POINT, + machine: idPointToolStateMachine, + }, + { + id: 'LIST', + label: '리스트', + aliases: ['LI'], + glyph: '≣', + hint: '선택 객체의 종류·길이·크기를 표시한다', + run: listSelectedEntities, + }, + { + id: 'QSELECT', + label: '빠른 선택', + glyph: '⋔', + hint: '객체 유형으로 선택 집합을 만든다', + tool: Tool.QSELECT, + machine: qSelectToolStateMachine, + }, + { + id: 'QUICKCALC', + label: '계산기', + aliases: ['QC'], + glyph: '🖩', + hint: '수식을 계산한다', + tool: Tool.QUICKCALC, + machine: quickCalcToolStateMachine, + }, + { + id: 'SELECTSIMILAR', + label: '유사 선택', + glyph: '⁝⁝', + hint: '같은 종류·색·도면층 객체를 모두 선택한다', + tool: Tool.SELECTSIMILAR, + machine: selectSimilarToolStateMachine, + }, + { + id: 'ISOLATEOBJECTS', + label: '객체 분리', + glyph: '◧', + hint: '선택 객체만 남기고 나머지를 숨긴다', + tool: Tool.ISOLATEOBJECTS, + machine: isolateObjectsToolStateMachine, + }, + { + id: 'HIDEOBJECTS', + label: '객체 숨기기', + glyph: '◌', + hint: '선택 객체를 일시적으로 숨긴다', + tool: Tool.HIDEOBJECTS, + machine: hideObjectsToolStateMachine, + }, + { + id: 'UNISOLATEOBJECTS', + label: '객체 분리 종료', + aliases: ['UNHIDE', 'UNISOLATE'], + glyph: '◉', + hint: '숨긴 객체를 다시 표시한다', + run: unhideAllObjects, + }, +]; + +export const CLIPBOARD_COMMANDS: CadCommand[] = [ + { + id: 'COPYCLIP', + label: '복사', + glyph: '⎘', + hint: '선택 객체를 클립보드로 복사한다', + run: copySelectionToClipboard, + }, + { + id: 'CUTCLIP', + label: '잘라내기', + glyph: '✂', + hint: '선택 객체를 클립보드로 옮긴다', + run: cutSelectionToClipboard, + }, + { + id: 'PASTECLIP', + label: '붙여넣기', + glyph: '📋', + hint: '클립보드 객체를 지정 위치에 붙여넣는다', + tool: Tool.PASTECLIP, + machine: pasteToolStateMachine, + }, + { + id: 'COPYBASE', + label: '기준점 복사', + glyph: '⌖', + hint: '기준점을 지정해 복사한다', + tool: Tool.COPYBASE, + machine: copyBaseToolStateMachine, + }, + { + id: 'PASTEORIG', + label: '원래 좌표로 붙여넣기', + glyph: '⇱', + hint: '복사한 좌표 그대로 붙여넣는다', + run: pasteAtOriginalCoordinates, + }, + { + id: 'PASTEBLOCK', + label: '그룹으로 붙여넣기', + glyph: '❐', + hint: '붙여넣으면서 하나의 그룹으로 묶는다', + run: pasteAsGroup, + }, +]; diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.view.ts b/B07_DesignDetail/openwebcad/src/commands/commands.view.ts new file mode 100644 index 00000000..6f1fd7ad --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/commands.view.ts @@ -0,0 +1,71 @@ +/** 뷰 탭 — 탐색·재생성 명령 (조사표 7절 중 구현분) */ +import { toast } from 'react-toastify'; +import type { CadCommand } from './command.types'; +import { bumpSceneVersion } from '../helpers/scene-version'; +import { getScreenCanvasDrawController, setEntities, getEntities } from '../state'; +import { Tool } from '../tools'; +import { selectToolStateMachine } from '../tools/select-tool'; + +const zoomBy = (factor: number): string => { + const controller = getScreenCanvasDrawController(); + controller.setScreenScale(Math.max(0.01, controller.getScreenScale() * factor)); + return `줌 ${Math.round(controller.getScreenScale() * 100)}%`; +}; + +export const VIEW_COMMANDS: CadCommand[] = [ + { + id: 'SELECT', + label: '선택', + aliases: ['SE'], + glyph: '↖', + hint: '객체를 클릭하거나 선택 사각형으로 고른다', + tool: Tool.SELECT, + machine: selectToolStateMachine, + }, + { + id: 'ZOOM', + label: '범위 줌', + aliases: ['Z'], + glyph: '⛶', + hint: '도면 전체가 보이도록 화면을 맞춘다', + run: () => { + getScreenCanvasDrawController().zoomToFitScreen(); + return '도면 전체 보기'; + }, + }, + { + id: 'ZOOMIN', + label: '확대', + glyph: '+', + run: () => zoomBy(1.25), + }, + { + id: 'ZOOMOUT', + label: '축소', + glyph: '-', + run: () => zoomBy(0.8), + }, + { + id: 'REGEN', + label: '재생성', + aliases: ['RE'], + glyph: '⟳', + hint: '화면 캐시를 버리고 객체를 다시 그린다', + run: () => { + bumpSceneVersion(); + setEntities([...getEntities()], false); + return '화면 재생성'; + }, + }, + { + id: 'PAN', + label: '초점이동', + aliases: ['P'], + glyph: '✋', + hint: '휠 버튼을 누른 채 끌면 화면이 움직인다', + run: () => { + toast.info('휠 버튼을 누른 채 끌면 화면이 이동합니다.'); + return '초점이동 안내'; + }, + }, +]; diff --git a/B07_DesignDetail/openwebcad/src/commands/dim-settings.ts b/B07_DesignDetail/openwebcad/src/commands/dim-settings.ts new file mode 100644 index 00000000..d8b8ef85 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/dim-settings.ts @@ -0,0 +1,40 @@ +/** + * 치수·지시선·표 스타일 설정 (DIMSTYLE·MLEADERSTYLE·TABLESTYLE·주석 축척). + * 기본값은 App.consts의 상수를 그대로 쓰고, 명령이 바꾸면 그 뒤로 반영된다. + */ +import { + ARROW_HEAD_LENGTH, + MEASUREMENT_DECIMAL_PLACES, + MEASUREMENT_FONT_SIZE, +} from '../App.consts'; + +let dimTextHeight = MEASUREMENT_FONT_SIZE; +let dimArrowSize = ARROW_HEAD_LENGTH; +let dimDecimals = MEASUREMENT_DECIMAL_PLACES; +/** 주석 축척 — 치수·문자·화살표 크기에 곱한다 */ +let annotationScale = 1; + +let tableColumnWidth = 40; +let tableRowHeight = 10; + +export const getDimTextHeight = () => dimTextHeight * annotationScale; +export const getDimArrowSize = () => dimArrowSize * annotationScale; +export const getDimDecimals = () => dimDecimals; +export const getAnnotationScale = () => annotationScale; +export const getTableColumnWidth = () => tableColumnWidth; +export const getTableRowHeight = () => tableRowHeight; + +export function setDimStyle(textHeight: number, arrowSize: number, decimals: number): void { + if (textHeight > 0) dimTextHeight = textHeight; + if (arrowSize > 0) dimArrowSize = arrowSize; + if (decimals >= 0) dimDecimals = Math.min(6, Math.round(decimals)); +} + +export function setAnnotationScale(scale: number): void { + if (scale > 0) annotationScale = scale; +} + +export function setTableStyle(columnWidth: number, rowHeight: number): void { + if (columnWidth > 0) tableColumnWidth = columnWidth; + if (rowHeight > 0) tableRowHeight = rowHeight; +} diff --git a/B07_DesignDetail/openwebcad/src/commands/draw-settings.ts b/B07_DesignDetail/openwebcad/src/commands/draw-settings.ts new file mode 100644 index 00000000..f33a3e09 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/draw-settings.ts @@ -0,0 +1,36 @@ +/** + * 그리기 명령의 설정값 — AutoCAD의 MLSTYLE·HATCH 대화상자에 해당하는 상태. + * 명령이 물어보는 값 중 "매번 묻지 않는 것"만 여기에 둔다. + */ +import type { HatchStyle } from '../entities/HatchEntity'; + +let mlineElements = 2; +let mlineSpacing = 1; +let hatchStyle: HatchStyle = 'pattern'; +/** null이면 경계 크기에 맞춰 자동 계산한다 */ +let hatchSpacing: number | null = null; +let hatchAngle = Math.PI / 4; + +export const getMlineElements = () => mlineElements; +export const getMlineSpacing = () => mlineSpacing; +export const setMlineStyle = (elements: number, spacing: number) => { + mlineElements = Math.max(2, Math.round(elements)); + mlineSpacing = Math.abs(spacing) || 1; +}; + +export const getHatchStyle = () => hatchStyle; +export const getHatchSpacing = () => hatchSpacing; +export const getHatchAngle = () => hatchAngle; +export const setHatchStyle = (style: HatchStyle) => { + hatchStyle = style; +}; +export const setHatchSpacing = (spacing: number | null) => { + hatchSpacing = spacing; +}; +export const setHatchAngle = (angleRad: number) => { + hatchAngle = angleRad; +}; + +/** 경계 크기에 맞춘 기본 해치 간격 — 도면 축척이 달라도 눈에 보이게 한다 */ +export const autoHatchSpacing = (width: number, height: number): number => + Math.max(Math.hypot(width, height) / 30, 1e-6); diff --git a/B07_DesignDetail/openwebcad/src/commands/registry.ts b/B07_DesignDetail/openwebcad/src/commands/registry.ts new file mode 100644 index 00000000..9b976b78 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/registry.ts @@ -0,0 +1,95 @@ +/** + * 명령 레지스트리 — 리본·명령행·단축키가 읽는 단일 소스. + * 새 명령은 commands.*.ts에 한 줄 추가하면 세 곳에 동시에 나타난다. + */ +import type { AnyToolMachine, CadCommand } from './command.types'; +import type { Tool } from '../tools'; +import { + DIMENSION_COMMANDS, + LEADER_COMMANDS, + MARKUP_COMMANDS, + TABLE_COMMANDS, + TEXT_COMMANDS, +} from './commands.annotate'; +import { DRAW_COMMANDS } from './commands.draw'; +import { FILE_COMMANDS } from './commands.file'; +import { INSERT_COMMANDS } from './commands.insert'; +import { LAYER_COMMANDS } from './commands.layer'; +import { + CLIPBOARD_COMMANDS, + GROUP_COMMANDS, + INQUIRY_COMMANDS, + PROPERTY_COMMANDS, +} from './commands.utility'; +import { ALIGN_COMMANDS, MODIFY_COMMANDS } from './commands.modify'; +import { VIEW_COMMANDS } from './commands.view'; + +const ALL_COMMANDS: CadCommand[] = [ + ...DRAW_COMMANDS, + ...MODIFY_COMMANDS, + ...ALIGN_COMMANDS, + ...TEXT_COMMANDS, + ...DIMENSION_COMMANDS, + ...LEADER_COMMANDS, + ...TABLE_COMMANDS, + ...MARKUP_COMMANDS, + ...LAYER_COMMANDS, + ...PROPERTY_COMMANDS, + ...GROUP_COMMANDS, + ...INQUIRY_COMMANDS, + ...CLIPBOARD_COMMANDS, + ...INSERT_COMMANDS, + ...VIEW_COMMANDS, + ...FILE_COMMANDS, +]; + +const COMMANDS_BY_ID = new Map( + ALL_COMMANDS.map((command) => [command.id, command]) +); + +const COMMANDS_BY_ALIAS = new Map(); +for (const command of ALL_COMMANDS) { + for (const alias of command.aliases ?? []) { + if (!COMMANDS_BY_ALIAS.has(alias)) { + COMMANDS_BY_ALIAS.set(alias, command); + } + } +} + +export const getAllCommands = (): CadCommand[] => ALL_COMMANDS; + +export const getCommandById = (id: string): CadCommand | undefined => COMMANDS_BY_ID.get(id); + +/** 활성 도구로 어떤 명령이 실행 중인지 되짚는다 (리본 강조·상태 표시용) */ +export const getCommandByTool = (tool: Tool | null): CadCommand | undefined => + tool ? ALL_COMMANDS.find((command) => command.tool === tool) : undefined; + +/** 도구형 명령의 xstate 머신 표 — 도구 활성화 경로가 이 표만 본다 */ +export const TOOL_STATE_MACHINES = Object.fromEntries( + ALL_COMMANDS.filter((command) => command.tool && command.machine).map((command) => [ + command.tool, + command.machine, + ]) +) as Record; + +/** 명령행 입력 해석 — 정확한 이름·별칭이 먼저, 없으면 접두사 후보의 첫 항목 */ +export function resolveCommandInput(input: string): CadCommand | undefined { + const text = input.trim().toUpperCase(); + if (!text) return undefined; + return COMMANDS_BY_ID.get(text) ?? COMMANDS_BY_ALIAS.get(text) ?? matchCommandPrefixes(text)[0]; +} + +/** 접두사가 같은 명령 후보 (커서 옆 자동완성 목록에 쓴다) */ +export function matchCommandPrefixes(input: string): CadCommand[] { + const text = input.trim().toUpperCase(); + if (!text) return []; + const byAlias = ALL_COMMANDS.filter((command) => + (command.aliases ?? []).some((alias) => alias.startsWith(text)) + ); + const byId = ALL_COMMANDS.filter((command) => command.id.startsWith(text)); + return [...new Set([...byAlias, ...byId])]; +} + +/** 명령행 자동완성에 보여줄 문자열 (예: "LINE (L) 선") */ +export const describeCommand = (command: CadCommand): string => + `${command.id}${command.aliases?.length ? ` (${command.aliases[0]})` : ''} ${command.label}`; diff --git a/B07_DesignDetail/openwebcad/src/commands/run-command.ts b/B07_DesignDetail/openwebcad/src/commands/run-command.ts new file mode 100644 index 00000000..89f486e3 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/run-command.ts @@ -0,0 +1,61 @@ +/** 명령 실행기 — 리본 버튼·명령행·단축키가 모두 이 경로로 들어온다. */ +import { toast } from 'react-toastify'; +import { Actor } from 'xstate'; +import type { CadCommand } from './command.types'; +import { getCommandById, resolveCommandInput } from './registry'; +import { HtmlEvent } from '../App.types'; +import { getSelectedEntities, setActiveToolActor } from '../state'; + +const COMMAND_HISTORY_LIMIT = 200; + +let lastCommandId: string | null = null; +const commandHistory: string[] = []; + +export const getLastCommandId = (): string | null => lastCommandId; +export const getCommandHistory = (): string[] => commandHistory; + +function log(line: string) { + commandHistory.push(line); + if (commandHistory.length > COMMAND_HISTORY_LIMIT) { + commandHistory.splice(0, commandHistory.length - COMMAND_HISTORY_LIMIT); + } + window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE)); +} + +/** 명령 한 건 실행. 도구형이면 도구를 활성화하고, 즉시형이면 run()을 부른다. */ +export function runCommand(command: CadCommand): string { + if (command.needsSelection && getSelectedEntities().length === 0) { + toast.info(`${command.label}: 객체를 먼저 선택하십시오.`); + log(`${command.id}: 선택 없음`); + return ''; + } + + lastCommandId = command.id; + + if (command.machine) { + setActiveToolActor(new Actor(command.machine)); + log(`${command.id} ${command.label}`); + return command.id; + } + + const result = command.run?.(); + log(`${command.id} ${typeof result === 'string' ? result : command.label}`); + return typeof result === 'string' ? result : command.id; +} + +/** 명령 이름·별칭 문자열로 실행 (명령행 입력) */ +export function runCommandInput(input: string): boolean { + const command = resolveCommandInput(input); + if (!command) return false; + runCommand(command); + return true; +} + +/** 직전 명령 반복 (AutoCAD의 빈 ENTER 동작) */ +export function repeatLastCommand(): boolean { + if (!lastCommandId) return false; + const command = getCommandById(lastCommandId); + if (!command) return false; + runCommand(command); + return true; +} diff --git a/B07_DesignDetail/openwebcad/src/components/CommandLine.tsx b/B07_DesignDetail/openwebcad/src/components/CommandLine.tsx new file mode 100644 index 00000000..8a3c73e7 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/components/CommandLine.tsx @@ -0,0 +1,98 @@ +/** 명령행 — AutoCAD 명령행처럼 기록·입력·자동완성을 담당한다. */ +import { type FC, type FormEvent, useMemo, useRef, useState } from 'react'; +import { describeCommand, matchCommandPrefixes } from '../commands/registry'; +import { getCommandHistory, runCommandInput } from '../commands/run-command'; +import { getInputController, getLastStateInstructions } from '../state'; + +export const CommandLine: FC = () => { + const [text, setText] = useState(''); + const [historyIndex, setHistoryIndex] = useState(null); + const inputRef = useRef(null); + const history = getCommandHistory(); + const instruction = getLastStateInstructions() || '명령을 입력하거나 도구를 선택하십시오.'; + + const suggestions = useMemo(() => matchCommandPrefixes(text).slice(0, 8), [text]); + + const submit = (event: FormEvent) => { + event.preventDefault(); + const value = text.trim(); + if (!value) { + // 빈 ENTER는 활성 명령의 확정 신호다 (선택 끝내기·직전 명령 반복) + getInputController().handleEnterKey(); + return; + } + // 진행 중인 명령이 문자·숫자를 기다리면 그쪽으로, 아니면 명령으로 해석된다 + getInputController().submitText(value); + setText(''); + setHistoryIndex(null); + }; + + const recallHistory = (direction: -1 | 1) => { + if (!history.length) return; + const nextIndex = + historyIndex === null + ? history.length - 1 + : Math.min(history.length - 1, Math.max(0, historyIndex + direction)); + setHistoryIndex(nextIndex); + setText(history[nextIndex].split(' ')[0]); + }; + + return ( +
+
+ {history.slice(-3).map((line, index) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: 기록은 순서 자체가 식별자다 + {line} + ))} +
+
+ + setText(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Escape') { + event.preventDefault(); + getInputController().handleEscapeKey(); + } + if (event.key === 'ArrowUp') { + event.preventDefault(); + recallHistory(-1); + } + if (event.key === 'ArrowDown') { + event.preventDefault(); + recallHistory(1); + } + if (event.key === 'Tab' && suggestions.length) { + event.preventDefault(); + setText(suggestions[0].id); + } + }} + placeholder="명령 입력 (예: L, LINE, REC, MOVE)" + autoComplete="off" + spellCheck={false} + /> + {instruction} +
+ {text.trim() && suggestions.length > 0 && ( +
+ {suggestions.map((command) => ( + + ))} +
+ )} +
+ ); +}; diff --git a/B07_DesignDetail/openwebcad/src/components/InspectorPanel.tsx b/B07_DesignDetail/openwebcad/src/components/InspectorPanel.tsx new file mode 100644 index 00000000..0eb76091 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/components/InspectorPanel.tsx @@ -0,0 +1,68 @@ +/** 좌측 팔레트 — 특성(PROPERTIES)과 도면층 관리자 (AutoCAD 팔레트 자리) */ +import type { FC } from 'react'; +import { LayerManager } from './LayerManager'; +import { PropertiesEditor } from './PropertiesEditor'; +import { getInspectorTab, openInspector } from './ui-state'; +import { + getActiveLayerId, + getLayers, + setActiveLayerId, + setLayers, +} from '../state'; + +interface InspectorPanelProps { + collapsed: boolean; + onToggleCollapsed: () => void; +} + +export const InspectorPanel: FC = ({ collapsed, onToggleCollapsed }) => { + const tab = getInspectorTab(); + const layers = getLayers(); + const activeLayerId = getActiveLayerId(); + + return ( + + ); +}; diff --git a/B07_DesignDetail/openwebcad/src/components/LayerManager.tsx b/B07_DesignDetail/openwebcad/src/components/LayerManager.tsx index c0962b7b..6b2c865a 100644 --- a/B07_DesignDetail/openwebcad/src/components/LayerManager.tsx +++ b/B07_DesignDetail/openwebcad/src/components/LayerManager.tsx @@ -93,7 +93,7 @@ export const LayerManager: FC = ({ title="Set this layer as active" active={activeLayerId === layer.id} onClick={(evt) => handleLayerClick(evt, layer.id)} - className="flex-grow" + className="flex-grow min-w-0 overflow-hidden" left={ <> + ); + })} + +
+ + 현재 도면 +
+ +); diff --git a/B07_DesignDetail/openwebcad/src/components/QuickProperties.tsx b/B07_DesignDetail/openwebcad/src/components/QuickProperties.tsx new file mode 100644 index 00000000..b4f5c47f --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/components/QuickProperties.tsx @@ -0,0 +1,21 @@ +/** 빠른 특성 — 선택이 있을 때만 화면 오른쪽 위에 뜨는 간이 특성 상자 (QUICKPROPERTIES) */ +import type { FC } from 'react'; +import { PropertiesEditor } from './PropertiesEditor'; +import { getSelectedEntities } from '../state'; +import { isQuickPropertiesVisible, setQuickPropertiesVisible } from './ui-state'; + +export const QuickProperties: FC = () => { + if (!isQuickPropertiesVisible() || getSelectedEntities().length === 0) return null; + + return ( +
+
+ 빠른 특성 + +
+ +
+ ); +}; diff --git a/B07_DesignDetail/openwebcad/src/components/RibbonWidgets.tsx b/B07_DesignDetail/openwebcad/src/components/RibbonWidgets.tsx new file mode 100644 index 00000000..1b05d47a --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/components/RibbonWidgets.tsx @@ -0,0 +1,191 @@ +/** 리본 안에 들어가는 위젯 패널 — 특성(색·굵기·선종류), 도면층, 문자 스타일 */ +import type { FC } from 'react'; +import type { Entity } from '../entities/Entity'; +import { EntityName } from '../entities/Entity'; +import type { TextEntity } from '../entities/TextEntity'; +import { + getActiveLayerId, + getActiveLineColor, + getActiveLineDash, + getActiveLineWidth, + getActiveTextStyle, + getEntities, + getLayers, + getSelectedEntities, + setActiveLayerId, + setActiveLineColor, + setActiveLineDash, + setActiveLineWidth, + setActiveTextStyle, + setEntities, +} from '../state'; +import { runCommandInput } from '../commands/run-command'; + +export const LINE_TYPES: { value: string; label: string; dash: number[] | undefined }[] = [ + { value: 'solid', label: '실선', dash: undefined }, + { value: 'dashed', label: '파선', dash: [10, 5] }, + { value: 'dashdot', label: '1점쇄선', dash: [12, 4, 2, 4] }, + { value: 'dotted', label: '점선', dash: [2, 4] }, +]; + +export const LINE_WIDTHS = [1, 2, 3, 4, 5]; + +const FONT_FAMILIES = ['Noto Sans KR', 'Malgun Gothic', 'Pretendard', 'Arial', 'monospace']; + +export const dashToLineType = (dash: number[] | undefined): string => + LINE_TYPES.find((type) => JSON.stringify(type.dash) === JSON.stringify(dash))?.value ?? 'solid'; + +/** 선택 객체가 있으면 즉시 적용하고, 없으면 이후 그리기 기본값만 바꾼다. */ +function applyToSelection(mutate: (entity: Entity) => void): boolean { + const selected = getSelectedEntities(); + if (!selected.length) return false; + for (const entity of selected) { + mutate(entity); + } + setEntities([...getEntities()], true); + return true; +} + +export const PropertiesWidget: FC = () => { + const lineColor = getActiveLineColor(); + const lineWidth = getActiveLineWidth(); + const lineType = dashToLineType(getActiveLineDash()); + + return ( +
+ + + +
+ ); +}; + +export const LayersWidget: FC = () => { + const layers = getLayers(); + const activeLayerId = getActiveLayerId(); + + return ( +
+ + +
+ ); +}; + +export const TextStyleWidget: FC = () => { + const textStyle = getActiveTextStyle(); + + const handle = (patch: Partial) => { + setActiveTextStyle(patch); + applyToSelection((entity) => { + if (entity.getType() === EntityName.Text) { + (entity as TextEntity).setTextOptions(patch); + } + }); + }; + + return ( +
+ + + +
+ ); +}; diff --git a/B07_DesignDetail/openwebcad/src/components/StatusBar.tsx b/B07_DesignDetail/openwebcad/src/components/StatusBar.tsx new file mode 100644 index 00000000..8183eb9f --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/components/StatusBar.tsx @@ -0,0 +1,67 @@ +/** 상태막대 — AutoCAD 하단 제도 보조 토글 */ +import type { FC } from 'react'; +import { + getAngleStep, + getGridEnabled, + getSnapEnabled, + setAngleStep, + setGridEnabled, + setSnapEnabled, +} from '../state'; + +interface StatusBarProps { + commandLineVisible: boolean; + onToggleCommandLine: () => void; +} + +export const StatusBar: FC = ({ commandLineVisible, onToggleCommandLine }) => { + const snap = getSnapEnabled(); + const grid = getGridEnabled(); + const ortho = getAngleStep() === 90; + + return ( +
+ + + + + + 휠: 줌 · 휠 드래그: 초점이동 · Esc: 취소 +
+ ); +}; diff --git a/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx b/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx index e10d1b97..f3257f05 100644 --- a/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx +++ b/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx @@ -1,483 +1,61 @@ -import { type FC, type FormEvent, useCallback, useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; -import { Actor } from 'xstate'; -import { HtmlEvent, type Layer } from '../App.types'; -import { exportEntitiesToJsonFile } from '../helpers/import-export-handlers/export-entities-to-json'; -import { exportEntitiesToLocalStorage } from '../helpers/import-export-handlers/export-entities-to-local-storage'; -import type { Entity } from '../entities/Entity'; -import { EntityName } from '../entities/Entity'; -import { TextEntity } from '../entities/TextEntity'; -import { - getActiveLayerId, - getActiveLineColor, - getActiveLineDash, - getActiveLineWidth, - getActiveTextStyle, - getActiveToolActor, - getAngleStep, - getEntities, - getGridEnabled, - getInputController, - getLastStateInstructions, - getLayers, - getScreenCanvasDrawController, - getSelectedEntities, - getSnapEnabled, - redo, - setActiveLayerId, - setActiveLineColor, - setActiveLineDash, - setActiveLineWidth, - setActiveTextStyle, - setActiveToolActor, - setAngleStep, - setEntities, - setGridEnabled, - setLayers, - setSnapEnabled, - undo, -} from '../state'; -import { Tool } from '../tools'; -import { TOOL_STATE_MACHINES } from '../tools/tool.consts'; -import { LayerManager } from './LayerManager'; - -interface RibbonTool { - label: string; - shortcut?: string; - tool?: Tool; - glyph: string; - disabled?: boolean; -} - -const RIBBON_GROUPS: { label: string; tools: RibbonTool[] }[] = [ - { - label: '그리기', - tools: [ - { label: '선', shortcut: 'L', tool: Tool.LINE, glyph: '╱' }, - { label: '폴리선', shortcut: 'PE', tool: Tool.PEDIT, glyph: '⌁' }, - { label: '원', shortcut: 'C', tool: Tool.CIRCLE, glyph: '○' }, - { label: '사각형', shortcut: 'R', tool: Tool.RECTANGLE, glyph: '□' }, - { label: '호', glyph: '◜', disabled: true }, - ], - }, - { - label: '수정', - tools: [ - { label: '선택', shortcut: 'S', tool: Tool.SELECT, glyph: '↖' }, - { label: '이동', tool: Tool.MOVE, glyph: '✥' }, - { label: '복사', tool: Tool.COPY, glyph: '▣' }, - { label: '회전', tool: Tool.ROTATE, glyph: '↻' }, - { label: '자르기', tool: Tool.ERASER, glyph: '⌫' }, - { label: '간격띄우기', glyph: '⇶', disabled: true }, - ], - }, - { - label: '주석', - tools: [ - { label: '치수', tool: Tool.MEASUREMENT, glyph: '↔' }, - { label: '문자', glyph: 'A', disabled: true }, - ], - }, -]; - -const COMMANDS = Object.values(Tool); - -const LINE_TYPES: { value: string; label: string; dash: number[] | undefined }[] = [ - { value: 'solid', label: '실선', dash: undefined }, - { value: 'dashed', label: '파선', dash: [10, 5] }, - { value: 'dashdot', label: '1점쇄선', dash: [12, 4, 2, 4] }, - { value: 'dotted', label: '점선', dash: [2, 4] }, -]; - -const LINE_WIDTHS = [1, 2, 3, 4, 5]; - -const FONT_FAMILIES = ['Noto Sans KR', 'Malgun Gothic', 'Pretendard', 'Arial', 'monospace']; - -const dashToLineType = (dash: number[] | undefined): string => - LINE_TYPES.find((type) => JSON.stringify(type.dash) === JSON.stringify(dash))?.value ?? 'solid'; +/** + * CAD 화면 골격 조립 — 제목표시줄 · 리본 · 좌측 팔레트 · 탐색막대 · 명령행 · 상태막대. + * 각 조각은 자기 파일에 있고, 여기서는 배치와 표시 상태만 다룬다. + */ +import { type FC, useEffect, useState } from 'react'; +import { CommandLine } from './CommandLine'; +import { InspectorPanel } from './InspectorPanel'; +import { QuickProperties } from './QuickProperties'; +import { LayersWidget, PropertiesWidget, TextStyleWidget } from './RibbonWidgets'; +import { QuickAccessBar } from './QuickAccessBar'; +import { Ribbon } from '../ribbon/Ribbon'; +import { StatusBar } from './StatusBar'; +import { useCadRefresh } from './use-cad-refresh'; +import { getActiveToolActor } from '../state'; +import type { Tool } from '../tools'; +import { ViewControls } from './ViewControls'; export const Toolbar: FC = () => { - const [activeTool, setActiveTool] = useState(Tool.LINE); - const [zoom, setZoom] = useState(1); - const [layers, setLayersLocal] = useState(getLayers()); - const [activeLayerId, setActiveLayerIdLocal] = useState(getActiveLayerId()); - const [selectedCount, setSelectedCount] = useState(0); - const [selectedType, setSelectedType] = useState('선택 없음'); - const [instruction, setInstruction] = useState('명령을 입력하거나 도구를 선택하십시오.'); - const [panelTab, setPanelTab] = useState<'properties' | 'layers'>('layers'); + useCadRefresh(); const [panelCollapsed, setPanelCollapsed] = useState(false); - const [snap, setSnap] = useState(getSnapEnabled()); - const [grid, setGrid] = useState(getGridEnabled()); - const [ortho, setOrtho] = useState(getAngleStep() === 90); - const [command, setCommand] = useState(''); - const [commandLog, setCommandLog] = useState('준비'); - const [lineColor, setLineColorLocal] = useState(getActiveLineColor()); - const [lineWidth, setLineWidthLocal] = useState(getActiveLineWidth()); - const [lineType, setLineTypeLocal] = useState(dashToLineType(getActiveLineDash())); - const [textStyle, setTextStyleLocal] = useState(getActiveTextStyle()); + const [commandLineVisible, setCommandLineVisible] = useState(true); - const refresh = useCallback(() => { - setActiveTool(getActiveToolActor()?.getSnapshot()?.context.type ?? Tool.LINE); - setZoom(getScreenCanvasDrawController().getScreenScale()); - setLayersLocal([...getLayers()]); - setActiveLayerIdLocal(getActiveLayerId()); - const selected = getSelectedEntities(); - setSelectedCount(selected.length); - setSelectedType( - selected.length === 1 ? selected[0].getType() : selected.length ? '여러 객체' : '선택 없음' - ); - setInstruction(getLastStateInstructions() || '명령을 입력하거나 도구를 선택하십시오.'); - setSnap(getSnapEnabled()); - setGrid(getGridEnabled()); - setOrtho(getAngleStep() === 90); - setLineColorLocal(getActiveLineColor()); - setLineWidthLocal(getActiveLineWidth()); - setLineTypeLocal(dashToLineType(getActiveLineDash())); - setTextStyleLocal({ ...getActiveTextStyle() }); - }, []); - - useEffect(() => { - window.addEventListener(HtmlEvent.UPDATE_STATE, refresh); - return () => window.removeEventListener(HtmlEvent.UPDATE_STATE, refresh); - }, [refresh]); + const activeTool = (getActiveToolActor()?.getSnapshot()?.context?.type ?? null) as Tool | null; useEffect(() => { document.documentElement.style.setProperty( '--cad-panel-width', panelCollapsed ? '0px' : '248px' ); + document.documentElement.style.setProperty( + '--cad-command-height', + commandLineVisible ? '86px' : '0px' + ); window.dispatchEvent(new Event('resize')); - }, [panelCollapsed]); - - const activateTool = useCallback((tool: Tool) => { - const actor = new Actor(TOOL_STATE_MACHINES[tool]); - setActiveToolActor(actor); - setActiveTool(tool); - setCommandLog(`${tool} 명령 실행`); - }, []); - - const handleCommand = (event: FormEvent) => { - event.preventDefault(); - const value = command.trim(); - if (!value) return; - getInputController().submitText(value); - setCommandLog(`명령: ${value.toUpperCase()}`); - setCommand(''); - }; - - const changeZoom = (factor: number) => { - const controller = getScreenCanvasDrawController(); - controller.setScreenScale(Math.max(0.05, controller.getScreenScale() * factor)); - setZoom(controller.getScreenScale()); - }; - - /** 선택 객체가 있으면 스타일을 즉시 적용하고, 없으면 이후 그리기 기본값만 바꾼다. */ - const applyToSelection = useCallback((mutate: (entity: Entity) => void): boolean => { - const selected = getSelectedEntities(); - if (!selected.length) return false; - for (const entity of selected) { - mutate(entity); - } - setEntities([...getEntities()], true); - return true; - }, []); - - const handleLineColor = (color: string) => { - setActiveLineColor(color); - setLineColorLocal(color); - if (applyToSelection((entity) => (entity.lineColor = color))) { - setCommandLog('선택 객체 색상 변경'); - } - }; - - const handleLineWidth = (width: number) => { - setActiveLineWidth(width); - setLineWidthLocal(width); - if (applyToSelection((entity) => (entity.lineWidth = width))) { - setCommandLog('선택 객체 선굵기 변경'); - } - }; - - const handleLineType = (value: string) => { - const dash = LINE_TYPES.find((type) => type.value === value)?.dash; - setActiveLineDash(dash ? [...dash] : undefined); - setLineTypeLocal(value); - if (applyToSelection((entity) => (entity.lineDash = dash ? [...dash] : undefined))) { - setCommandLog('선택 객체 선종류 변경'); - } - }; - - const handleTextStyle = (patch: Partial) => { - setActiveTextStyle(patch); - setTextStyleLocal((previous) => ({ ...previous, ...patch })); - const applied = applyToSelection((entity) => { - if (entity.getType() === EntityName.Text) { - (entity as TextEntity).setTextOptions(patch); - } - }); - if (applied) { - setCommandLog('선택 문자 스타일 변경'); - } - }; + }, [panelCollapsed, commandLineVisible]); return ( <> -
-
- Aislo CAD - B07 상세 설계 -
-
- - 현재 도면 · 저장됨 -
-
- - - - -
-
- - - - - -
- - - {Math.round(zoom * 100)}% - -
- -
-
- {commandLog} - {instruction} -
-
- - setCommand(event.target.value)} - placeholder="명령 입력 (예: LINE, MOVE, CIRCLE)" - autoComplete="off" - /> - - {COMMANDS.map((item) => ( - -
-
- -
- - - - 휠: 줌 · 휠 드래그: 팬 · Esc: 취소 -
+ + , + layers: , + textStyle: , + }} + /> + setPanelCollapsed((value) => !value)} + /> + + + {commandLineVisible && } + setCommandLineVisible((value) => !value)} + /> ); }; diff --git a/B07_DesignDetail/openwebcad/src/components/ViewControls.tsx b/B07_DesignDetail/openwebcad/src/components/ViewControls.tsx new file mode 100644 index 00000000..6b0b707a --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/components/ViewControls.tsx @@ -0,0 +1,28 @@ +/** 화면 우하단 탐색 막대 (줌 표시·전체 보기) */ +import type { FC } from 'react'; +import { runCommandInput } from '../commands/run-command'; +import { getScreenCanvasDrawController } from '../state'; + +export const ViewControls: FC = () => { + const zoom = getScreenCanvasDrawController()?.getScreenScale() ?? 1; + + return ( +
+ + + {Math.round(zoom * 100)}% + +
+ ); +}; diff --git a/B07_DesignDetail/openwebcad/src/components/ui-state.ts b/B07_DesignDetail/openwebcad/src/components/ui-state.ts new file mode 100644 index 00000000..fe5f041c --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/components/ui-state.ts @@ -0,0 +1,40 @@ +/** + * 화면 표시 상태 — 명령(레지스트리)에서도 팔레트를 열 수 있어야 해서 + * 리액트 컴포넌트 바깥에 둔다. 값이 바뀌면 UPDATE_STATE로 다시 그린다. + */ +import { HtmlEvent } from '../App.types'; + +export type InspectorTab = 'properties' | 'layers'; + +let inspectorTab: InspectorTab = 'layers'; +let inspectorCollapsed = false; +let quickPropertiesVisible = false; +let activeRibbonTab = 'home'; + +const notify = () => window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE)); + +export const getInspectorTab = () => inspectorTab; +export const isInspectorCollapsed = () => inspectorCollapsed; +export const isQuickPropertiesVisible = () => quickPropertiesVisible; +export const getActiveRibbonTab = () => activeRibbonTab; + +export function setActiveRibbonTab(tabId: string): void { + activeRibbonTab = tabId; + notify(); +} + +export function openInspector(tab: InspectorTab): void { + inspectorTab = tab; + inspectorCollapsed = false; + notify(); +} + +export function setInspectorCollapsed(collapsed: boolean): void { + inspectorCollapsed = collapsed; + notify(); +} + +export function setQuickPropertiesVisible(visible: boolean): void { + quickPropertiesVisible = visible; + notify(); +} diff --git a/B07_DesignDetail/openwebcad/src/components/use-cad-refresh.ts b/B07_DesignDetail/openwebcad/src/components/use-cad-refresh.ts new file mode 100644 index 00000000..7b00eb30 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/components/use-cad-refresh.ts @@ -0,0 +1,18 @@ +import { useEffect, useState } from 'react'; +import { HtmlEvent } from '../App.types'; + +/** + * CAD 상태(state.ts)는 리액트 밖에 있다. UPDATE_STATE 이벤트가 올 때마다 + * 카운터를 올려 컴포넌트가 getter를 다시 읽게 한다. + */ +export function useCadRefresh(): number { + const [tick, setTick] = useState(0); + + useEffect(() => { + const handler = () => setTick((value) => value + 1); + window.addEventListener(HtmlEvent.UPDATE_STATE, handler); + return () => window.removeEventListener(HtmlEvent.UPDATE_STATE, handler); + }, []); + + return tick; +} diff --git a/B07_DesignDetail/openwebcad/src/drawControllers/DrawController.ts b/B07_DesignDetail/openwebcad/src/drawControllers/DrawController.ts index 5d210b8f..11275dcb 100644 --- a/B07_DesignDetail/openwebcad/src/drawControllers/DrawController.ts +++ b/B07_DesignDetail/openwebcad/src/drawControllers/DrawController.ts @@ -19,6 +19,8 @@ export interface DrawController { dash?: number[], ): void; setFillStyles(fillColor: string): void; + /** 0~1. 도면층·객체 투명도를 그릴 때 반영한다 */ + setOpacity(opacity: number): void; clear(): void; drawLine(startPoint: Point, endPoint: Point): void; drawArc( diff --git a/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts b/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts index 4d3fcd71..b2532e21 100644 --- a/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts +++ b/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts @@ -304,6 +304,11 @@ export class ScreenCanvasDrawController implements DrawController { this.batchLastY = Number.NaN; } + /** 도면층·객체 투명도 (1이면 불투명) */ + public setOpacity(opacity: number) { + this.context.globalAlpha = Math.min(1, Math.max(0, opacity)); + } + public setFillStyles(fillColor: string) { this.context.fillStyle = paintColor(fillColor); } diff --git a/B07_DesignDetail/openwebcad/src/drawControllers/svg.drawController.ts b/B07_DesignDetail/openwebcad/src/drawControllers/svg.drawController.ts index 51343268..f15d3271 100644 --- a/B07_DesignDetail/openwebcad/src/drawControllers/svg.drawController.ts +++ b/B07_DesignDetail/openwebcad/src/drawControllers/svg.drawController.ts @@ -117,6 +117,9 @@ export class SvgDrawController implements DrawController { this.lineDash = lineDash; } + /** SVG 내보내기는 투명도를 쓰지 않는다 (출력은 항상 불투명) */ + public setOpacity(_opacity: number) {} + public setFillStyles(fillColor: string) { if ( fillColor.toLowerCase() === '#fff' || diff --git a/B07_DesignDetail/openwebcad/src/entities/ArcEntity.ts b/B07_DesignDetail/openwebcad/src/entities/ArcEntity.ts index f8b42890..f5e9e49c 100644 --- a/B07_DesignDetail/openwebcad/src/entities/ArcEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/ArcEntity.ts @@ -22,6 +22,10 @@ export class ArcEntity implements Entity, StartAndEndpointEntity { public lineWidth = 1; public lineDash: number[] | undefined = undefined; public layerId: string; + /** 0~1 객체 투명도 (Entity 인터페이스의 선택 항목) */ + public opacity?: number; + /** GROUP으로 묶인 객체가 공유하는 식별자 */ + public groupId?: string; private arc: Arc; diff --git a/B07_DesignDetail/openwebcad/src/entities/ArrowHeadEntity.ts b/B07_DesignDetail/openwebcad/src/entities/ArrowHeadEntity.ts index 63a58116..a96ae6aa 100644 --- a/B07_DesignDetail/openwebcad/src/entities/ArrowHeadEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/ArrowHeadEntity.ts @@ -15,6 +15,10 @@ export class ArrowHeadEntity implements Entity { public lineWidth = 1; public lineDash: number[] = []; public layerId: string; + /** 0~1 객체 투명도 (Entity 인터페이스의 선택 항목) */ + public opacity?: number; + /** GROUP으로 묶인 객체가 공유하는 식별자 */ + public groupId?: string; // 3 corners of the arrow head constructor( diff --git a/B07_DesignDetail/openwebcad/src/entities/CircleEntity.ts b/B07_DesignDetail/openwebcad/src/entities/CircleEntity.ts index 9beee87b..6df5c05a 100644 --- a/B07_DesignDetail/openwebcad/src/entities/CircleEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/CircleEntity.ts @@ -14,6 +14,10 @@ export class CircleEntity implements Entity { public lineWidth = 1; public lineDash: number[] | undefined = undefined; public layerId: string; + /** 0~1 객체 투명도 (Entity 인터페이스의 선택 항목) */ + public opacity?: number; + /** GROUP으로 묶인 객체가 공유하는 식별자 */ + public groupId?: string; private circle: Circle; diff --git a/B07_DesignDetail/openwebcad/src/entities/Entity.ts b/B07_DesignDetail/openwebcad/src/entities/Entity.ts index 2a8eb6af..3404c667 100644 --- a/B07_DesignDetail/openwebcad/src/entities/Entity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/Entity.ts @@ -8,6 +8,7 @@ import type { ImageJsonData } from './ImageEntity'; import type { LineEntity, LineJsonData } from './LineEntity'; import type { PointJsonData } from './PointEntity'; import type { RectangleJsonData } from './RectangleEntity'; +import type { HatchJsonData } from './HatchEntity'; import type { TextJsonData } from './TextEntity.ts'; export interface Entity { @@ -18,6 +19,10 @@ export interface Entity { lineWidth: number; lineDash: number[] | undefined; layerId: string; + /** 0~1 객체 투명도. 없으면 도면층 값을 따른다 */ + opacity?: number; + /** GROUP으로 묶인 객체가 공유하는 식별자 */ + groupId?: string; draw(drawController: DrawController, highlighted?: boolean, selected?: boolean): void; /** @@ -57,6 +62,7 @@ export enum EntityName { ArrowHead = 'ArrowHead', Text = 'Text', PolyLine = 'PolyLine', + Hatch = 'Hatch', } export type ShapeJsonData = @@ -67,6 +73,7 @@ export type ShapeJsonData = | PointJsonData | ImageJsonData | ArrowHeadJsonData + | HatchJsonData | TextJsonData; export interface JsonEntity { diff --git a/B07_DesignDetail/openwebcad/src/entities/HatchEntity.ts b/B07_DesignDetail/openwebcad/src/entities/HatchEntity.ts new file mode 100644 index 00000000..d15fcc23 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/entities/HatchEntity.ts @@ -0,0 +1,261 @@ +/** + * 채움 객체 — 해치(HATCH)·그라데이션(GRADIENT)·와이프아웃(WIPEOUT)이 공유한다. + * 경계는 닫힌 점렬 하나로 갖는다 (섬 경계는 아직 다루지 않는다). + */ +import { Box, Point, Polygon, Segment } from '@flatten-js/core'; +import type { Shape, SnapPoint } from '../App.types'; +import { SnapPointType } from '../App.types'; +import type { DrawController } from '../drawControllers/DrawController'; +import { hatchSpans } from '../helpers/geometry/hatch-lines'; +import { getExportColor } from '../helpers/get-export-color'; +import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis'; +import { scalePoint } from '../helpers/scale-point'; +import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state'; +import { type Entity, EntityName, type JsonEntity } from './Entity'; +import type { LineEntity } from './LineEntity'; + +export type HatchStyle = 'solid' | 'pattern' | 'cross' | 'gradient'; + +export interface HatchOptions { + style: HatchStyle; + /** 채움 색 (solid·gradient 시작색) */ + color: string; + /** gradient 끝색 */ + color2?: string; + /** 패턴 선 간격 (도면 단위) */ + spacing: number; + /** 패턴 선 각도 (라디안) */ + angle: number; +} + +const DEFAULT_OPTIONS: HatchOptions = { + style: 'pattern', + color: '#ffffff', + spacing: 1, + angle: Math.PI / 4, +}; + +const GRADIENT_STEPS = 48; + +export class HatchEntity implements Entity { + public id: string = crypto.randomUUID(); + public lineColor = '#fff'; + public lineWidth = 1; + public lineDash: number[] | undefined = undefined; + public layerId: string; + /** 0~1 객체 투명도 (Entity 인터페이스의 선택 항목) */ + public opacity?: number; + /** GROUP으로 묶인 객체가 공유하는 식별자 */ + public groupId?: string; + + private points: Point[]; + public options: HatchOptions; + + constructor(layerId: string, points: Point[], options?: Partial) { + this.layerId = layerId; + this.points = points.map((point) => point.clone()); + this.options = { ...DEFAULT_OPTIONS, ...options }; + } + + public getPoints(): Point[] { + return this.points; + } + + public draw( + drawController: DrawController, + parentHighlighted?: boolean, + parentSelected?: boolean + ): void { + if (this.points.length < 3) return; + const highlighted = parentHighlighted ?? isEntityHighlighted(this); + const selected = parentSelected ?? isEntitySelected(this); + + if (this.options.style === 'solid') { + drawController.setFillStyles(this.options.color); + drawController.fillPolygon(...this.points); + } else if (this.options.style === 'gradient') { + this.drawGradient(drawController); + } else { + drawController.setLineStyles(highlighted, selected, this.options.color, this.lineWidth); + const angles = + this.options.style === 'cross' + ? [this.options.angle, this.options.angle + Math.PI / 2] + : [this.options.angle]; + for (const angle of angles) { + for (const [start, end] of hatchSpans(this.points, angle, this.options.spacing)) { + drawController.drawLine(start, end); + } + } + } + + // 경계선 — 선택·강조 상태를 볼 수 있어야 하므로 항상 그린다 + drawController.setLineStyles(highlighted, selected, this.lineColor, this.lineWidth, this.lineDash); + for (let index = 1; index < this.points.length; index++) { + drawController.drawLine(this.points[index - 1], this.points[index]); + } + } + + /** 그라데이션 — 촘촘한 스캔선의 색을 조금씩 바꿔 표현한다 */ + private drawGradient(drawController: DrawController): void { + const box = this.getBoundingBox(); + const spacing = Math.max((box.ymax - box.ymin) / GRADIENT_STEPS, 1e-6); + const spans = hatchSpans(this.points, 0, spacing); + if (!spans.length) return; + const minY = Math.min(...spans.map(([start]) => start.y)); + const maxY = Math.max(...spans.map(([start]) => start.y)); + const range = maxY - minY || 1; + for (const [start, end] of spans) { + const ratio = (start.y - minY) / range; + drawController.setLineStyles( + false, + false, + mixColors(this.options.color, this.options.color2 ?? this.options.color, ratio), + 2 + ); + drawController.drawLine(start, end); + } + } + + public move(x: number, y: number) { + this.points = this.points.map((point) => point.translate(x, y)); + } + + public scale(scaleOrigin: Point, scaleFactor: number) { + this.points = this.points.map((point) => scalePoint(point, scaleOrigin, scaleFactor)); + this.options.spacing *= scaleFactor; + } + + public rotate(rotateOrigin: Point, angle: number) { + this.points = this.points.map((point) => point.rotate(angle, rotateOrigin)); + } + + public mirror(mirrorAxis: LineEntity) { + this.points = this.points.map((point) => mirrorPointOverAxis(point, mirrorAxis)); + } + + public clone(): HatchEntity { + return new HatchEntity(getActiveLayerId(), this.points, { ...this.options }); + } + + private toPolygon(): Polygon { + return new Polygon(this.points.map((point) => [point.x, point.y] as [number, number])); + } + + public getBoundingBox(): Box { + const xs = this.points.map((point) => point.x); + const ys = this.points.map((point) => point.y); + return new Box(Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)); + } + + public intersectsWithBox(box: Box): boolean { + return this.getBoundingBox().intersect(box); + } + + public isContainedInBox(box: Box): boolean { + const own = this.getBoundingBox(); + return ( + box.xmin <= own.xmin && box.ymin <= own.ymin && box.xmax >= own.xmax && box.ymax >= own.ymax + ); + } + + public getFirstPoint(): Point | null { + return this.points[0] ?? null; + } + + public getShape(): Shape | null { + return this.points.length >= 3 ? this.toPolygon() : null; + } + + public getSnapPoints(): SnapPoint[] { + return this.points.map((point) => ({ point, type: SnapPointType.LineEndPoint })); + } + + public getIntersections(entity: Entity): Point[] { + const otherShape = entity.getShape(); + if (!otherShape || this.points.length < 3) return []; + return this.toPolygon().intersect(otherShape); + } + + public distanceTo(shape: Shape): [number, Segment] | null { + if (this.points.length < 3) return null; + const polygon = this.toPolygon(); + // 채운 면 안쪽을 찍어도 잡히도록 내부는 거리 0으로 본다 (AutoCAD의 해치 선택) + if (shape instanceof Point && polygon.contains(shape)) { + return [0, new Segment(shape, shape)]; + } + return polygon.distanceTo(shape) as [number, Segment]; + } + + public getSvgString(): string | null { + const path = this.points + .map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`) + .join(' '); + const fill = this.options.style === 'solid' ? getExportColor(this.options.color) : 'none'; + return ``; + } + + public getType(): EntityName { + return EntityName.Hatch; + } + + public containsPointOnShape(point: Point): boolean { + if (this.points.length < 3) return false; + return this.toPolygon().contains(point); + } + + public async toJson(): Promise | null> { + return { + id: this.id, + type: EntityName.Hatch, + lineColor: this.lineColor, + lineWidth: this.lineWidth, + lineDash: this.lineDash, + layerId: this.layerId, + shapeData: { + points: this.points.map((point) => ({ x: point.x, y: point.y })), + options: this.options, + }, + }; + } + + public static async fromJson(jsonEntity: JsonEntity): Promise { + if (!jsonEntity.shapeData) { + throw new Error('Invalid JSON entity of type Hatch: missing shapeData'); + } + const hatch = new HatchEntity( + jsonEntity.layerId || getActiveLayerId(), + jsonEntity.shapeData.points.map((point) => new Point(point.x, point.y)), + jsonEntity.shapeData.options + ); + hatch.id = jsonEntity.id; + hatch.lineColor = jsonEntity.lineColor; + hatch.lineWidth = jsonEntity.lineWidth; + hatch.lineDash = jsonEntity.lineDash; + return hatch; + } +} + +/** 두 hex 색을 ratio(0~1)로 섞는다 */ +function mixColors(from: string, to: string, ratio: number): string { + const parse = (color: string) => { + const hex = color.replace('#', ''); + const full = hex.length === 3 ? [...hex].map((char) => char + char).join('') : hex; + return [ + Number.parseInt(full.slice(0, 2), 16), + Number.parseInt(full.slice(2, 4), 16), + Number.parseInt(full.slice(4, 6), 16), + ]; + }; + const [r1, g1, b1] = parse(from); + const [r2, g2, b2] = parse(to); + const channel = (a: number, b: number) => + Math.round(a + (b - a) * Math.min(1, Math.max(0, ratio))) + .toString(16) + .padStart(2, '0'); + return `#${channel(r1, r2)}${channel(g1, g2)}${channel(b1, b2)}`; +} + +export interface HatchJsonData { + points: { x: number; y: number }[]; + options: HatchOptions; +} diff --git a/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts b/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts index ad9f5f91..058439f4 100644 --- a/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts @@ -18,6 +18,10 @@ export class ImageEntity implements Entity { public lineWidth = 1; public lineDash: number[] | undefined = undefined; public layerId: string; + /** 0~1 객체 투명도 (Entity 인터페이스의 선택 항목) */ + public opacity?: number; + /** GROUP으로 묶인 객체가 공유하는 식별자 */ + public groupId?: string; private imageElement: HTMLImageElement; private polygon: Polygon; diff --git a/B07_DesignDetail/openwebcad/src/entities/LineEntity.ts b/B07_DesignDetail/openwebcad/src/entities/LineEntity.ts index 71ca1ace..590a5d1b 100644 --- a/B07_DesignDetail/openwebcad/src/entities/LineEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/LineEntity.ts @@ -22,6 +22,10 @@ export class LineEntity implements Entity, StartAndEndpointEntity { public lineWidth = 1; public lineDash: number[] | undefined = undefined; public layerId: string; + /** 0~1 객체 투명도 (Entity 인터페이스의 선택 항목) */ + public opacity?: number; + /** GROUP으로 묶인 객체가 공유하는 식별자 */ + public groupId?: string; private segment: Segment; diff --git a/B07_DesignDetail/openwebcad/src/entities/MeasurementEntity.ts b/B07_DesignDetail/openwebcad/src/entities/MeasurementEntity.ts index a6a8ea13..ea3a408e 100644 --- a/B07_DesignDetail/openwebcad/src/entities/MeasurementEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/MeasurementEntity.ts @@ -2,16 +2,14 @@ import { Box, Line, Point, Segment, Vector } from '@flatten-js/core'; import { minBy, round } from 'es-toolkit'; import { max, min } from 'es-toolkit/compat'; import { - ARROW_HEAD_LENGTH, ARROW_HEAD_WIDTH, EPSILON, - MEASUREMENT_DECIMAL_PLACES, MEASUREMENT_EXTENSION_LENGTH, - MEASUREMENT_FONT_SIZE, MEASUREMENT_LABEL_OFFSET, MEASUREMENT_ORIGIN_MARGIN, TO_RADIANS, } from '../App.consts'; +import { getDimArrowSize, getDimDecimals, getDimTextHeight } from '../commands/dim-settings'; import type { Shape, SnapPoint } from '../App.types'; import type { DrawController } from '../drawControllers/DrawController'; import { pointDistance } from '../helpers/distance-between-points'; @@ -45,6 +43,10 @@ export class MeasurementEntity implements Entity { public lineWidth = 1; public lineDash: number[] | undefined = undefined; public layerId: string; + /** 0~1 객체 투명도 (Entity 인터페이스의 선택 항목) */ + public opacity?: number; + /** GROUP으로 묶인 객체가 공유하는 식별자 */ + public groupId?: string; private startPoint: Point; private endPoint: Point; @@ -57,6 +59,22 @@ export class MeasurementEntity implements Entity { this.offsetPoint = offsetPoint; } + public getStartPoint(): Point { + return this.startPoint; + } + + public getEndPoint(): Point { + return this.endPoint; + } + + public getOffsetPoint(): Point { + return this.offsetPoint; + } + + public setOffsetPoint(point: Point): void { + this.offsetPoint = point; + } + public getDrawPoints() { // Return if measurement is zero length if (isPointEqual(this.startPoint, this.endPoint)) { @@ -136,7 +154,7 @@ export class MeasurementEntity implements Entity { (offsetStartPoint.x + offsetEndPoint.x) / 2, (offsetStartPoint.y + offsetEndPoint.y) / 2 ); - const textHeight = MEASUREMENT_FONT_SIZE / worldFactor; + const textHeight = getDimTextHeight() / worldFactor; const totalOffset = MEASUREMENT_LABEL_OFFSET / worldFactor + textHeight / 2; const midpointMeasurementLineOffset = midpointMeasurementLine .clone() @@ -197,7 +215,7 @@ export class MeasurementEntity implements Entity { const vectorFromEndToStartUnit = vectorFromEndToStart.normalize(); const baseOfArrow = endPoint .clone() - .translate(vectorFromEndToStartUnit.multiply(ARROW_HEAD_LENGTH / worldFactor)); + .translate(vectorFromEndToStartUnit.multiply(getDimArrowSize() / worldFactor)); const perpendicularVector1 = vectorFromEndToStartUnit.rotate(90 * TO_RADIANS); const perpendicularVector2 = vectorFromEndToStartUnit.rotate(-90 * TO_RADIANS); const leftCornerOfArrow = baseOfArrow @@ -284,7 +302,7 @@ export class MeasurementEntity implements Entity { drawController.drawLine(offsetEndPointMargin, offsetEndPointExtend); const distance = String( - round(pointDistance(this.startPoint, this.endPoint), MEASUREMENT_DECIMAL_PLACES) + round(pointDistance(this.startPoint, this.endPoint), getDimDecimals()) ); const originalTextDirection = normalUnit.rotate90CW(); let finalTextDirection = originalTextDirection; @@ -297,7 +315,7 @@ export class MeasurementEntity implements Entity { drawController.drawText(distance, midpointMeasurementLineOffset, { textAlign: 'center', textDirection: finalTextDirection, - fontSize: MEASUREMENT_FONT_SIZE / (drawController.getScreenScale() || 1), + fontSize: getDimTextHeight() / (drawController.getScreenScale() || 1), textColor: this.lineColor, }); } @@ -401,12 +419,12 @@ export class MeasurementEntity implements Entity { // Calculate text properties const distance = String( - round(pointDistance(this.startPoint, this.endPoint), MEASUREMENT_DECIMAL_PLACES) + round(pointDistance(this.startPoint, this.endPoint), getDimDecimals()) ); const worldFactor = annotationWorldFactor(); - const textHeight = MEASUREMENT_FONT_SIZE / worldFactor; + const textHeight = getDimTextHeight() / worldFactor; // Estimate width: textString.length * fontSize * aspectRatioFactor - const textWidth = (distance.length * MEASUREMENT_FONT_SIZE * 0.6) / worldFactor; + const textWidth = (distance.length * getDimTextHeight() * 0.6) / worldFactor; const { midpointMeasurementLineOffset, normalUnit } = drawPoints; diff --git a/B07_DesignDetail/openwebcad/src/entities/PointEntity.ts b/B07_DesignDetail/openwebcad/src/entities/PointEntity.ts index c522343a..bab2e201 100644 --- a/B07_DesignDetail/openwebcad/src/entities/PointEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/PointEntity.ts @@ -15,6 +15,10 @@ export class PointEntity implements Entity { public lineWidth = 1; public lineDash: number[] | undefined = undefined; public layerId: string; + /** 0~1 객체 투명도 (Entity 인터페이스의 선택 항목) */ + public opacity?: number; + /** GROUP으로 묶인 객체가 공유하는 식별자 */ + public groupId?: string; public point: Point; diff --git a/B07_DesignDetail/openwebcad/src/entities/PolyLineEntity.ts b/B07_DesignDetail/openwebcad/src/entities/PolyLineEntity.ts index 68284d08..34a7c035 100644 --- a/B07_DesignDetail/openwebcad/src/entities/PolyLineEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/PolyLineEntity.ts @@ -16,6 +16,10 @@ export class PolyLineEntity implements Entity { public lineWidth = 1; public lineDash: number[] | undefined = undefined; public layerId: string; + /** 0~1 객체 투명도 (Entity 인터페이스의 선택 항목) */ + public opacity?: number; + /** GROUP으로 묶인 객체가 공유하는 식별자 */ + public groupId?: string; private readonly entities: Entity[]; @@ -26,6 +30,11 @@ export class PolyLineEntity implements Entity { ); } + /** 폴리선을 이루는 선·호 목록 (EXPLODE·샘플링에서 읽는다) */ + public getEntities(): Entity[] { + return this.entities; + } + public numberOfSegments(): number { return this.entities.length; } diff --git a/B07_DesignDetail/openwebcad/src/entities/RectangleEntity.ts b/B07_DesignDetail/openwebcad/src/entities/RectangleEntity.ts index 69d86bd5..70affaec 100644 --- a/B07_DesignDetail/openwebcad/src/entities/RectangleEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/RectangleEntity.ts @@ -17,6 +17,10 @@ export class RectangleEntity implements Entity { public lineWidth = 1; public lineDash: number[] | undefined = undefined; public layerId: string; + /** 0~1 객체 투명도 (Entity 인터페이스의 선택 항목) */ + public opacity?: number; + /** GROUP으로 묶인 객체가 공유하는 식별자 */ + public groupId?: string; private polygon: Polygon; diff --git a/B07_DesignDetail/openwebcad/src/entities/TextEntity.ts b/B07_DesignDetail/openwebcad/src/entities/TextEntity.ts index 33a8fb92..63e3a9ef 100644 --- a/B07_DesignDetail/openwebcad/src/entities/TextEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/TextEntity.ts @@ -22,6 +22,10 @@ export class TextEntity implements Entity { public lineWidth = 1; public lineDash: number[] = []; public layerId: string; + /** 0~1 객체 투명도 (Entity 인터페이스의 선택 항목) */ + public opacity?: number; + /** GROUP으로 묶인 객체가 공유하는 식별자 */ + public groupId?: string; private readonly options: TextOptions; constructor( diff --git a/B07_DesignDetail/openwebcad/src/helpers/cad-clipboard.ts b/B07_DesignDetail/openwebcad/src/helpers/cad-clipboard.ts new file mode 100644 index 00000000..3667eec5 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/helpers/cad-clipboard.ts @@ -0,0 +1,53 @@ +/** + * 내부 클립보드 — 잘라내기·복사·붙여넣기(기준점 포함). + * 시스템 클립보드는 도면 객체를 담을 수 없어 앱 안에서만 오간다. + */ +import { Point } from '@flatten-js/core'; +import type { Entity } from '../entities/Entity'; +import { getBoundingBoxOfMultipleEntities } from './get-bounding-box-of-multiple-entities'; + +interface ClipboardContent { + entities: Entity[]; + /** 붙여넣기 기준점 — 지정하지 않으면 선택 영역의 좌하단 */ + basePoint: Point; +} + +let clipboard: ClipboardContent | null = null; + +export const hasClipboardContent = (): boolean => !!clipboard?.entities.length; + +export function copyToClipboard(entities: Entity[], basePoint?: Point): number { + if (!entities.length) return 0; + const box = getBoundingBoxOfMultipleEntities(entities); + clipboard = { + entities: entities.map((entity) => { + const copy = entity.clone(); + copy.lineColor = entity.lineColor; + copy.lineWidth = entity.lineWidth; + copy.lineDash = entity.lineDash; + copy.layerId = entity.layerId; + return copy; + }), + basePoint: basePoint ?? new Point(box.minX, box.minY), + }; + return clipboard.entities.length; +} + +/** + * 클립보드 내용을 새 객체로 꺼낸다. + * target을 주면 기준점이 그 위치에 오도록 옮기고, 없으면 원래 좌표 그대로 둔다. + */ +export function pasteFromClipboard(target?: Point): Entity[] { + if (!clipboard) return []; + return clipboard.entities.map((entity) => { + const copy = entity.clone(); + copy.lineColor = entity.lineColor; + copy.lineWidth = entity.lineWidth; + copy.lineDash = entity.lineDash; + copy.layerId = entity.layerId; + if (target) { + copy.move(target.x - (clipboard as ClipboardContent).basePoint.x, target.y - (clipboard as ClipboardContent).basePoint.y); + } + return copy; + }); +} diff --git a/B07_DesignDetail/openwebcad/src/helpers/debug-hook.ts b/B07_DesignDetail/openwebcad/src/helpers/debug-hook.ts new file mode 100644 index 00000000..e3d4a835 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/helpers/debug-hook.ts @@ -0,0 +1,32 @@ +/** + * 화면 검증용 상태 조회 창구 — 브라우저 콘솔·자동화 스크립트가 + * window.__aisloCad로 현재 객체·도면층·선택을 읽는다. 앱 동작에는 관여하지 않는다. + */ +import { getEntities, getLayers, getSelectedEntityIds } from '../state'; +import { isEntityHidden } from './visibility'; + +export function registerCadDebugHook(): void { + (window as unknown as Record).__aisloCad = { + entities: () => + getEntities().map((entity) => { + const box = entity.getBoundingBox(); + return { + id: entity.id, + type: entity.getType(), + layerId: entity.layerId, + groupId: entity.groupId ?? null, + opacity: entity.opacity ?? null, + hidden: isEntityHidden(entity), + color: entity.lineColor, + box: [ + Math.round(box.xmin), + Math.round(box.ymin), + Math.round(box.xmax), + Math.round(box.ymax), + ], + }; + }), + layers: () => getLayers(), + selection: () => getSelectedEntityIds(), + }; +} diff --git a/B07_DesignDetail/openwebcad/src/helpers/draw-functions.ts b/B07_DesignDetail/openwebcad/src/helpers/draw-functions.ts index 71ff2abe..c5f93a1e 100644 --- a/B07_DesignDetail/openwebcad/src/helpers/draw-functions.ts +++ b/B07_DesignDetail/openwebcad/src/helpers/draw-functions.ts @@ -5,6 +5,7 @@ import type {DrawController} from '../drawControllers/DrawController'; import type {ScreenCanvasDrawController} from '../drawControllers/screenCanvas.drawController'; import type {Entity} from '../entities/Entity'; import {getLayerById, isEntityHighlighted, isEntitySelected} from '../state'; +import {isEntityHidden} from './visibility'; import {toast} from 'react-toastify'; export function drawEntities(drawController: DrawController, entities: Entity[]) { @@ -15,9 +16,13 @@ export function drawEntities(drawController: DrawController, entities: Entity[]) console.error('Failed to find layer for entity: ', entity); continue; } - if (!layer?.isVisible) { - continue; // Layer not visible, skip drawing + if (!layer?.isVisible || layer.isFrozen) { + continue; // 꺼졌거나 동결된 도면층은 그리지 않는다 } + if (isEntityHidden(entity)) { + continue; // ISOLATEOBJECTS·HIDEOBJECTS로 숨긴 객체 + } + drawController.setOpacity(entity.opacity ?? layer.opacity ?? 1); drawController.setLineStyles( isEntityHighlighted(entity), isEntitySelected(entity), @@ -26,6 +31,7 @@ export function drawEntities(drawController: DrawController, entities: Entity[]) [] ); entity.draw(drawController); + drawController.setOpacity(1); } } diff --git a/B07_DesignDetail/openwebcad/src/helpers/entity-groups.ts b/B07_DesignDetail/openwebcad/src/helpers/entity-groups.ts new file mode 100644 index 00000000..ac69c0ac --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/helpers/entity-groups.ts @@ -0,0 +1,22 @@ +/** + * GROUP으로 묶인 객체를 함께 선택하기 위한 확장. + * 선택 도구가 부르는 자리라 도구 모듈을 import하지 않는다 (순환 참조 방지). + */ +import { getEntities } from '../state'; + +export function expandSelectionWithGroups(entityIds: string[]): string[] { + const entities = getEntities(); + const ids = new Set(entityIds); + const selectedGroups = new Set( + entities + .filter((entity) => ids.has(entity.id) && entity.groupId) + .map((entity) => entity.groupId as string) + ); + if (!selectedGroups.size) return entityIds; + + const expanded = new Set(entityIds); + for (const entity of entities) { + if (entity.groupId && selectedGroups.has(entity.groupId)) expanded.add(entity.id); + } + return [...expanded]; +} diff --git a/B07_DesignDetail/openwebcad/src/helpers/geometry/entity-loop.ts b/B07_DesignDetail/openwebcad/src/helpers/geometry/entity-loop.ts new file mode 100644 index 00000000..6cd36aa1 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/helpers/geometry/entity-loop.ts @@ -0,0 +1,50 @@ +/** + * 선택한 객체들을 하나의 닫힌 경계(점렬)로 잇는다. + * HATCH·BOUNDARY·REGION·WIPEOUT이 공유한다. + */ +import type { Point } from '@flatten-js/core'; +import type { Entity } from '../../entities/Entity'; +import { dedupeConsecutive, sampleEntityPoints } from './sample-entity'; + +const DEFAULT_TOLERANCE = 1e-3; + +const near = (a: Point, b: Point, tolerance: number): boolean => + Math.abs(a.x - b.x) <= tolerance && Math.abs(a.y - b.y) <= tolerance; + +/** + * 끝점이 맞닿는 순서대로 이어 붙여 경계를 만든다. + * 이미 닫힌 객체(원·사각형) 하나만 골랐다면 그 점렬을 그대로 돌려준다. + */ +export function entitiesToLoop(entities: Entity[], tolerance = DEFAULT_TOLERANCE): Point[] { + const chains = entities + .map((entity) => sampleEntityPoints(entity)) + .filter((points) => points.length >= 2); + if (!chains.length) return []; + if (chains.length === 1) return closeLoop(chains[0], tolerance); + + const remaining = [...chains]; + let loop = remaining.shift() as Point[]; + + while (remaining.length) { + const tail = loop[loop.length - 1]; + const index = remaining.findIndex( + (chain) => + near(chain[0], tail, tolerance) || near(chain[chain.length - 1], tail, tolerance) + ); + if (index === -1) break; // 끊긴 경계 — 여기까지만 잇는다 + const [chain] = remaining.splice(index, 1); + const ordered = near(chain[0], tail, tolerance) ? chain : [...chain].reverse(); + loop = dedupeConsecutive([...loop, ...ordered]); + } + + return closeLoop(loop, tolerance); +} + +/** 시작점과 끝점이 떨어져 있으면 닫는다 */ +export function closeLoop(points: Point[], tolerance = DEFAULT_TOLERANCE): Point[] { + const cleaned = dedupeConsecutive(points); + if (cleaned.length < 3) return cleaned; + const first = cleaned[0]; + const last = cleaned[cleaned.length - 1]; + return near(first, last, tolerance) ? cleaned : [...cleaned, first.clone()]; +} diff --git a/B07_DesignDetail/openwebcad/src/helpers/geometry/hatch-lines.ts b/B07_DesignDetail/openwebcad/src/helpers/geometry/hatch-lines.ts new file mode 100644 index 00000000..07c29145 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/helpers/geometry/hatch-lines.ts @@ -0,0 +1,67 @@ +/** 해치·그라데이션이 쓰는 스캔선 계산 — 상태 없는 순수 함수. */ +import { Point } from '@flatten-js/core'; + +export type Span = [Point, Point]; + +const rotate = (point: Point, cos: number, sin: number): Point => + new Point(point.x * cos - point.y * sin, point.x * sin + point.y * cos); + +/** + * 닫힌 다각형 내부를 angle 방향 평행선으로 채울 때의 선분 목록. + * 오목한 다각형도 스캔선 교차를 짝지어 처리한다. + */ +export function hatchSpans(polygon: Point[], angleRad: number, spacing: number): Span[] { + if (polygon.length < 3 || spacing <= 0) return []; + + const cos = Math.cos(-angleRad); + const sin = Math.sin(-angleRad); + const rotated = polygon.map((point) => rotate(point, cos, sin)); + + const minY = Math.min(...rotated.map((point) => point.y)); + const maxY = Math.max(...rotated.map((point) => point.y)); + if (!Number.isFinite(minY) || !Number.isFinite(maxY)) return []; + + // 스캔선 수 상한 — 축척이 클 때 무한 루프처럼 보이는 것을 막는다 + const lineCount = Math.floor((maxY - minY) / spacing); + if (lineCount > 5000) return []; + + const backCos = Math.cos(angleRad); + const backSin = Math.sin(angleRad); + const spans: Span[] = []; + + for (let y = minY + spacing / 2; y < maxY; y += spacing) { + const crossings: number[] = []; + for (let index = 0; index < rotated.length; index++) { + const start = rotated[index]; + const end = rotated[(index + 1) % rotated.length]; + if (start.y === end.y) continue; + const low = Math.min(start.y, end.y); + const high = Math.max(start.y, end.y); + if (y < low || y >= high) continue; + const ratio = (y - start.y) / (end.y - start.y); + crossings.push(start.x + (end.x - start.x) * ratio); + } + crossings.sort((a, b) => a - b); + for (let index = 0; index + 1 < crossings.length; index += 2) { + spans.push([ + rotate(new Point(crossings[index], y), backCos, backSin), + rotate(new Point(crossings[index + 1], y), backCos, backSin), + ]); + } + } + return spans; +} + +/** 점이 닫힌 다각형 안에 있는가 (홀수 교차 판정) */ +export function isPointInPolygon(polygon: Point[], point: Point): boolean { + let inside = false; + for (let index = 0, previous = polygon.length - 1; index < polygon.length; previous = index++) { + const a = polygon[index]; + const b = polygon[previous]; + const crosses = a.y > point.y !== b.y > point.y; + if (crosses && point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x) { + inside = !inside; + } + } + return inside; +} diff --git a/B07_DesignDetail/openwebcad/src/helpers/geometry/sample-entity.ts b/B07_DesignDetail/openwebcad/src/helpers/geometry/sample-entity.ts new file mode 100644 index 00000000..788d4891 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/helpers/geometry/sample-entity.ts @@ -0,0 +1,125 @@ +/** + * 엔티티를 점렬(폴리라인)로 펴는 순수 함수들. + * 등분(DIVIDE)·길이분할(MEASURE)·경계(BOUNDARY)·해치가 모두 이 표현을 공유한다. + */ +import { Arc, Circle, Point, Polygon, Segment } from '@flatten-js/core'; +import type { Entity } from '../../entities/Entity'; +import { EntityName } from '../../entities/Entity'; +import type { PolyLineEntity } from '../../entities/PolyLineEntity'; +import { polygonToSegments } from '../polygon-to-segments'; + +const CURVE_SEGMENTS = 64; + +/** 두 점이 사실상 같은 위치인지 (좌표 오차 허용) */ +const samePoint = (a: Point, b: Point, tolerance = 1e-6): boolean => + Math.abs(a.x - b.x) <= tolerance && Math.abs(a.y - b.y) <= tolerance; + +function sampleArc(arc: Arc, segments: number): Point[] { + const points: Point[] = []; + const sweep = arc.sweep * (arc.counterClockwise ? 1 : -1); + // flatten-js 타입 선언이 반지름을 Number 객체로 잡아 두어 숫자로 되돌린다 + const radius = Number(arc.r); + for (let index = 0; index <= segments; index++) { + const angle = arc.startAngle + (sweep * index) / segments; + points.push( + new Point(arc.center.x + radius * Math.cos(angle), arc.center.y + radius * Math.sin(angle)) + ); + } + return points; +} + +function sampleCircle(circle: Circle, segments: number): Point[] { + const points: Point[] = []; + for (let index = 0; index <= segments; index++) { + const angle = (2 * Math.PI * index) / segments; + points.push( + new Point(circle.center.x + circle.r * Math.cos(angle), circle.center.y + circle.r * Math.sin(angle)) + ); + } + return points; +} + +/** 이어지는 중복점을 제거한다 (폴리선 이음매에서 생긴다) */ +export function dedupeConsecutive(points: Point[]): Point[] { + return points.filter((point, index) => index === 0 || !samePoint(point, points[index - 1])); +} + +/** 엔티티 하나를 점렬로 편다. 곡선은 curveSegments 등분해 근사한다. */ +export function sampleEntityPoints(entity: Entity, curveSegments = CURVE_SEGMENTS): Point[] { + if (entity.getType() === EntityName.PolyLine) { + const children = (entity as PolyLineEntity).getEntities(); + return dedupeConsecutive( + children.flatMap((child) => sampleEntityPoints(child, curveSegments)) + ); + } + + const shape = entity.getShape(); + if (shape instanceof Segment) return [shape.start, shape.end]; + if (shape instanceof Arc) return sampleArc(shape, curveSegments); + if (shape instanceof Circle) return sampleCircle(shape, curveSegments); + if (shape instanceof Polygon) { + const segments = polygonToSegments(shape); + return dedupeConsecutive([ + ...segments.map((segment) => segment.start), + ...(segments.length ? [segments[segments.length - 1].end] : []), + ]); + } + if (shape instanceof Point) return [shape]; + return []; +} + +/** 점렬의 누적 길이 */ +export function polylineLengths(points: Point[]): number[] { + const lengths = [0]; + for (let index = 1; index < points.length; index++) { + lengths.push(lengths[index - 1] + points[index - 1].distanceTo(points[index])[0]); + } + return lengths; +} + +export const polylineLength = (points: Point[]): number => { + const lengths = polylineLengths(points); + return lengths[lengths.length - 1] ?? 0; +}; + +/** 시작점에서 distance만큼 진행한 위치 (길이를 넘으면 null) */ +export function pointAtDistance(points: Point[], distance: number): Point | null { + if (points.length < 2) return null; + const lengths = polylineLengths(points); + const total = lengths[lengths.length - 1]; + if (distance < 0 || distance > total) return null; + for (let index = 1; index < points.length; index++) { + if (lengths[index] >= distance) { + const segmentLength = lengths[index] - lengths[index - 1]; + const ratio = segmentLength === 0 ? 0 : (distance - lengths[index - 1]) / segmentLength; + const start = points[index - 1]; + const end = points[index]; + return new Point(start.x + (end.x - start.x) * ratio, start.y + (end.y - start.y) * ratio); + } + } + return points[points.length - 1]; +} + +/** DIVIDE — 객체를 count 등분하는 내부 점 (count-1개) */ +export function dividePoints(points: Point[], count: number): Point[] { + if (count < 2) return []; + const total = polylineLength(points); + const result: Point[] = []; + for (let index = 1; index < count; index++) { + const point = pointAtDistance(points, (total * index) / count); + if (point) result.push(point); + } + return result; +} + +/** MEASURE — 시작점에서 spacing 간격마다 찍는 점 */ +export function measurePoints(points: Point[], spacing: number): Point[] { + if (spacing <= 0) return []; + const total = polylineLength(points); + const result: Point[] = []; + for (let distance = spacing; distance <= total + 1e-9; distance += spacing) { + const point = pointAtDistance(points, Math.min(distance, total)); + if (point) result.push(point); + } + return result; +} diff --git a/B07_DesignDetail/openwebcad/src/helpers/geometry/shape-points.ts b/B07_DesignDetail/openwebcad/src/helpers/geometry/shape-points.ts new file mode 100644 index 00000000..94bc2ac9 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/helpers/geometry/shape-points.ts @@ -0,0 +1,207 @@ +/** + * 점 몇 개로 정의되는 도형의 좌표 계산 — 순수 함수. + * 정다각형·타원·스플라인·구름형처럼 폴리선으로 근사해 그리는 도형이 여기서 나온다. + */ +import { Point } from '@flatten-js/core'; + +export interface ArcDefinition { + center: Point; + radius: number; + startAngle: number; + endAngle: number; + counterClockwise: boolean; +} + +/** 3점(시작·통과·끝)을 지나는 호. 세 점이 일직선이면 null */ +export function arcThroughThreePoints( + start: Point, + through: Point, + end: Point +): ArcDefinition | null { + const ax = start.x; + const ay = start.y; + const bx = through.x; + const by = through.y; + const cx = end.x; + const cy = end.y; + + const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)); + if (Math.abs(d) < 1e-9) return null; // 일직선 + + const ux = + ((ax * ax + ay * ay) * (by - cy) + + (bx * bx + by * by) * (cy - ay) + + (cx * cx + cy * cy) * (ay - by)) / + d; + const uy = + ((ax * ax + ay * ay) * (cx - bx) + + (bx * bx + by * by) * (ax - cx) + + (cx * cx + cy * cy) * (bx - ax)) / + d; + + const center = new Point(ux, uy); + const radius = center.distanceTo(start)[0]; + const startAngle = Math.atan2(ay - uy, ax - ux); + const throughAngle = Math.atan2(by - uy, bx - ux); + const endAngle = Math.atan2(cy - uy, cx - ux); + + // 통과점이 시작→끝 사이에 오도록 회전 방향을 고른다 + const counterClockwise = isAngleBetween(throughAngle, startAngle, endAngle, true); + return { center, radius, startAngle, endAngle, counterClockwise }; +} + +/** 반시계(또는 시계) 방향으로 start에서 end로 갈 때 angle을 지나는가 */ +export function isAngleBetween( + angle: number, + start: number, + end: number, + counterClockwise: boolean +): boolean { + const normalize = (value: number) => ((value % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI); + const sweep = counterClockwise ? normalize(end - start) : normalize(start - end); + const offset = counterClockwise ? normalize(angle - start) : normalize(start - angle); + return offset <= sweep; +} + +/** 정다각형 — 중심과 첫 꼭짓점, 변 수 */ +export function regularPolygonPoints(center: Point, vertex: Point, sides: number): Point[] { + const count = Math.max(3, Math.round(sides)); + const radius = center.distanceTo(vertex)[0]; + const startAngle = Math.atan2(vertex.y - center.y, vertex.x - center.x); + const points: Point[] = []; + for (let index = 0; index < count; index++) { + const angle = startAngle + (2 * Math.PI * index) / count; + points.push(new Point(center.x + radius * Math.cos(angle), center.y + radius * Math.sin(angle))); + } + points.push(points[0].clone()); + return points; +} + +/** 타원 — 중심, 장축 끝점, 단축 반지름. 폴리선으로 근사한다 */ +export function ellipsePoints( + center: Point, + majorPoint: Point, + minorRadius: number, + segments = 72 +): Point[] { + const majorRadius = center.distanceTo(majorPoint)[0]; + const rotation = Math.atan2(majorPoint.y - center.y, majorPoint.x - center.x); + const cos = Math.cos(rotation); + const sin = Math.sin(rotation); + const points: Point[] = []; + for (let index = 0; index <= segments; index++) { + const angle = (2 * Math.PI * index) / segments; + const x = majorRadius * Math.cos(angle); + const y = minorRadius * Math.sin(angle); + points.push(new Point(center.x + x * cos - y * sin, center.y + x * sin + y * cos)); + } + return points; +} + +/** 조정점을 지나는 부드러운 곡선 (Catmull-Rom → 폴리선) */ +export function splinePoints(controlPoints: Point[], segmentsPerSpan = 12): Point[] { + if (controlPoints.length < 3) return [...controlPoints]; + const extended = [ + controlPoints[0], + ...controlPoints, + controlPoints[controlPoints.length - 1], + ]; + const result: Point[] = []; + for (let index = 1; index < extended.length - 2; index++) { + const p0 = extended[index - 1]; + const p1 = extended[index]; + const p2 = extended[index + 1]; + const p3 = extended[index + 2]; + for (let step = 0; step < segmentsPerSpan; step++) { + const t = step / segmentsPerSpan; + result.push(catmullRom(p0, p1, p2, p3, t)); + } + } + result.push(controlPoints[controlPoints.length - 1]); + return result; +} + +function catmullRom(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point { + const t2 = t * t; + const t3 = t2 * t; + const axis = (a: number, b: number, c: number, d: number) => + 0.5 * (2 * b + (c - a) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (3 * b - 3 * c + d - a) * t3); + return new Point(axis(p0.x, p1.x, p2.x, p3.x), axis(p0.y, p1.y, p2.y, p3.y)); +} + +/** 구름형 리비전 — 경로를 따라 반원 스캘럽을 이어붙인 점렬 */ +export function revisionCloudPoints(path: Point[], arcRadius: number): Point[] { + if (path.length < 2 || arcRadius <= 0) return [...path]; + const result: Point[] = []; + for (let index = 1; index < path.length; index++) { + const start = path[index - 1]; + const end = path[index]; + const length = start.distanceTo(end)[0]; + const bulges = Math.max(1, Math.round(length / (arcRadius * 2))); + for (let bulge = 0; bulge < bulges; bulge++) { + const from = lerp(start, end, bulge / bulges); + const to = lerp(start, end, (bulge + 1) / bulges); + result.push(...halfArcPoints(from, to)); + } + } + return result; +} + +const lerp = (a: Point, b: Point, t: number): Point => + new Point(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); + +/** 두 점을 지름으로 하는 반원 (구름형 한 칸) */ +function halfArcPoints(from: Point, to: Point, segments = 8): Point[] { + const center = lerp(from, to, 0.5); + const radius = from.distanceTo(to)[0] / 2; + const baseAngle = Math.atan2(to.y - from.y, to.x - from.x); + const points: Point[] = []; + for (let index = 0; index <= segments; index++) { + const angle = baseAngle + Math.PI - (Math.PI * index) / segments; + points.push(new Point(center.x + radius * Math.cos(angle), center.y + radius * Math.sin(angle))); + } + return points; +} + +/** 폴리선을 distance만큼 나란히 민 점렬 (양수: 진행 방향 왼쪽) */ +export function offsetPolylinePoints(points: Point[], distance: number): Point[] { + if (points.length < 2 || distance === 0) return [...points]; + const offsetLines: { start: Point; end: Point }[] = []; + for (let index = 1; index < points.length; index++) { + const start = points[index - 1]; + const end = points[index]; + const dx = end.x - start.x; + const dy = end.y - start.y; + const length = Math.hypot(dx, dy); + if (length < 1e-9) continue; + const nx = (-dy / length) * distance; + const ny = (dx / length) * distance; + offsetLines.push({ + start: new Point(start.x + nx, start.y + ny), + end: new Point(end.x + nx, end.y + ny), + }); + } + if (!offsetLines.length) return [...points]; + + const result: Point[] = [offsetLines[0].start]; + for (let index = 1; index < offsetLines.length; index++) { + const previous = offsetLines[index - 1]; + const current = offsetLines[index]; + const joint = intersectLines(previous.start, previous.end, current.start, current.end); + result.push(joint ?? current.start); + } + result.push(offsetLines[offsetLines.length - 1].end); + return result; +} + +/** 두 직선(무한 연장)의 교점. 평행이면 null */ +export function intersectLines(a1: Point, a2: Point, b1: Point, b2: Point): Point | null { + const d1x = a2.x - a1.x; + const d1y = a2.y - a1.y; + const d2x = b2.x - b1.x; + const d2y = b2.y - b1.y; + const denominator = d1x * d2y - d1y * d2x; + if (Math.abs(denominator) < 1e-12) return null; + const t = ((b1.x - a1.x) * d2y - (b1.y - a1.y) * d2x) / denominator; + return new Point(a1.x + d1x * t, a1.y + d1y * t); +} diff --git a/B07_DesignDetail/openwebcad/src/helpers/import-export-handlers/import-entities-from-json.ts b/B07_DesignDetail/openwebcad/src/helpers/import-export-handlers/import-entities-from-json.ts index 1bae91ca..fd4519da 100644 --- a/B07_DesignDetail/openwebcad/src/helpers/import-export-handlers/import-entities-from-json.ts +++ b/B07_DesignDetail/openwebcad/src/helpers/import-export-handlers/import-entities-from-json.ts @@ -2,6 +2,7 @@ import {compact} from 'es-toolkit'; import {ArcEntity, type ArcJsonData} from '../../entities/ArcEntity'; import {CircleEntity, type CircleJsonData} from '../../entities/CircleEntity'; import {type Entity, EntityName, type JsonEntity} from '../../entities/Entity'; +import {HatchEntity, type HatchJsonData} from '../../entities/HatchEntity'; import {ImageEntity, type ImageJsonData} from '../../entities/ImageEntity.ts'; import {LineEntity, type LineJsonData} from '../../entities/LineEntity'; import {MeasurementEntity, type MeasurementJsonData} from '../../entities/MeasurementEntity.ts'; @@ -60,6 +61,8 @@ export async function getEntitiesAndLayersFromJsonObject( return ImageEntity.fromJson(entity as JsonEntity); case EntityName.PolyLine: return PolyLineEntity.fromJson(entity as JsonEntity); + case EntityName.Hatch: + return HatchEntity.fromJson(entity as JsonEntity); default: throw new Error(`Invalid entity type: ${entity.type}`); diff --git a/B07_DesignDetail/openwebcad/src/helpers/layer-history.ts b/B07_DesignDetail/openwebcad/src/helpers/layer-history.ts new file mode 100644 index 00000000..b42befff --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/helpers/layer-history.ts @@ -0,0 +1,32 @@ +/** + * 도면층 상태 기록 — 이전 상태(LAYERP)와 이름 붙인 상태(LAYERSTATE). + * 도면층 목록 자체를 통째로 복사해 둔다 (개수가 적어 비용이 무시할 만하다). + */ +import type { Layer } from '../App.types'; + +const HISTORY_LIMIT = 20; + +const history: Layer[][] = []; +const namedStates = new Map(); + +const copy = (layers: Layer[]): Layer[] => layers.map((layer) => ({ ...layer })); + +/** 도면층을 바꾸기 직전에 부른다 */ +export const pushLayerHistory = (layers: Layer[]): void => { + history.push(copy(layers)); + if (history.length > HISTORY_LIMIT) history.shift(); +}; + +/** 직전 상태를 꺼낸다 (없으면 null) */ +export const popLayerHistory = (): Layer[] | null => history.pop() ?? null; + +export const saveLayerState = (name: string, layers: Layer[]): void => { + namedStates.set(name.trim().toUpperCase(), copy(layers)); +}; + +export const restoreLayerState = (name: string): Layer[] | null => { + const saved = namedStates.get(name.trim().toUpperCase()); + return saved ? copy(saved) : null; +}; + +export const getLayerStateNames = (): string[] => [...namedStates.keys()]; diff --git a/B07_DesignDetail/openwebcad/src/helpers/visibility.ts b/B07_DesignDetail/openwebcad/src/helpers/visibility.ts new file mode 100644 index 00000000..f70854eb --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/helpers/visibility.ts @@ -0,0 +1,34 @@ +/** + * 객체 분리·숨기기 상태 (ISOLATEOBJECTS·HIDEOBJECTS·UNISOLATEOBJECTS). + * 도면층 가시성과 달리 객체 단위이며 저장하지 않는 일시 상태다. + */ +import { HtmlEvent } from '../App.types'; +import type { Entity } from '../entities/Entity'; +import { bumpSceneVersion } from './scene-version'; + +let hiddenEntityIds = new Set(); + +export const isEntityHidden = (entity: Entity): boolean => hiddenEntityIds.has(entity.id); +export const getHiddenEntityCount = (): number => hiddenEntityIds.size; + +function apply(ids: Set): void { + hiddenEntityIds = ids; + bumpSceneVersion(); + window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE)); +} + +/** 지정 객체를 숨긴다 */ +export const hideEntities = (entities: Entity[]): void => { + const next = new Set(hiddenEntityIds); + for (const entity of entities) next.add(entity.id); + apply(next); +}; + +/** 지정 객체만 남기고 나머지를 숨긴다 */ +export const isolateEntities = (entities: Entity[], allEntities: Entity[]): void => { + const keep = new Set(entities.map((entity) => entity.id)); + apply(new Set(allEntities.filter((entity) => !keep.has(entity.id)).map((entity) => entity.id))); +}; + +/** 숨김 해제 */ +export const showAllEntities = (): void => apply(new Set()); diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts index f70c8a9e..5c02726f 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts @@ -1,6 +1,5 @@ import { Point } from '@flatten-js/core'; import { compact, round } from 'es-toolkit'; -import { Actor } from 'xstate'; import { CANVAS_INPUT_FIELD_BACKGROUND_COLOR, CANVAS_INPUT_FIELD_HEIGHT, @@ -32,7 +31,6 @@ import { getSnapPoint, getSnapPointOnAngleGuide, redo, - setActiveToolActor, setGhostHelperEntities, setHighlightedEntityIds, setPanStartLocation, @@ -40,8 +38,13 @@ import { setShouldDrawCursor, undo, } from '../state.ts'; +import { + describeCommand, + matchCommandPrefixes, + resolveCommandInput, +} from '../commands/registry.ts'; +import { runCommand } from '../commands/run-command.ts'; import { Tool } from '../tools.ts'; -import { TOOL_STATE_MACHINES } from '../tools/tool.consts.ts'; import { type AbsolutePointInputEvent, ActorEvent, @@ -119,7 +122,7 @@ export class InputController { ); } - const matchingToolNames = this.getToolNamesFromPrefixText(); + const matchingCommands = this.getCommandSuggestions(); const toolInstruction = getLastStateInstructions(); const texts: string[] = []; if (toolInstruction) { @@ -129,15 +132,16 @@ export class InputController { const roundedY = round(drawController.getWorldMouseLocation().y, 2); texts.push(`${roundedX},${roundedY}`); } - if (matchingToolNames.length) { - // Draw list of matching tools. eg: C => CIRCLE, COPY, ... - texts.push(...matchingToolNames); + if (matchingCommands.length) { + // 입력 중인 문자로 시작하는 명령 후보. 예: C => CIRCLE (C), COPY (CO) + texts.push(...matchingCommands); } this.drawListBelowInputField(drawController, texts); } public submitText(value: string) { - this.text = value.trim().toUpperCase(); + // 명령 해석은 대소문자를 가리지 않으므로 원문을 그대로 둔다 (문자 주석의 대소문자 보존) + this.text = value.trim(); this.handleEnterKey(); } @@ -399,13 +403,12 @@ export class InputController { } } - private getToolNamesFromPrefixText(): Tool[] { + /** 커서 옆에 띄울 명령 후보 (이름·별칭 접두사 일치) */ + private getCommandSuggestions(): string[] { if (this.text === '') { return []; } - return (Object.keys(TOOL_STATE_MACHINES).filter((cmd) => - cmd.startsWith(this.text.toUpperCase()) - ) || null) as Tool[]; + return matchCommandPrefixes(this.text).slice(0, 6).map(describeCommand); } public handleEnterKey() { @@ -436,21 +439,12 @@ export class InputController { value: this.text, } as TextInputEvent); this.text = ''; - } else if (this.getToolNamesFromPrefixText()[0]) { - // User entered a command. eg: L or LINE - const toolName = this.getToolNamesFromPrefixText()[0]; - - getActiveToolActor()?.stop(); - - const newToolActor = new Actor(TOOL_STATE_MACHINES[toolName]); - setActiveToolActor(newToolActor); - - console.log('SWITCH TO TOOL: ', { - toolName, - // biome-ignore lint/suspicious/noExplicitAny: - activeTool: (getActiveToolActor()?.src as any).config.context.type, - }); - + } else if (resolveCommandInput(this.text)) { + // 명령 이름 또는 AutoCAD 별칭을 입력했다. 예: L, LINE, REC + const command = resolveCommandInput(this.text); + if (command) { + runCommand(command); + } this.text = ''; } else if (NUMBER_REGEXP.test(this.text)) { console.log(' NUMBER_INPUT: ', { diff --git a/B07_DesignDetail/openwebcad/src/main.tsx b/B07_DesignDetail/openwebcad/src/main.tsx index bbc02783..98f6de97 100644 --- a/B07_DesignDetail/openwebcad/src/main.tsx +++ b/B07_DesignDetail/openwebcad/src/main.tsx @@ -9,6 +9,7 @@ import { draw } from './helpers/draw'; import { findClosestEntity } from './helpers/find-closest-entity'; import { scenePerf } from './helpers/scene-cache'; import { queryEntitiesNearPoint } from './helpers/spatial-index'; +import { registerCadDebugHook } from './helpers/debug-hook.ts'; import { getNewLayer } from './helpers/get-new-layer.ts'; import { trackHoveredSnapPoint } from './helpers/track-hovered-snap-points'; import { InputController } from './inputController/input-controller.ts'; @@ -33,7 +34,7 @@ import { } from './state'; import { syncThemeFromHost } from './theme.ts'; import { Tool } from './tools'; -import { TOOL_STATE_MACHINES } from './tools/tool.consts'; +import { TOOL_STATE_MACHINES } from './commands/registry'; import { ActorEvent, type DrawEvent } from './tools/tool.types'; // 호스트 앱의 화이트/블랙 모드를 먼저 붙인 뒤 렌더한다. @@ -153,6 +154,7 @@ function initApplication() { setLayers(layers); setActiveLayerId(layers[0].id); registerAisloDrawingBridge(); + registerCadDebugHook(); const screenCanvasDrawController = new ScreenCanvasDrawController(context); setScreenCanvasDrawController(screenCanvasDrawController); diff --git a/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx b/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx new file mode 100644 index 00000000..cbf9ad42 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx @@ -0,0 +1,148 @@ +/** 리본 렌더러 — ribbon.config.ts의 데이터와 명령 레지스트리만 보고 그린다. */ +import { type FC, type ReactNode, useState } from 'react'; +import { getActiveRibbonTab, setActiveRibbonTab } from '../components/ui-state'; +import type { CadCommand } from '../commands/command.types'; +import { getCommandById } from '../commands/registry'; +import { runCommand } from '../commands/run-command'; +import { type RibbonPanel, RIBBON_TABS, type RibbonWidget } from './ribbon.config'; +import type { Tool } from '../tools'; + +interface RibbonProps { + activeTool: Tool | null; + widgets: Partial>; + onCommandRun?: (command: CadCommand) => void; +} + +interface CommandButtonProps { + command: CadCommand; + size: 'big' | 'small'; + activeTool: Tool | null; + onRun: (command: CadCommand) => void; +} + +const CommandButton: FC = ({ command, size, activeTool, onRun }) => ( + +); + +const resolve = (ids: string[] | undefined): CadCommand[] => + (ids ?? []).map((id) => getCommandById(id)).filter((command): command is CadCommand => !!command); + +const Panel: FC<{ + panel: RibbonPanel; + activeTool: Tool | null; + widgets: Partial>; + onRun: (command: CadCommand) => void; +}> = ({ panel, activeTool, widgets, onRun }) => { + const [overflowOpen, setOverflowOpen] = useState(false); + const overflowCommands = resolve(panel.overflow); + + return ( +
+
+ {panel.widget ? ( + (widgets[panel.widget] ?? null) + ) : ( + <> + {resolve(panel.big).map((command) => ( + + ))} +
+ {resolve(panel.commands).map((command) => ( + + ))} +
+ + )} +
+ + {overflowOpen && overflowCommands.length > 0 && ( +
+ {overflowCommands.map((command) => ( + + ))} +
+ )} +
+ ); +}; + +export const Ribbon: FC = ({ activeTool, widgets, onCommandRun }) => { + const activeTabId = getActiveRibbonTab(); + const activeTab = RIBBON_TABS.find((tab) => tab.id === activeTabId) ?? RIBBON_TABS[0]; + + const handleRun = (command: CadCommand) => { + runCommand(command); + onCommandRun?.(command); + }; + + return ( + + ); +}; diff --git a/B07_DesignDetail/openwebcad/src/ribbon/ribbon.config.ts b/B07_DesignDetail/openwebcad/src/ribbon/ribbon.config.ts new file mode 100644 index 00000000..ae08bffd --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/ribbon/ribbon.config.ts @@ -0,0 +1,203 @@ +/** + * 리본 구성 — AutoCAD 2024 `제도 및 주석` 작업공간의 탭·패널 배치를 따른다. + * 여기에는 데이터만 둔다. 버튼이 무엇을 하는지는 commands 레지스트리가 안다. + */ + +export type RibbonWidget = 'properties' | 'layers' | 'textStyle'; + +export interface RibbonPanel { + label: string; + /** 크게 표시할 대표 명령 (AutoCAD의 큰 버튼) */ + big?: string[]; + /** 작은 버튼으로 나열할 명령 */ + commands?: string[]; + /** 패널 확장(▾)을 눌러야 나오는 명령 */ + overflow?: string[]; + /** 명령 버튼이 아닌 특수 위젯 패널 */ + widget?: RibbonWidget; +} + +export interface RibbonTab { + id: string; + label: string; + panels: RibbonPanel[]; +} + +export const RIBBON_TABS: RibbonTab[] = [ + { + id: 'home', + label: '홈', + panels: [ + { + label: '그리기', + big: ['LINE', 'PLINE'], + commands: ['CIRCLE', 'ARC', 'RECTANG', 'HATCH'], + overflow: [ + 'POLYGON', + 'POINT', + 'ELLIPSE', + 'SPLINE', + 'MLINE', + 'MLSTYLE', + 'XLINE', + 'RAY', + 'DONUT', + 'DIVIDE', + 'MEASURE', + 'GRADIENT', + 'BOUNDARY', + 'REGION', + 'WIPEOUT', + ], + }, + { + label: '수정', + big: ['MOVE', 'COPY'], + commands: ['ROTATE', 'MIRROR', 'TRIM', 'OFFSET'], + overflow: [ + 'SCALE', + 'ERASE', + 'ALIGN', + 'ARRAY', + 'EXTEND', + 'STRETCH', + 'FILLET', + 'CHAMFER', + 'BLEND', + 'BREAK', + 'BREAKATPOINT', + 'JOIN', + 'EXPLODE', + 'LENGTHEN', + 'PEDIT', + 'HATCHEDIT', + 'DRAWORDER', + 'DRAWORDERBACK', + 'MATCHPROP', + 'OVERKILL', + 'REVERSE', + 'SETBYLAYER', + 'ALIGNLEFT', + 'ALIGNCENTERH', + 'ALIGNRIGHT', + 'ALIGNTOP', + 'ALIGNCENTERV', + 'ALIGNBOTTOM', + ], + }, + { + label: '도면층', + widget: 'layers', + overflow: [ + 'LAYCURSET', + 'LAYOFF', + 'LAYON', + 'LAYFRZ', + 'LAYTHW', + 'LAYLCK', + 'LAYULK', + 'LAYISO', + 'LAYUNISO', + 'LAYERP', + 'LAYERSTATE', + 'LAYERSTATERESTORE', + 'LAYMCH', + 'LAYCUR', + 'LAYMRG', + 'LAYDEL', + 'LAYWALK', + ], + }, + { + label: '특성', + widget: 'properties', + overflow: ['PROPERTIES', 'QUICKPROPERTIES', 'COLOR', 'LINETYPE', 'LWEIGHT', 'TRANSPARENCY'], + }, + { label: '그룹', commands: ['GROUP', 'UNGROUP'] }, + { + label: '유틸리티', + big: ['SELECT'], + commands: ['DIST', 'AREA'], + overflow: [ + 'ID', + 'LIST', + 'QSELECT', + 'SELECTSIMILAR', + 'RADIUS', + 'ANGLE', + 'MEASUREGEOM', + 'QUICKCALC', + 'ISOLATEOBJECTS', + 'HIDEOBJECTS', + 'UNISOLATEOBJECTS', + ], + }, + { + label: '클립보드', + big: ['PASTECLIP'], + commands: ['COPYCLIP', 'CUTCLIP'], + overflow: ['COPYBASE', 'PASTEORIG', 'PASTEBLOCK'], + }, + ], + }, + { + id: 'insert', + label: '삽입', + panels: [{ label: '참조', commands: ['IMAGEATTACH'] }], + }, + { + id: 'annotate', + label: '주석', + panels: [ + { + label: '문자', + big: ['MTEXT'], + commands: ['TEXT', 'TEXTEDIT', 'FIND', 'STYLE'], + }, + { label: '문자 스타일', widget: 'textStyle' }, + { + label: '치수', + big: ['DIMLINEAR', 'DIMALIGNED'], + commands: ['DIM', 'DIMANGULAR', 'DIMRADIUS', 'DIMDIAMETER', 'DIMCONTINUE', 'QDIM'], + overflow: [ + 'DIMARC', + 'DIMORDINATE', + 'DIMBASELINE', + 'CENTERMARK', + 'CENTERLINE', + 'DIMSTYLE', + 'DIMUPDATE', + 'DIMSPACE', + ], + }, + { + label: '지시선', + big: ['MLEADER'], + commands: ['MLEADERSTYLE', 'MLEADERALIGN'], + }, + { label: '표', commands: ['TABLE', 'TABLESTYLE'] }, + { label: '표식', commands: ['REVCLOUD', 'ANNOSCALE'] }, + ], + }, + { + id: 'view', + label: '뷰', + panels: [ + { label: '탐색', big: ['ZOOM'], commands: ['ZOOMIN', 'ZOOMOUT', 'PAN', 'REGEN'] }, + ], + }, + { + id: 'output', + label: '출력', + panels: [ + { + label: '내보내기', + big: ['EXPORT'], + commands: ['EXPORTSVG', 'EXPORTPNG', 'QSAVE'], + }, + ], + }, +]; + +/** 제목표시줄의 빠른 실행 도구막대 */ +export const QUICK_ACCESS_COMMANDS: string[] = ['QSAVE', 'EXPORT', 'UNDO', 'REDO']; diff --git a/B07_DesignDetail/openwebcad/src/tools.ts b/B07_DesignDetail/openwebcad/src/tools.ts index f103c202..fe2d15ca 100644 --- a/B07_DesignDetail/openwebcad/src/tools.ts +++ b/B07_DesignDetail/openwebcad/src/tools.ts @@ -9,7 +9,6 @@ export enum Tool { ERASER = 'ERASER', IMAGE_IMPORT = 'IMAGE_IMPORT', ROTATE = 'ROTATE', - MEASUREMENT = 'MEASUREMENT', ALIGN_LEFT = 'ALIGN_LEFT', ALIGN_RIGHT = 'ALIGN_RIGHT', ALIGN_CENTER_HORIZONTAL = 'ALIGN_CENTER_HORIZONTAL', @@ -17,5 +16,105 @@ export enum Tool { ALIGN_BOTTOM = 'ALIGN_BOTTOM', ALIGN_CENTER_VERTICAL = 'ALIGN_CENTER_VERTICAL', ARRAY = 'ARRAY', - PEDIT = 'PEDIT' + PEDIT = 'PEDIT', + // 조사표 1절 — 그리기 + PLINE = 'PLINE', + ARC = 'ARC', + POLYGON = 'POLYGON', + ELLIPSE = 'ELLIPSE', + SPLINE = 'SPLINE', + MLINE = 'MLINE', + MLSTYLE = 'MLSTYLE', + XLINE = 'XLINE', + RAY = 'RAY', + POINT = 'POINT', + DONUT = 'DONUT', + DIVIDE = 'DIVIDE', + MEASURE_LENGTH = 'MEASURE_LENGTH', + HATCH = 'HATCH', + GRADIENT = 'GRADIENT', + BOUNDARY = 'BOUNDARY', + REGION = 'REGION', + WIPEOUT = 'WIPEOUT', + // 조사표 2절 — 수정 + MIRROR = 'MIRROR', + ALIGN = 'ALIGN', + OFFSET = 'OFFSET', + STRETCH = 'STRETCH', + LENGTHEN = 'LENGTHEN', + FILLET = 'FILLET', + CHAMFER = 'CHAMFER', + BLEND = 'BLEND', + BREAK = 'BREAK', + BREAK_AT_POINT = 'BREAK_AT_POINT', + EXTEND = 'EXTEND', + JOIN = 'JOIN', + EXPLODE = 'EXPLODE', + ERASE = 'ERASE', + MATCHPROP = 'MATCHPROP', + DRAWORDER_FRONT = 'DRAWORDER_FRONT', + DRAWORDER_BACK = 'DRAWORDER_BACK', + SETBYLAYER = 'SETBYLAYER', + REVERSE = 'REVERSE', + OVERKILL = 'OVERKILL', + HATCHEDIT = 'HATCHEDIT', + // 조사표 3절 — 도면층·특성·그룹·유틸리티 + LAYER_CURRENT = 'LAYER_CURRENT', + LAYOFF = 'LAYOFF', + LAYFRZ = 'LAYFRZ', + LAYLCK = 'LAYLCK', + LAYULK = 'LAYULK', + LAYISO = 'LAYISO', + LAYERSTATE = 'LAYERSTATE', + LAYERSTATE_RESTORE = 'LAYERSTATE_RESTORE', + LAYMCH = 'LAYMCH', + LAYCUR = 'LAYCUR', + LAYMRG = 'LAYMRG', + LAYDEL = 'LAYDEL', + TRANSPARENCY = 'TRANSPARENCY', + GROUP = 'GROUP', + UNGROUP = 'UNGROUP', + DIST = 'DIST', + RADIUS_INQUIRY = 'RADIUS_INQUIRY', + ANGLE_INQUIRY = 'ANGLE_INQUIRY', + AREA = 'AREA', + ID_POINT = 'ID_POINT', + QUICKCALC = 'QUICKCALC', + MEASUREGEOM = 'MEASUREGEOM', + QSELECT = 'QSELECT', + SELECTSIMILAR = 'SELECTSIMILAR', + ISOLATEOBJECTS = 'ISOLATEOBJECTS', + HIDEOBJECTS = 'HIDEOBJECTS', + COPYBASE = 'COPYBASE', + PASTECLIP = 'PASTECLIP', + // 조사표 5절 — 주석 + TEXT = 'TEXT', + MTEXT = 'MTEXT', + TEXTEDIT = 'TEXTEDIT', + FIND = 'FIND', + DIM = 'DIM', + DIMLINEAR = 'DIMLINEAR', + DIMALIGNED = 'DIMALIGNED', + DIMBASELINE = 'DIMBASELINE', + DIMCONTINUE = 'DIMCONTINUE', + QDIM = 'QDIM', + DIMRADIUS = 'DIMRADIUS', + DIMDIAMETER = 'DIMDIAMETER', + DIMANGULAR = 'DIMANGULAR', + DIMARC = 'DIMARC', + DIMORDINATE = 'DIMORDINATE', + CENTERMARK = 'CENTERMARK', + CENTERLINE = 'CENTERLINE', + DIMSPACE = 'DIMSPACE', + DIMSTYLE = 'DIMSTYLE', + MLEADER = 'MLEADER', + MLEADERSTYLE = 'MLEADERSTYLE', + MLEADERALIGN = 'MLEADERALIGN', + TABLE = 'TABLE', + TABLESTYLE = 'TABLESTYLE', + REVCLOUD = 'REVCLOUD', + ANNOSCALE = 'ANNOSCALE', + COLOR = 'COLOR', + LINETYPE = 'LINETYPE', + LWEIGHT = 'LWEIGHT' } diff --git a/B07_DesignDetail/openwebcad/src/tools/annotate/annotation.helpers.ts b/B07_DesignDetail/openwebcad/src/tools/annotate/annotation.helpers.ts new file mode 100644 index 00000000..fbdbf407 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/annotate/annotation.helpers.ts @@ -0,0 +1,66 @@ +/** 치수·지시선이 함께 쓰는 조각 만들기 (화살표·치수 문자·직전 치수 기억) */ +import { Point } from '@flatten-js/core'; +import { getDimArrowSize, getDimDecimals, getDimTextHeight } from '../../commands/dim-settings'; +import { ArrowHeadEntity } from '../../entities/ArrowHeadEntity'; +import type { Entity } from '../../entities/Entity'; +import type { MeasurementEntity } from '../../entities/MeasurementEntity'; +import { getActiveLayerId, getActiveLineColor, getScreenCanvasDrawController } from '../../state'; +import { textEntity } from '../factories/entity-factory'; + +/** 화면 px 기준 상수를 도면 단위로 바꾼다 (줌이 달라도 크기가 일정하게 보인다) */ +export function worldFactor(): number { + try { + return getScreenCanvasDrawController().getScreenScale() || 1; + } catch { + return 1; + } +} + +/** tip을 꼭짓점으로 하고 from 방향에서 들어오는 화살표 */ +export function arrowHead(tip: Point, from: Point, size = getDimArrowSize()): ArrowHeadEntity { + const length = size / worldFactor(); + const dx = tip.x - from.x; + const dy = tip.y - from.y; + const distance = Math.hypot(dx, dy) || 1; + const ux = dx / distance; + const uy = dy / distance; + const baseX = tip.x - ux * length; + const baseY = tip.y - uy * length; + const halfWidth = length * 0.35; + + const head = new ArrowHeadEntity( + getActiveLayerId(), + tip, + new Point(baseX - uy * halfWidth, baseY + ux * halfWidth), + new Point(baseX + uy * halfWidth, baseY - ux * halfWidth) + ); + head.lineColor = getActiveLineColor(); + head.fillColor = getActiveLineColor(); + return head; +} + +/** 치수 문자 — 설정한 소수 자릿수·문자 높이를 따른다 */ +export function dimensionText(value: number, position: Point, prefix = '', suffix = ''): Entity { + const label = `${prefix}${value.toFixed(getDimDecimals())}${suffix}`; + return textEntity(label, position, { + fontSize: getDimTextHeight() / worldFactor(), + textAlign: 'center', + }); +} + +/** 문자 그대로 찍는 주석 (좌표·각도처럼 단위가 다른 값) */ +export function annotationText(label: string, position: Point): Entity { + return textEntity(label, position, { + fontSize: getDimTextHeight() / worldFactor(), + textAlign: 'center', + }); +} + +/** DIMBASELINE·DIMCONTINUE가 이어 그릴 직전 치수 */ +let lastDimension: MeasurementEntity | null = null; + +export const rememberDimension = (dimension: MeasurementEntity): void => { + lastDimension = dimension; +}; + +export const getLastDimension = (): MeasurementEntity | null => lastDimension; diff --git a/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-radial-tools.ts b/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-radial-tools.ts new file mode 100644 index 00000000..8e094c2e --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-radial-tools.ts @@ -0,0 +1,256 @@ +/** 반지름·지름·각도·호길이·세로좌표 치수와 중심 표식 (조사표 5절) */ +import { Arc, Circle, Point } from '@flatten-js/core'; +import { toast } from 'react-toastify'; +import { getDimDecimals } from '../../commands/dim-settings'; +import type { Entity } from '../../entities/Entity'; +import { MeasurementEntity } from '../../entities/MeasurementEntity'; +import { polylineLength, sampleEntityPoints } from '../../helpers/geometry/sample-entity'; +import { addEntities, getEntities, setEntities, setSelectedEntityIds } from '../../state'; +import { Tool } from '../../tools'; +import { arcEntity, lineEntity } from '../factories/entity-factory'; +import { createSequenceTool } from '../factories/sequence-tool'; +import { annotationText, arrowHead, dimensionText } from './annotation.helpers'; + +interface CircularShape { + center: Point; + radius: number; +} + +/** 원·호에서 중심과 반지름을 꺼낸다 */ +function circularOf(entity: Entity): CircularShape | null { + const shape = entity.getShape(); + if (shape instanceof Circle) return { center: shape.center, radius: Number(shape.r) }; + if (shape instanceof Arc) return { center: shape.center, radius: Number(shape.r) }; + return null; +} + +function radialDimension(entity: Entity, textPoint: Point, diameter: boolean): Entity[] | null { + const circular = circularOf(entity); + if (!circular) return null; + + const angle = Math.atan2(textPoint.y - circular.center.y, textPoint.x - circular.center.x); + const edge = new Point( + circular.center.x + circular.radius * Math.cos(angle), + circular.center.y + circular.radius * Math.sin(angle) + ); + const start = diameter + ? new Point( + circular.center.x - circular.radius * Math.cos(angle), + circular.center.y - circular.radius * Math.sin(angle) + ) + : circular.center; + + return [ + lineEntity(start, textPoint), + arrowHead(edge, circular.center), + dimensionText( + diameter ? circular.radius * 2 : circular.radius, + textPoint, + diameter ? 'Ø' : 'R' + ), + ]; +} + +export const dimRadiusToolStateMachine = createSequenceTool({ + tool: Tool.DIMRADIUS, + steps: [ + { kind: 'entity', instructions: '반지름을 기입할 원 또는 호를 선택하십시오.' }, + { kind: 'point', instructions: '치수 문자 위치를 지정하십시오.' }, + ], + commit: (input) => { + const parts = radialDimension(input.entity(0), input.point(1), false); + if (!parts) { + toast.warn('원 또는 호를 선택하십시오.'); + return; + } + addEntities(parts, true); + }, +}); + +export const dimDiameterToolStateMachine = createSequenceTool({ + tool: Tool.DIMDIAMETER, + steps: [ + { kind: 'entity', instructions: '지름을 기입할 원 또는 호를 선택하십시오.' }, + { kind: 'point', instructions: '치수 문자 위치를 지정하십시오.' }, + ], + commit: (input) => { + const parts = radialDimension(input.entity(0), input.point(1), true); + if (!parts) { + toast.warn('원 또는 호를 선택하십시오.'); + return; + } + addEntities(parts, true); + }, +}); + +export const dimAngularToolStateMachine = createSequenceTool({ + tool: Tool.DIMANGULAR, + steps: [ + { kind: 'point', instructions: '각의 꼭짓점을 지정하십시오.' }, + { kind: 'point', instructions: '첫 번째 변 위의 점을 지정하십시오.' }, + { kind: 'point', instructions: '두 번째 변 위의 점을 지정하십시오.' }, + ], + commit: (input) => { + const [vertex, first, second] = input.points(); + const radius = Math.min(vertex.distanceTo(first)[0], vertex.distanceTo(second)[0]) * 0.6; + if (radius <= 0) return; + + const startAngle = Math.atan2(first.y - vertex.y, first.x - vertex.x); + const endAngle = Math.atan2(second.y - vertex.y, second.x - vertex.x); + const sweep = ((endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI)); + const midAngle = startAngle + sweep / 2; + const degrees = (sweep * 180) / Math.PI; + + addEntities( + [ + lineEntity(vertex, first), + lineEntity(vertex, second), + arcEntity({ + center: vertex, + radius, + startAngle, + endAngle, + counterClockwise: true, + }), + annotationText( + `${degrees.toFixed(getDimDecimals())}°`, + new Point( + vertex.x + radius * 1.25 * Math.cos(midAngle), + vertex.y + radius * 1.25 * Math.sin(midAngle) + ) + ), + ], + true + ); + }, +}); + +export const dimArcToolStateMachine = createSequenceTool({ + tool: Tool.DIMARC, + steps: [ + { kind: 'entity', instructions: '호 길이를 기입할 호를 선택하십시오.' }, + { kind: 'point', instructions: '치수 문자 위치를 지정하십시오.' }, + ], + commit: (input) => { + const entity = input.entity(0); + const circular = circularOf(entity); + if (!circular) { + toast.warn('호를 선택하십시오.'); + return; + } + const length = polylineLength(sampleEntityPoints(entity)); + addEntities([dimensionText(length, input.point(1), '⌒ ')], true); + }, +}); + +export const dimOrdinateToolStateMachine = createSequenceTool({ + tool: Tool.DIMORDINATE, + steps: [ + { kind: 'point', instructions: '좌표를 기입할 피처 위치를 지정하십시오.' }, + { kind: 'point', instructions: '지시선 끝점을 지정하십시오.' }, + ], + commit: (input) => { + const [feature, leaderEnd] = input.points(); + // 지시선이 세로로 길면 X좌표를, 가로로 길면 Y좌표를 기입한다 (AutoCAD와 같다) + const vertical = Math.abs(leaderEnd.y - feature.y) >= Math.abs(leaderEnd.x - feature.x); + const value = vertical ? feature.x : feature.y; + addEntities( + [ + lineEntity(feature, leaderEnd), + annotationText( + `${vertical ? 'X' : 'Y'} ${value.toFixed(getDimDecimals())}`, + new Point(leaderEnd.x, leaderEnd.y) + ), + ], + true + ); + }, +}); + +export const centerMarkToolStateMachine = createSequenceTool({ + tool: Tool.CENTERMARK, + steps: [{ kind: 'entity', instructions: '중심 표식을 넣을 원 또는 호를 선택하십시오.' }], + commit: (input) => { + const circular = circularOf(input.entity(0)); + if (!circular) { + toast.warn('원 또는 호를 선택하십시오.'); + return; + } + const size = circular.radius * 0.15; + addEntities( + [ + lineEntity( + new Point(circular.center.x - size, circular.center.y), + new Point(circular.center.x + size, circular.center.y) + ), + lineEntity( + new Point(circular.center.x, circular.center.y - size), + new Point(circular.center.x, circular.center.y + size) + ), + ], + true + ); + }, +}); + +export const centerLineToolStateMachine = createSequenceTool({ + tool: Tool.CENTERLINE, + helpers: false, + steps: [ + { kind: 'entity', instructions: '첫 번째 선을 선택하십시오.' }, + { kind: 'entity', instructions: '두 번째 선을 선택하십시오.' }, + ], + commit: (input) => { + const first = sampleEntityPoints(input.entity(0)); + const second = sampleEntityPoints(input.entity(1)); + if (first.length < 2 || second.length < 2) { + toast.warn('두 선을 선택하십시오.'); + return; + } + const middle = (a: Point, b: Point) => new Point((a.x + b.x) / 2, (a.y + b.y) / 2); + const line = lineEntity( + middle(first[0], second[0]), + middle(first[first.length - 1], second[second.length - 1]) + ); + line.lineDash = [12, 4, 2, 4]; + addEntities([line], true); + }, +}); + +/** DIMSPACE — 선택한 치수들의 치수선 간격을 고르게 맞춘다 */ +export const dimSpaceToolStateMachine = createSequenceTool({ + tool: Tool.DIMSPACE, + helpers: false, + steps: [ + { kind: 'selection', instructions: '간격을 맞출 치수를 선택한 뒤 ENTER.' }, + { kind: 'number', instructions: '치수선 간격을 입력하십시오 <10>.', defaultValue: 10 }, + ], + commit: (input) => { + const dimensions = input + .entities(0) + .filter((entity): entity is MeasurementEntity => entity instanceof MeasurementEntity); + if (dimensions.length < 2) { + toast.warn('치수를 두 개 이상 선택하십시오.'); + return; + } + const spacing = input.number(1); + const base = dimensions[0]; + const baseStart = base.getStartPoint(); + const baseOffset = base.getOffsetPoint(); + const direction = new Point(baseOffset.x - baseStart.x, baseOffset.y - baseStart.y); + const length = Math.hypot(direction.x, direction.y) || 1; + + dimensions.forEach((dimension, index) => { + if (index === 0) return; + const start = dimension.getStartPoint(); + dimension.setOffsetPoint( + new Point( + start.x + (direction.x / length) * (length + spacing * index), + start.y + (direction.y / length) * (length + spacing * index) + ) + ); + }); + setEntities([...getEntities()], true); + setSelectedEntityIds([]); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-tools.ts b/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-tools.ts new file mode 100644 index 00000000..c29a53da --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-tools.ts @@ -0,0 +1,163 @@ +/** 선형 치수 계열 — 선형·정렬·기준선·연속·빠른 치수·자동 치수 (조사표 5절) */ +import { Circle, Point } from '@flatten-js/core'; +import { toast } from 'react-toastify'; +import { EntityName } from '../../entities/Entity'; +import { MeasurementEntity } from '../../entities/MeasurementEntity'; +import { sampleEntityPoints } from '../../helpers/geometry/sample-entity'; +import { addEntities, getActiveLayerId, setSelectedEntityIds } from '../../state'; +import { Tool } from '../../tools'; +import { styled } from '../factories/entity-factory'; +import { createSequenceTool } from '../factories/sequence-tool'; +import { getLastDimension, rememberDimension } from './annotation.helpers'; + +/** 치수 객체 하나 만들기 — 현재 선 특성을 입히고 직전 치수로 기억한다 */ +function makeDimension(start: Point, end: Point, offset: Point): MeasurementEntity { + const dimension = styled(new MeasurementEntity(getActiveLayerId(), start, end, offset)); + rememberDimension(dimension); + return dimension; +} + +/** + * 선형 치수 — 치수선을 놓은 방향으로 수평/수직을 고른다. + * 위·아래에 놓으면 가로 거리, 좌·우에 놓으면 세로 거리를 잰다 (AutoCAD와 같다). + */ +function projectForLinear(start: Point, end: Point, offset: Point): [Point, Point] { + const horizontalSpan = Math.abs(end.x - start.x); + const verticalSpan = Math.abs(end.y - start.y); + const offsetIsVertical = + Math.abs(offset.y - (start.y + end.y) / 2) >= Math.abs(offset.x - (start.x + end.x) / 2); + if (offsetIsVertical || horizontalSpan >= verticalSpan) { + return [start, new Point(end.x, start.y)]; + } + return [start, new Point(start.x, end.y)]; +} + +export const dimLinearToolStateMachine = createSequenceTool({ + tool: Tool.DIMLINEAR, + steps: [ + { kind: 'point', instructions: '첫 번째 치수보조선 원점을 지정하십시오.' }, + { kind: 'point', instructions: '두 번째 치수보조선 원점을 지정하십시오.' }, + { kind: 'point', instructions: '치수선 위치를 지정하십시오.' }, + ], + preview: (input) => { + const points = input.points(); + if (points.length !== 2) return []; + const [start, end] = projectForLinear(points[0], points[1], input.cursor); + return [new MeasurementEntity(getActiveLayerId(), start, end, input.cursor)]; + }, + commit: (input) => { + const [first, second, offset] = input.points(); + const [start, end] = projectForLinear(first, second, offset); + addEntities([makeDimension(start, end, offset)], true); + }, +}); + +export const dimAlignedToolStateMachine = createSequenceTool({ + tool: Tool.DIMALIGNED, + steps: [ + { kind: 'point', instructions: '첫 번째 치수보조선 원점을 지정하십시오.' }, + { kind: 'point', instructions: '두 번째 치수보조선 원점을 지정하십시오.' }, + { kind: 'point', instructions: '치수선 위치를 지정하십시오.' }, + ], + preview: (input) => { + const points = input.points(); + if (points.length !== 2) return []; + return [new MeasurementEntity(getActiveLayerId(), points[0], points[1], input.cursor)]; + }, + commit: (input) => { + const [start, end, offset] = input.points(); + addEntities([makeDimension(start, end, offset)], true); + }, +}); + +export const dimBaselineToolStateMachine = createSequenceTool({ + tool: Tool.DIMBASELINE, + steps: [{ kind: 'point', instructions: '다음 치수보조선 원점을 지정하십시오.' }], + commit: (input) => { + const previous = getLastDimension(); + if (!previous) { + toast.warn('먼저 치수를 하나 작성하십시오.'); + return; + } + const start = previous.getStartPoint(); + const offset = previous.getOffsetPoint(); + // 기준선 치수는 같은 시작점에서 재고, 치수선을 한 칸 더 띄운다 + const spacing = offset.distanceTo(start)[0] * 0.35 || 10; + const direction = new Point(offset.x - start.x, offset.y - start.y); + const length = Math.hypot(direction.x, direction.y) || 1; + const nextOffset = new Point( + offset.x + (direction.x / length) * spacing, + offset.y + (direction.y / length) * spacing + ); + addEntities([makeDimension(start, input.point(0), nextOffset)], true); + }, +}); + +export const dimContinueToolStateMachine = createSequenceTool({ + tool: Tool.DIMCONTINUE, + steps: [{ kind: 'point', instructions: '다음 치수보조선 원점을 지정하십시오.' }], + commit: (input) => { + const previous = getLastDimension(); + if (!previous) { + toast.warn('먼저 치수를 하나 작성하십시오.'); + return; + } + addEntities( + [makeDimension(previous.getEndPoint(), input.point(0), previous.getOffsetPoint())], + true + ); + }, +}); + +export const qDimToolStateMachine = createSequenceTool({ + tool: Tool.QDIM, + helpers: false, + steps: [ + { kind: 'selection', instructions: '치수를 넣을 객체를 선택한 뒤 ENTER.' }, + { kind: 'point', instructions: '치수선 위치를 지정하십시오.' }, + ], + commit: (input) => { + const offset = input.point(1); + const dimensions = input + .entities(0) + .map((entity) => { + const points = sampleEntityPoints(entity); + if (points.length < 2) return null; + return makeDimension(points[0], points[points.length - 1], offset); + }) + .filter((dimension): dimension is MeasurementEntity => !!dimension); + if (!dimensions.length) { + toast.warn('치수를 넣을 수 있는 객체가 없습니다.'); + return; + } + addEntities(dimensions, true); + setSelectedEntityIds([]); + }, +}); + +/** DIM — 선택한 객체 종류에 맞는 치수를 자동으로 고른다 */ +export const dimAutoToolStateMachine = createSequenceTool({ + tool: Tool.DIM, + helpers: false, + steps: [ + { kind: 'entity', instructions: '치수를 넣을 객체를 선택하십시오.' }, + { kind: 'point', instructions: '치수선 위치를 지정하십시오.' }, + ], + commit: (input) => { + const entity = input.entity(0); + const offset = input.point(1); + const shape = entity.getShape(); + + if (shape instanceof Circle || entity.getType() === EntityName.Arc) { + // 원·호는 반지름 치수 명령이 더 알맞다 + toast.info('원·호에는 DIMRADIUS 또는 DIMDIAMETER를 사용하십시오.'); + return; + } + const points = sampleEntityPoints(entity); + if (points.length < 2) { + toast.warn('치수를 넣을 수 없는 객체입니다.'); + return; + } + addEntities([makeDimension(points[0], points[points.length - 1], offset)], true); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/annotate/leader-table-tools.ts b/B07_DesignDetail/openwebcad/src/tools/annotate/leader-table-tools.ts new file mode 100644 index 00000000..d0c46cf4 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/annotate/leader-table-tools.ts @@ -0,0 +1,209 @@ +/** 지시선·표·구름형·스타일 설정 (조사표 5절 지시선·표·표식·주석 축척) */ +import { Point } from '@flatten-js/core'; +import { toast } from 'react-toastify'; +import { + getAnnotationScale, + getDimTextHeight, + getTableColumnWidth, + getTableRowHeight, + setAnnotationScale, + setDimStyle, + setTableStyle, +} from '../../commands/dim-settings'; +import type { Entity } from '../../entities/Entity'; +import { EntityName } from '../../entities/Entity'; +import type { TextEntity } from '../../entities/TextEntity'; +import { revisionCloudPoints } from '../../helpers/geometry/shape-points'; +import { addEntities, getEntities, setEntities, setSelectedEntityIds } from '../../state'; +import { Tool } from '../../tools'; +import { lineEntity, polyLineEntity } from '../factories/entity-factory'; +import { createSequenceTool } from '../factories/sequence-tool'; +import { annotationText, arrowHead, worldFactor } from './annotation.helpers'; + +export const mleaderToolStateMachine = createSequenceTool({ + tool: Tool.MLEADER, + steps: [ + { kind: 'point', instructions: '지시선 화살표 위치를 지정하십시오.' }, + { kind: 'point', instructions: '지시선 꺾임점을 지정하십시오.' }, + { kind: 'text', instructions: '지시선 문자를 입력하십시오.' }, + ], + preview: (input) => { + const points = input.points(); + if (points.length !== 1) return []; + return [lineEntity(points[0], input.cursor), arrowHead(points[0], input.cursor)]; + }, + commit: (input) => { + const [tip, knee] = input.points(); + const textHeight = getDimTextHeight() / worldFactor(); + // 꺾임점에서 문자 쪽으로 짧은 가로선을 하나 더 뽑는다 (AutoCAD 지시선 모양) + const landingLength = textHeight * 2; + const toRight = knee.x >= tip.x; + const landingEnd = new Point(knee.x + (toRight ? landingLength : -landingLength), knee.y); + const label = annotationText(input.text(2), new Point(landingEnd.x, landingEnd.y + textHeight * 0.4)); + const groupId = crypto.randomUUID(); + const parts: Entity[] = [ + lineEntity(tip, knee), + lineEntity(knee, landingEnd), + arrowHead(tip, knee), + label, + ]; + for (const part of parts) part.groupId = groupId; + addEntities(parts, true); + }, +}); + +export const mleaderStyleToolStateMachine = createSequenceTool({ + tool: Tool.MLEADERSTYLE, + helpers: false, + steps: [ + { kind: 'number', instructions: '지시선 문자 높이를 입력하십시오 <16>.', defaultValue: 16 }, + { kind: 'number', instructions: '화살표 크기를 입력하십시오 <20>.', defaultValue: 20 }, + ], + commit: (input) => { + setDimStyle(input.number(0), input.number(1), -1); + toast.success(`지시선 스타일: 문자 ${input.number(0)} · 화살표 ${input.number(1)}`); + }, +}); + +/** 지시선 정렬 — 선택한 지시선 문자의 X 위치를 첫 문자에 맞춘다 */ +export const mleaderAlignToolStateMachine = createSequenceTool({ + tool: Tool.MLEADERALIGN, + helpers: false, + steps: [{ kind: 'selection', instructions: '정렬할 지시선 문자를 선택한 뒤 ENTER.' }], + commit: (input) => { + const texts = input + .entities(0) + .filter((entity) => entity.getType() === EntityName.Text) as TextEntity[]; + if (texts.length < 2) { + toast.warn('지시선 문자를 두 개 이상 선택하십시오.'); + return; + } + const targetX = texts[0].getBoundingBox().xmin; + for (const text of texts.slice(1)) { + text.move(targetX - text.getBoundingBox().xmin, 0); + } + setEntities([...getEntities()], true); + setSelectedEntityIds([]); + toast.success(`${texts.length}개 지시선 문자를 정렬했습니다.`); + }, +}); + +export const dimStyleToolStateMachine = createSequenceTool({ + tool: Tool.DIMSTYLE, + helpers: false, + steps: [ + { kind: 'number', instructions: '치수 문자 높이를 입력하십시오 <16>.', defaultValue: 16 }, + { kind: 'number', instructions: '화살표 크기를 입력하십시오 <20>.', defaultValue: 20 }, + { kind: 'number', instructions: '소수 자릿수를 입력하십시오 <2>.', defaultValue: 2 }, + ], + commit: (input) => { + setDimStyle(input.number(0), input.number(1), input.number(2)); + setEntities([...getEntities()], false); + toast.success('치수 스타일을 바꿨습니다.'); + }, +}); + +/** 치수 업데이트 — 스타일을 바꾼 뒤 화면을 다시 그린다 */ +export function updateDimensions(): string { + setEntities([...getEntities()], false); + toast.success('치수를 현재 스타일로 갱신했습니다.'); + return '치수 업데이트'; +} + +export const annotationScaleToolStateMachine = createSequenceTool({ + tool: Tool.ANNOSCALE, + helpers: false, + steps: [{ kind: 'number', instructions: '주석 축척 배율을 입력하십시오 <1>.', defaultValue: 1 }], + commit: (input) => { + setAnnotationScale(input.number(0)); + setEntities([...getEntities()], false); + toast.success(`주석 축척 ${getAnnotationScale()}배`); + }, +}); + +export const tableStyleToolStateMachine = createSequenceTool({ + tool: Tool.TABLESTYLE, + helpers: false, + steps: [ + { kind: 'number', instructions: '열 너비를 입력하십시오 <40>.', defaultValue: 40 }, + { kind: 'number', instructions: '행 높이를 입력하십시오 <10>.', defaultValue: 10 }, + ], + commit: (input) => { + setTableStyle(input.number(0), input.number(1)); + toast.success(`표 스타일: 열 ${input.number(0)} · 행 ${input.number(1)}`); + }, +}); + +export const tableToolStateMachine = createSequenceTool({ + tool: Tool.TABLE, + steps: [ + { kind: 'number', instructions: '열 수를 입력하십시오 <3>.', defaultValue: 3 }, + { kind: 'number', instructions: '행 수를 입력하십시오 <3>.', defaultValue: 3 }, + { kind: 'point', instructions: '표의 좌측 상단 삽입점을 지정하십시오.' }, + ], + commit: (input) => { + const columns = Math.max(1, Math.round(input.number(0))); + const rows = Math.max(1, Math.round(input.number(1))); + const origin = input.point(2); + const width = getTableColumnWidth(); + const height = getTableRowHeight(); + const groupId = crypto.randomUUID(); + const parts: Entity[] = []; + + for (let column = 0; column <= columns; column++) { + parts.push( + lineEntity( + new Point(origin.x + column * width, origin.y), + new Point(origin.x + column * width, origin.y - rows * height) + ) + ); + } + for (let row = 0; row <= rows; row++) { + parts.push( + lineEntity( + new Point(origin.x, origin.y - row * height), + new Point(origin.x + columns * width, origin.y - row * height) + ) + ); + } + for (const part of parts) part.groupId = groupId; + addEntities(parts, true); + toast.info('표를 만들었습니다. 칸 내용은 문자(TEXT) 명령으로 채우십시오.'); + }, +}); + +export const revCloudToolStateMachine = createSequenceTool({ + tool: Tool.REVCLOUD, + steps: [ + { kind: 'point', instructions: '구름형 경로의 첫 점을 지정하십시오.' }, + { kind: 'point', instructions: '다음 점을 지정하십시오. ENTER로 닫습니다.' }, + ], + repeatLastStep: true, + preview: (input) => { + const preview = polyLineEntity([...input.points(), input.cursor]); + return preview ? [preview] : []; + }, + commit: (input) => { + const points = input.points(); + if (points.length < 2) return; + const closed = [...points, points[0].clone()]; + const box = closed.reduce( + (size, point) => ({ + minX: Math.min(size.minX, point.x), + minY: Math.min(size.minY, point.y), + maxX: Math.max(size.maxX, point.x), + maxY: Math.max(size.maxY, point.y), + }), + { + minX: Number.POSITIVE_INFINITY, + minY: Number.POSITIVE_INFINITY, + maxX: Number.NEGATIVE_INFINITY, + maxY: Number.NEGATIVE_INFINITY, + } + ); + // 스캘럽 크기는 구름 크기에 비례시켜 어떤 축척에서도 비슷하게 보이게 한다 + const arcRadius = Math.max(Math.hypot(box.maxX - box.minX, box.maxY - box.minY) / 40, 1e-6); + const cloud = polyLineEntity(revisionCloudPoints(closed, arcRadius)); + if (cloud) addEntities([cloud], true); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/annotate/text-tools.ts b/B07_DesignDetail/openwebcad/src/tools/annotate/text-tools.ts new file mode 100644 index 00000000..00305825 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/annotate/text-tools.ts @@ -0,0 +1,103 @@ +/** 문자 명령 — 여러 줄 문자·단일 행 문자·편집·찾기 (조사표 5절 문자 패널) */ +import { Point } from '@flatten-js/core'; +import { toast } from 'react-toastify'; +import { getAnnotationScale } from '../../commands/dim-settings'; +import { EntityName } from '../../entities/Entity'; +import type { Entity } from '../../entities/Entity'; +import type { TextEntity } from '../../entities/TextEntity'; +import { addEntities, getActiveTextStyle, getEntities, setEntities } from '../../state'; +import { Tool } from '../../tools'; +import { textEntity } from '../factories/entity-factory'; +import { createSequenceTool } from '../factories/sequence-tool'; + +/** AutoCAD 여러 줄 문자의 줄바꿈 표기(\P)와 \n을 모두 받는다 */ +const splitLines = (text: string): string[] => + text + .replace(/\\P/gi, '\n') + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0); + +function buildTextLines(lines: string[], basePoint: Point): Entity[] { + const lineHeight = getActiveTextStyle().fontSize * getAnnotationScale() * 1.35; + const groupId = lines.length > 1 ? crypto.randomUUID() : undefined; + return lines.map((line, index) => { + const entity = textEntity(line, new Point(basePoint.x, basePoint.y - lineHeight * index), { + textAlign: 'left', + fontSize: getActiveTextStyle().fontSize * getAnnotationScale(), + }); + entity.groupId = groupId; + return entity; + }); +} + +export const textToolStateMachine = createSequenceTool({ + tool: Tool.TEXT, + steps: [ + { kind: 'point', instructions: '문자의 시작점을 지정하십시오.' }, + { kind: 'text', instructions: '문자를 입력하십시오.' }, + ], + commit: (input) => { + const lines = splitLines(input.text(1)); + if (!lines.length) return; + addEntities(buildTextLines(lines.slice(0, 1), input.point(0)), true); + }, +}); + +export const mtextToolStateMachine = createSequenceTool({ + tool: Tool.MTEXT, + steps: [ + { kind: 'point', instructions: '여러 줄 문자의 첫 코너를 지정하십시오.' }, + { kind: 'text', instructions: '문자를 입력하십시오 (줄바꿈은 \\P).' }, + ], + commit: (input) => { + const lines = splitLines(input.text(1)); + if (!lines.length) return; + addEntities(buildTextLines(lines, input.point(0)), true); + }, +}); + +export const textEditToolStateMachine = createSequenceTool({ + tool: Tool.TEXTEDIT, + helpers: false, + steps: [ + { kind: 'entity', instructions: '편집할 문자를 선택하십시오.' }, + { kind: 'text', instructions: '새 문자를 입력하십시오.' }, + ], + commit: (input) => { + const entity = input.entity(0); + if (entity.getType() !== EntityName.Text) { + toast.warn('문자 객체를 선택하십시오.'); + return; + } + (entity as TextEntity).setLabel(input.text(1)); + setEntities([...getEntities()], true); + }, +}); + +export const findToolStateMachine = createSequenceTool({ + tool: Tool.FIND, + helpers: false, + steps: [ + { kind: 'text', instructions: '찾을 문자열을 입력하십시오.' }, + { kind: 'text', instructions: '바꿀 문자열을 입력하십시오 (그대로 두려면 ENTER).', defaultValue: '' }, + ], + commit: (input) => { + const needle = input.text(0); + const replacement = input.text(1); + let found = 0; + let replaced = 0; + for (const entity of getEntities()) { + if (entity.getType() !== EntityName.Text) continue; + const text = entity as TextEntity; + if (!text.getLabel().includes(needle)) continue; + found += 1; + if (replacement) { + text.setLabel(text.getLabel().split(needle).join(replacement)); + replaced += 1; + } + } + if (replaced) setEntities([...getEntities()], true); + toast.info(replaced ? `${replaced}개 문자를 바꿨습니다.` : `${found}개 문자를 찾았습니다.`); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/draw/basic-draw-tools.ts b/B07_DesignDetail/openwebcad/src/tools/draw/basic-draw-tools.ts new file mode 100644 index 00000000..69be6e07 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/draw/basic-draw-tools.ts @@ -0,0 +1,149 @@ +/** 그리기 명령 — 폴리선·호·다각형·타원·스플라인·점 (조사표 1절) */ +import { CircleEntity } from '../../entities/CircleEntity'; +import type { Entity } from '../../entities/Entity'; +import { + arcThroughThreePoints, + ellipsePoints, + regularPolygonPoints, + splinePoints, +} from '../../helpers/geometry/shape-points'; +import { addEntities, getActiveLayerId } from '../../state'; +import { Tool } from '../../tools'; +import { arcEntity, lineEntity, pointEntity, polyLineEntity, styled } from '../factories/entity-factory'; +import { createSequenceTool } from '../factories/sequence-tool'; + +export const plineToolStateMachine = createSequenceTool({ + tool: Tool.PLINE, + steps: [ + { kind: 'point', instructions: '폴리선의 시작점을 지정하십시오.' }, + { kind: 'point', instructions: '다음 점을 지정하십시오. ENTER로 종료합니다.' }, + ], + repeatLastStep: true, + preview: (input) => { + const preview = polyLineEntity([...input.points(), input.cursor]); + return preview ? [preview] : []; + }, + commit: (input) => { + const polyline = polyLineEntity(input.points()); + if (polyline) addEntities([polyline], true); + }, +}); + +export const arcToolStateMachine = createSequenceTool({ + tool: Tool.ARC, + steps: [ + { kind: 'point', instructions: '호의 시작점을 지정하십시오.' }, + { kind: 'point', instructions: '호가 지나갈 두 번째 점을 지정하십시오.' }, + { kind: 'point', instructions: '호의 끝점을 지정하십시오.' }, + ], + preview: (input) => { + const points = input.points(); + if (points.length === 1) return [lineEntity(points[0], input.cursor)]; + if (points.length === 2) { + const definition = arcThroughThreePoints(points[0], points[1], input.cursor); + return definition ? [arcEntity(definition)] : [lineEntity(points[0], input.cursor)]; + } + return []; + }, + commit: (input) => { + const [start, through, end] = input.points(); + const definition = arcThroughThreePoints(start, through, end); + // 세 점이 일직선이면 호가 성립하지 않으므로 선으로 대체한다 + addEntities([definition ? arcEntity(definition) : lineEntity(start, end)], true); + }, +}); + +export const polygonToolStateMachine = createSequenceTool({ + tool: Tool.POLYGON, + steps: [ + { kind: 'number', instructions: '면의 수를 입력하십시오 <6>.', defaultValue: 6 }, + { kind: 'point', instructions: '다각형의 중심을 지정하십시오.' }, + { kind: 'point', instructions: '꼭짓점을 지정하십시오 (중심에서의 반지름).' }, + ], + preview: (input) => { + const points = input.points(); + if (points.length !== 1) return []; + const polygon = polyLineEntity(regularPolygonPoints(points[0], input.cursor, input.number(0))); + return polygon ? [polygon] : []; + }, + commit: (input) => { + const [center, vertex] = input.points(); + const polygon = polyLineEntity(regularPolygonPoints(center, vertex, input.number(0))); + if (polygon) addEntities([polygon], true); + }, +}); + +export const ellipseToolStateMachine = createSequenceTool({ + tool: Tool.ELLIPSE, + steps: [ + { kind: 'point', instructions: '타원의 중심을 지정하십시오.' }, + { kind: 'point', instructions: '장축의 끝점을 지정하십시오.' }, + { kind: 'point', instructions: '단축 거리를 지정하십시오.' }, + ], + preview: (input) => { + const points = input.points(); + if (points.length === 1) return [lineEntity(points[0], input.cursor)]; + if (points.length === 2) { + const minor = points[0].distanceTo(input.cursor)[0]; + const ellipse = polyLineEntity(ellipsePoints(points[0], points[1], minor)); + return ellipse ? [ellipse] : []; + } + return []; + }, + commit: (input) => { + const [center, majorPoint, minorPoint] = input.points(); + const minorRadius = center.distanceTo(minorPoint)[0]; + const ellipse = polyLineEntity(ellipsePoints(center, majorPoint, minorRadius)); + if (ellipse) addEntities([ellipse], true); + }, +}); + +export const splineToolStateMachine = createSequenceTool({ + tool: Tool.SPLINE, + steps: [ + { kind: 'point', instructions: '스플라인의 첫 점을 지정하십시오.' }, + { kind: 'point', instructions: '다음 점을 지정하십시오. ENTER로 종료합니다.' }, + ], + repeatLastStep: true, + preview: (input) => { + const preview = polyLineEntity(splinePoints([...input.points(), input.cursor])); + return preview ? [preview] : []; + }, + commit: (input) => { + const spline = polyLineEntity(splinePoints(input.points())); + if (spline) addEntities([spline], true); + }, +}); + +export const pointToolStateMachine = createSequenceTool({ + tool: Tool.POINT, + steps: [{ kind: 'point', instructions: '점의 위치를 지정하십시오.' }], + preview: (input) => [pointEntity(input.cursor)], + commit: (input) => { + addEntities([pointEntity(input.point(0))], true); + }, +}); + +/** 도넛 — 안쪽·바깥쪽 지름을 받아 동심원 두 개를 놓는다 */ +export const donutToolStateMachine = createSequenceTool({ + tool: Tool.DONUT, + steps: [ + { kind: 'number', instructions: '도넛의 내부 지름을 입력하십시오 <1>.', defaultValue: 1 }, + { kind: 'number', instructions: '도넛의 외부 지름을 입력하십시오 <2>.', defaultValue: 2 }, + { kind: 'point', instructions: '도넛의 중심을 지정하십시오.' }, + ], + preview: (input) => donutEntities(input.number(0), input.number(1), input.cursor), + commit: (input) => { + addEntities(donutEntities(input.number(0), input.number(1), input.point(2)), true); + }, +}); + +function donutEntities(innerDiameter: number, outerDiameter: number, center: Parameters[0]): Entity[] { + const circles: Entity[] = []; + for (const diameter of [innerDiameter, outerDiameter]) { + if (diameter > 0) { + circles.push(styled(new CircleEntity(getActiveLayerId(), center, diameter / 2))); + } + } + return circles; +} diff --git a/B07_DesignDetail/openwebcad/src/tools/draw/construction-tools.ts b/B07_DesignDetail/openwebcad/src/tools/draw/construction-tools.ts new file mode 100644 index 00000000..01209cce --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/draw/construction-tools.ts @@ -0,0 +1,142 @@ +/** 구성선·광선·다중선·와이프아웃 (조사표 1절 후반) */ +import { Point } from '@flatten-js/core'; +import { toast } from 'react-toastify'; +import type { Entity } from '../../entities/Entity'; +import { getMlineElements, getMlineSpacing, setMlineStyle } from '../../commands/draw-settings'; +import { offsetPolylinePoints } from '../../helpers/geometry/shape-points'; +import { addEntities, getScreenCanvasDrawController } from '../../state'; +import { Tool } from '../../tools'; +import { hatchEntity, lineEntity, polyLineEntity } from '../factories/entity-factory'; +import { createSequenceTool } from '../factories/sequence-tool'; + +/** + * 구성선은 원래 무한하지만, 무한 선은 경계상자를 망가뜨려 [범위 줌]을 못 쓰게 만든다. + * 현재 화면 대각선의 20배로 그어 화면 안에서는 무한선처럼 보이게 한다. + */ +function constructionLength(): number { + const controller = getScreenCanvasDrawController(); + const size = controller.getCanvasSize(); + const scale = controller.getScreenScale() || 1; + return (Math.hypot(size.x, size.y) / scale) * 20; +} + +function extendFrom(base: Point, through: Point, bothWays: boolean): [Point, Point] { + const dx = through.x - base.x; + const dy = through.y - base.y; + const length = Math.hypot(dx, dy) || 1; + const reach = constructionLength(); + const forward = new Point(base.x + (dx / length) * reach, base.y + (dy / length) * reach); + const backward = bothWays + ? new Point(base.x - (dx / length) * reach, base.y - (dy / length) * reach) + : base; + return [backward, forward]; +} + +export const xlineToolStateMachine = createSequenceTool({ + tool: Tool.XLINE, + steps: [ + { kind: 'point', instructions: '구성선이 지날 점을 지정하십시오.' }, + { kind: 'point', instructions: '방향을 지정하십시오.' }, + ], + preview: (input) => { + const points = input.points(); + if (points.length !== 1) return []; + const [start, end] = extendFrom(points[0], input.cursor, true); + return [lineEntity(start, end)]; + }, + commit: (input) => { + const [base, through] = input.points(); + const [start, end] = extendFrom(base, through, true); + addEntities([lineEntity(start, end)], true); + }, +}); + +export const rayToolStateMachine = createSequenceTool({ + tool: Tool.RAY, + steps: [ + { kind: 'point', instructions: '광선의 시작점을 지정하십시오.' }, + { kind: 'point', instructions: '통과점을 지정하십시오.' }, + ], + preview: (input) => { + const points = input.points(); + if (points.length !== 1) return []; + const [start, end] = extendFrom(points[0], input.cursor, false); + return [lineEntity(start, end)]; + }, + commit: (input) => { + const [base, through] = input.points(); + const [start, end] = extendFrom(base, through, false); + addEntities([lineEntity(start, end)], true); + }, +}); + +/** 다중선 — 중심선을 기준으로 MLSTYLE의 요소 수·간격만큼 평행선을 만든다 */ +function mlineEntities(points: Point[]): Entity[] { + if (points.length < 2) return []; + const elements = getMlineElements(); + const spacing = getMlineSpacing(); + const result: Entity[] = []; + for (let index = 0; index < elements; index++) { + const offset = (index - (elements - 1) / 2) * spacing; + const line = polyLineEntity(offsetPolylinePoints(points, offset)); + if (line) result.push(line); + } + return result; +} + +export const mlineToolStateMachine = createSequenceTool({ + tool: Tool.MLINE, + steps: [ + { kind: 'point', instructions: '다중선의 시작점을 지정하십시오.' }, + { kind: 'point', instructions: '다음 점을 지정하십시오. ENTER로 종료합니다.' }, + ], + repeatLastStep: true, + preview: (input) => mlineEntities([...input.points(), input.cursor]), + commit: (input) => { + const entities = mlineEntities(input.points()); + if (entities.length) addEntities(entities, true); + }, +}); + +export const mlstyleToolStateMachine = createSequenceTool({ + tool: Tool.MLSTYLE, + helpers: false, + steps: [ + { kind: 'number', instructions: '다중선 요소 수를 입력하십시오 <2>.', defaultValue: 2 }, + { kind: 'number', instructions: '요소 간격을 입력하십시오 <1>.', defaultValue: 1 }, + ], + commit: (input) => { + setMlineStyle(input.number(0), input.number(1)); + toast.success(`다중선 스타일: ${getMlineElements()}줄, 간격 ${getMlineSpacing()}`); + }, +}); + +/** 캔버스 배경색 — 와이프아웃은 이 색으로 뒤 객체를 가린다 */ +function canvasBackgroundColor(): string { + const value = getComputedStyle(document.documentElement).getPropertyValue('--cad-canvas').trim(); + return value || '#1e1e1e'; +} + +export const wipeoutToolStateMachine = createSequenceTool({ + tool: Tool.WIPEOUT, + steps: [ + { kind: 'point', instructions: '와이프아웃 경계의 첫 점을 지정하십시오.' }, + { kind: 'point', instructions: '다음 점을 지정하십시오. ENTER로 닫습니다.' }, + ], + repeatLastStep: true, + preview: (input) => { + const preview = polyLineEntity([...input.points(), input.cursor]); + return preview ? [preview] : []; + }, + commit: (input) => { + const points = input.points(); + if (points.length < 3) return; + const mask = hatchEntity([...points, points[0].clone()], { + style: 'solid', + color: canvasBackgroundColor(), + }); + mask.lineColor = canvasBackgroundColor(); + // 가리개는 뒤 객체를 덮어야 하므로 가장 나중에 그려지도록 목록 끝에 넣는다 + addEntities([mask], true); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/draw/divide-tools.ts b/B07_DesignDetail/openwebcad/src/tools/draw/divide-tools.ts new file mode 100644 index 00000000..8199865f --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/draw/divide-tools.ts @@ -0,0 +1,41 @@ +/** 등분(DIVIDE)·길이분할(MEASURE) — 객체를 자르지 않고 점만 놓는다 */ +import { toast } from 'react-toastify'; +import { dividePoints, measurePoints, sampleEntityPoints } from '../../helpers/geometry/sample-entity'; +import { addEntities } from '../../state'; +import { Tool } from '../../tools'; +import { pointEntity } from '../factories/entity-factory'; +import { createSequenceTool } from '../factories/sequence-tool'; + +export const divideToolStateMachine = createSequenceTool({ + tool: Tool.DIVIDE, + helpers: false, + steps: [ + { kind: 'entity', instructions: '등분할 객체를 선택하십시오.' }, + { kind: 'number', instructions: '세그먼트 수를 입력하십시오 <4>.', defaultValue: 4 }, + ], + commit: (input) => { + const points = dividePoints(sampleEntityPoints(input.entity(0)), input.number(1)); + if (!points.length) { + toast.warn('등분할 수 없는 객체입니다.'); + return; + } + addEntities(points.map(pointEntity), true); + }, +}); + +export const measureLengthToolStateMachine = createSequenceTool({ + tool: Tool.MEASURE_LENGTH, + helpers: false, + steps: [ + { kind: 'entity', instructions: '길이로 분할할 객체를 선택하십시오.' }, + { kind: 'number', instructions: '세그먼트 길이를 입력하십시오 <10>.', defaultValue: 10 }, + ], + commit: (input) => { + const points = measurePoints(sampleEntityPoints(input.entity(0)), input.number(1)); + if (!points.length) { + toast.warn('지정한 길이로 나눌 수 없습니다.'); + return; + } + addEntities(points.map(pointEntity), true); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/draw/fill-tools.ts b/B07_DesignDetail/openwebcad/src/tools/draw/fill-tools.ts new file mode 100644 index 00000000..c0de937a --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/draw/fill-tools.ts @@ -0,0 +1,113 @@ +/** 해치·그라데이션·경계·영역 — 선택한 객체가 이루는 닫힌 경계를 채우거나 뽑는다 */ +import type { Point } from '@flatten-js/core'; +import { toast } from 'react-toastify'; +import { + autoHatchSpacing, + getHatchAngle, + getHatchSpacing, + getHatchStyle, +} from '../../commands/draw-settings'; +import { entitiesToLoop } from '../../helpers/geometry/entity-loop'; +import { addEntities, getActiveLineColor, setSelectedEntityIds } from '../../state'; +import { Tool } from '../../tools'; +import { hatchEntity, polyLineEntity } from '../factories/entity-factory'; +import { createSequenceTool, type SequenceInput } from '../factories/sequence-tool'; + +/** 선택 객체에서 닫힌 경계를 얻는다. 못 얻으면 안내 후 빈 배열 */ +function loopFromSelection(input: SequenceInput): Point[] { + const loop = entitiesToLoop(input.entities(0)); + if (loop.length < 3) { + toast.warn('닫힌 경계를 만들 객체를 선택하십시오.'); + return []; + } + return loop; +} + +function boundingSize(points: Point[]): { width: number; height: number } { + const xs = points.map((point) => point.x); + const ys = points.map((point) => point.y); + return { width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) }; +} + +export const hatchToolStateMachine = createSequenceTool({ + tool: Tool.HATCH, + helpers: false, + steps: [ + { kind: 'selection', instructions: '해치를 채울 경계 객체를 선택한 뒤 ENTER를 누르십시오.' }, + ], + commit: (input) => { + const loop = loopFromSelection(input); + if (!loop.length) return; + const { width, height } = boundingSize(loop); + addEntities( + [ + hatchEntity(loop, { + style: getHatchStyle(), + color: getActiveLineColor(), + spacing: getHatchSpacing() ?? autoHatchSpacing(width, height), + angle: getHatchAngle(), + }), + ], + true + ); + setSelectedEntityIds([]); + }, +}); + +export const gradientToolStateMachine = createSequenceTool({ + tool: Tool.GRADIENT, + helpers: false, + steps: [ + { kind: 'selection', instructions: '그라데이션을 넣을 경계 객체를 선택한 뒤 ENTER.' }, + ], + commit: (input) => { + const loop = loopFromSelection(input); + if (!loop.length) return; + addEntities( + [ + hatchEntity(loop, { + style: 'gradient', + color: getActiveLineColor(), + color2: '#000000', + }), + ], + true + ); + setSelectedEntityIds([]); + }, +}); + +export const boundaryToolStateMachine = createSequenceTool({ + tool: Tool.BOUNDARY, + helpers: false, + steps: [ + { kind: 'selection', instructions: '경계를 뽑을 객체를 선택한 뒤 ENTER를 누르십시오.' }, + ], + commit: (input) => { + const loop = loopFromSelection(input); + if (!loop.length) return; + const boundary = polyLineEntity(loop); + if (boundary) { + addEntities([boundary], true); + toast.success('닫힌 폴리선 경계를 만들었습니다.'); + } + setSelectedEntityIds([]); + }, +}); + +export const regionToolStateMachine = createSequenceTool({ + tool: Tool.REGION, + helpers: false, + steps: [{ kind: 'selection', instructions: '영역으로 만들 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + const loop = loopFromSelection(input); + if (!loop.length) return; + const region = polyLineEntity(loop); + if (region) { + addEntities([region], true); + // 2D 웹 CAD에는 별도 영역 객체가 없다 — 닫힌 폴리선이 같은 역할을 한다 + toast.info('영역은 닫힌 폴리선으로 작성했습니다.'); + } + setSelectedEntityIds([]); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/factories/entity-factory.ts b/B07_DesignDetail/openwebcad/src/tools/factories/entity-factory.ts new file mode 100644 index 00000000..98a352d4 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/factories/entity-factory.ts @@ -0,0 +1,64 @@ +/** 현재 도면층·선 특성을 입혀 엔티티를 만드는 공통 생성기. */ +import type { Point } from '@flatten-js/core'; +import { ArcEntity } from '../../entities/ArcEntity'; +import type { Entity } from '../../entities/Entity'; +import { HatchEntity, type HatchOptions } from '../../entities/HatchEntity'; +import { LineEntity } from '../../entities/LineEntity'; +import { PointEntity } from '../../entities/PointEntity'; +import { PolyLineEntity } from '../../entities/PolyLineEntity'; +import { TextEntity, type TextOptions } from '../../entities/TextEntity'; +import type { ArcDefinition } from '../../helpers/geometry/shape-points'; +import { + getActiveLayerId, + getActiveLineColor, + getActiveLineDash, + getActiveLineWidth, + getActiveTextStyle, +} from '../../state'; + +/** 현재 리본에서 고른 색·굵기·선종류를 엔티티에 입힌다 */ +export function styled(entity: T): T { + entity.lineColor = getActiveLineColor(); + entity.lineWidth = getActiveLineWidth(); + entity.lineDash = getActiveLineDash(); + return entity; +} + +export const lineEntity = (start: Point, end: Point): LineEntity => + styled(new LineEntity(getActiveLayerId(), start, end)); + +export const pointEntity = (point: Point): PointEntity => + styled(new PointEntity(getActiveLayerId(), point)); + +export const arcEntity = (arc: ArcDefinition): ArcEntity => + styled( + new ArcEntity( + getActiveLayerId(), + arc.center, + arc.radius, + arc.startAngle, + arc.endAngle, + arc.counterClockwise + ) + ); + +/** 점렬을 하나의 폴리선 객체로 만든다 (2점 미만이면 null) */ +export function polyLineEntity(points: Point[]): PolyLineEntity | null { + if (points.length < 2) return null; + const segments: Entity[] = []; + for (let index = 1; index < points.length; index++) { + segments.push(lineEntity(points[index - 1], points[index])); + } + return styled(new PolyLineEntity(getActiveLayerId(), segments)); +} + +export const hatchEntity = (points: Point[], options?: Partial): HatchEntity => + styled(new HatchEntity(getActiveLayerId(), points, options)); + +export const textEntity = (label: string, basePoint: Point, options?: Partial) => + styled( + new TextEntity(getActiveLayerId(), label, basePoint, { + ...getActiveTextStyle(), + ...options, + }) + ); diff --git a/B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool.ts b/B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool.ts new file mode 100644 index 00000000..b7d02b93 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool.ts @@ -0,0 +1,349 @@ +/** + * 시퀀스 도구 공장 — AutoCAD 명령의 공통 골격(단계별 입력 → 미리보기 → 확정)을 + * 선언만으로 만든다. + * + * 명령마다 xstate 머신을 손으로 쓰면 2점 그리기에도 170줄이 든다. 여기서는 + * `steps`(점·숫자·문자·객체 선택)를 나열하고 `commit`만 채우면 같은 머신이 나온다. + */ +import type { Point } from '@flatten-js/core'; +import { Actor, assign, createMachine, sendTo } from 'xstate'; +import { HIGHLIGHT_ENTITY_DISTANCE } from '../../App.consts'; +import type { Entity } from '../../entities/Entity'; +import { findClosestEntity } from '../../helpers/find-closest-entity'; +import { getPointFromEvent } from '../../helpers/get-point-from-event'; +import { queryEntitiesNearPoint } from '../../helpers/spatial-index'; +import { + getScreenCanvasDrawController, + getSelectedEntities, + setActiveToolActor, + setAngleGuideOriginPoint, + setGhostHelperEntities, + setShouldDrawHelpers, +} from '../../state'; +import type { Tool } from '../../tools'; +import { selectToolStateMachine } from '../select-tool'; +import type { + DrawEvent, + MouseClickEvent, + NumberInputEvent, + PointInputEvent, + StateEvent, + TextInputEvent, + ToolContext, +} from '../tool.types'; + +export type SequenceValue = Point | number | string | Entity | Entity[]; + +export type SequenceStepKind = 'point' | 'number' | 'text' | 'entity' | 'selection'; + +export interface SequenceStep { + kind: SequenceStepKind; + /** 명령행·커서 옆에 뜨는 안내문 */ + instructions: string; + /** number·text 단계에서 ENTER만 눌렀을 때 채택할 값 */ + defaultValue?: number | string; +} + +/** commit·preview에 넘어가는 입력 묶음. 단계 순서 그대로 인덱스로 읽는다. */ +export interface SequenceInput { + values: SequenceValue[]; + /** 커서(미리보기) 또는 마지막 확정 점 */ + cursor: Point; + point(index: number): Point; + /** 모든 점 값 (가변 점 명령에서 사용) */ + points(): Point[]; + number(index: number): number; + text(index: number): string; + entity(index: number): Entity; + entities(index: number): Entity[]; + /** 객체 선택 단계에서 실제로 클릭한 위치 (어느 쪽을 집었는지 필요한 명령용) */ + pick(index: number): Point; +} + +export interface SequenceToolConfig { + tool: Tool; + steps: SequenceStep[]; + /** 마지막 점 단계를 ENTER 전까지 반복해 점을 모은다 (폴리선·스플라인) */ + repeatLastStep?: boolean; + /** 커서를 따라 그려줄 임시 엔티티 */ + preview?: (input: SequenceInput) => Entity[]; + /** 확정 — 엔티티 추가·상태 변경을 직접 수행한다 */ + commit: (input: SequenceInput) => void; + /** + * 확정 뒤 같은 명령을 처음부터 다시 시작할지. + * 기본값은 "점만 찍는 그리기 명령이면 계속, 숫자·문자·객체 선택이 섞이면 종료". + * 종료하면 선택 도구로 돌아가 명령행 입력이 다음 명령으로 해석된다 (AutoCAD와 같다). + */ + restart?: boolean; + /** 확정 뒤 마지막 점을 첫 값으로 이어받아 계속 그린다 (선) */ + chainFromLastPoint?: boolean; + /** 점 입력 중 스냅·각도 가이드 사용 여부 (기본 true) */ + helpers?: boolean; +} + +export interface SequenceContext extends ToolContext { + values: SequenceValue[]; + /** values와 같은 자리에 놓이는 클릭 위치 (객체를 집은 지점) */ + picks: (Point | null)[]; +} + +const STEP_STATE = (index: number) => `STEP_${index}`; +const COMMIT_STATE = 'COMMIT'; +const INIT_STATE = 'INIT'; + +function makeInput( + values: SequenceValue[], + cursor: Point, + picks: (Point | null)[] = [] +): SequenceInput { + /** 단계 인덱스로 값을 꺼낸다. 종류가 다르면 어느 단계가 잘못됐는지 바로 알린다. */ + const expect = (index: number, kind: string, ok: (value: SequenceValue) => boolean): T => { + const value = values[index]; + if (!ok(value)) { + throw new Error(`[sequence-tool] ${index}번 단계 값이 ${kind}이(가) 아닙니다`); + } + return value as T; + }; + + return { + values, + cursor, + pick: (index: number) => (picks[index] ?? cursor) as Point, + point: (index: number) => expect(index, '점', isPointValue), + points: () => values.filter((value) => isPointValue(value)) as Point[], + number: (index: number) => expect(index, '숫자', (v) => typeof v === 'number'), + text: (index: number) => expect(index, '문자', (v) => typeof v === 'string'), + entity: (index: number) => + expect(index, '객체', (v) => !!v && typeof (v as Entity).getType === 'function'), + entities: (index: number) => expect(index, '선택 목록', Array.isArray), + }; +} + +/** flatten-js Point 판별 — instanceof는 번들 중복 시 실패할 수 있어 형태로 본다. */ +function isPointValue(value: SequenceValue): boolean { + return ( + typeof value === 'object' && + value !== null && + typeof (value as Point).x === 'number' && + typeof (value as Point).y === 'number' + ); +} + +/** 마지막으로 찍은 점 — 상대 좌표·거리 입력의 기준점이 된다. */ +function lastPoint(values: SequenceValue[]): Point | null { + for (let index = values.length - 1; index >= 0; index--) { + if (isPointValue(values[index])) return values[index] as Point; + } + return null; +} + +function cursorPoint(event: StateEvent | undefined): Point { + if (event && (event as DrawEvent).drawController) { + return (event as DrawEvent).drawController.getWorldMouseLocation(); + } + return getScreenCanvasDrawController().getWorldMouseLocation(); +} + +/** 클릭 지점에서 가장 가까운 엔티티 (선택 반경 안에 있을 때만) */ +function pickEntityAt(worldPoint: Point): Entity | null { + const scale = getScreenCanvasDrawController().getScreenScale() || 1; + const radius = HIGHLIGHT_ENTITY_DISTANCE / scale; + const candidates = queryEntitiesNearPoint(worldPoint.x, worldPoint.y, radius); + const { distance, entity } = findClosestEntity(worldPoint, candidates); + return entity && distance <= radius ? entity : null; +} + +export function createSequenceTool(config: SequenceToolConfig) { + const { steps, tool } = config; + const useHelpers = config.helpers !== false; + const lastIndex = steps.length - 1; + + const nextTarget = (index: number): string => { + if (config.repeatLastStep && index === lastIndex) return STEP_STATE(index); + return index < lastIndex ? STEP_STATE(index + 1) : COMMIT_STATE; + }; + + const resetTool = () => { + setShouldDrawHelpers(useHelpers); + setGhostHelperEntities([]); + setAngleGuideOriginPoint(null); + }; + + const drawPreview = ({ context, event }: { context: SequenceContext; event: StateEvent }) => { + if (!config.preview) return; + const ghosts = config.preview(makeInput(context.values, cursorPoint(event), context.picks)); + setGhostHelperEntities(ghosts); + }; + + const pushPoint = assign(({ context, event }: { context: SequenceContext; event: StateEvent }) => { + const point = getPointFromEvent(lastPoint(context.values), event as PointInputEvent); + setAngleGuideOriginPoint(point); + return { values: [...context.values, point], picks: [...context.picks, point] }; + }); + + const pushNumberFromEvent = assign( + ({ context, event }: { context: SequenceContext; event: StateEvent }) => { + const value = + event.type === 'NUMBER_INPUT' + ? (event as NumberInputEvent).value + : Number.parseFloat((event as TextInputEvent).value); + return { values: [...context.values, value], picks: [...context.picks, null] }; + } + ); + + const pushDefault = (index: number) => + assign(({ context }: { context: SequenceContext }) => ({ + values: [...context.values, steps[index].defaultValue as SequenceValue], + picks: [...context.picks, null], + })); + + const pushText = assign(({ context, event }: { context: SequenceContext; event: StateEvent }) => ({ + values: [...context.values, (event as TextInputEvent).value], + picks: [...context.picks, null], + })); + + const pushEntity = assign( + ({ context, event }: { context: SequenceContext; event: StateEvent }) => { + const location = (event as MouseClickEvent).worldMouseLocation; + const picked = pickEntityAt(location); + return picked + ? { values: [...context.values, picked], picks: [...context.picks, location] } + : { values: context.values, picks: context.picks }; + } + ); + + const pushSelection = assign(({ context }: { context: SequenceContext }) => ({ + values: [...context.values, getSelectedEntities()], + picks: [...context.picks, null], + })); + + // 점만 받는 그리기 명령은 계속 그리게 두고, 그 밖의 명령은 끝나면 선택 도구로 돌아간다 + const keepRunning = + config.restart ?? (steps.every((step) => step.kind === 'point') || !!config.chainFromLastPoint); + + const commit = assign(({ context }: { context: SequenceContext }) => { + const points = context.values.filter(isPointValue) as Point[]; + const cursor = points.length ? points[points.length - 1] : cursorPoint(undefined); + config.commit(makeInput(context.values, cursor, context.picks)); + setGhostHelperEntities([]); + if (!keepRunning) { + // 전이 도중에 액터를 갈아치우지 않도록 다음 틱에 도구를 바꾼다 + setTimeout(() => setActiveToolActor(new Actor(selectToolStateMachine)), 0); + } + if (config.chainFromLastPoint && points.length) { + const tail = points[points.length - 1]; + setAngleGuideOriginPoint(tail); + return { values: [tail] as SequenceValue[], picks: [tail] as (Point | null)[] }; + } + return { values: [] as SequenceValue[], picks: [] as (Point | null)[] }; + }); + + const cancel = assign(({ context }: { context: SequenceContext }) => { + setGhostHelperEntities([]); + setAngleGuideOriginPoint(null); + if (context.values.length === 0) { + // 입력이 하나도 없는 상태의 ESC → 선택 도구로 빠져나간다 (AutoCAD와 동일) + setActiveToolActor(new Actor(selectToolStateMachine)); + } + return { values: [] as SequenceValue[], picks: [] as (Point | null)[] }; + }); + + // biome-ignore lint/suspicious/noExplicitAny: xstate 상태 구성은 동적으로 만든다 + const states: Record = { + [INIT_STATE]: { + always: { actions: resetTool, target: STEP_STATE(0) }, + }, + [COMMIT_STATE]: { + always: { + actions: commit, + target: config.chainFromLastPoint ? STEP_STATE(1) : INIT_STATE, + }, + }, + }; + + steps.forEach((step, index) => { + const target = nextTarget(index); + const escTransition = { actions: cancel, target: INIT_STATE }; + // biome-ignore lint/suspicious/noExplicitAny: 이벤트 맵도 단계 종류마다 달라진다 + const on: Record = { ESC: escTransition }; + + if (step.kind === 'point') { + on.DRAW = { actions: drawPreview }; + on.MOUSE_CLICK = { actions: pushPoint, target }; + on.ABSOLUTE_POINT_INPUT = { actions: pushPoint, target }; + on.RELATIVE_POINT_INPUT = { actions: pushPoint, target }; + on.NUMBER_INPUT = { actions: pushPoint, target }; + if (config.repeatLastStep && index === lastIndex) { + on.ENTER = { target: COMMIT_STATE }; + } + } else if (step.kind === 'number') { + on.DRAW = { actions: drawPreview }; + on.NUMBER_INPUT = { actions: pushNumberFromEvent, target }; + on.TEXT_INPUT = { + guard: ({ event }: { event: StateEvent }) => + Number.isFinite(Number.parseFloat((event as TextInputEvent).value)), + actions: pushNumberFromEvent, + target, + }; + if (step.defaultValue !== undefined) { + on.ENTER = { actions: pushDefault(index), target }; + } + } else if (step.kind === 'text') { + on.TEXT_INPUT = { actions: pushText, target }; + on.NUMBER_INPUT = { actions: pushNumberFromEvent, target }; + if (step.defaultValue !== undefined) { + on.ENTER = { actions: pushDefault(index), target }; + } + } else if (step.kind === 'entity') { + on.DRAW = { actions: drawPreview }; + on.MOUSE_CLICK = { + guard: ({ event }: { event: StateEvent }) => + !!pickEntityAt((event as MouseClickEvent).worldMouseLocation), + actions: pushEntity, + target, + }; + } + + if (step.kind === 'selection') { + const actorId = `selectInside_${index}`; + states[STEP_STATE(index)] = { + meta: { instructions: step.instructions }, + invoke: { + id: actorId, + src: selectToolStateMachine, + onDone: { actions: pushSelection, target }, + }, + on: { + MOUSE_CLICK: { actions: sendTo(actorId, ({ event }) => event) }, + ENTER: { actions: sendTo(actorId, ({ event }) => event) }, + DRAW: { actions: sendTo(actorId, ({ event }) => event) }, + ESC: escTransition, + }, + }; + return; + } + + states[STEP_STATE(index)] = { + meta: { instructions: step.instructions }, + entry: () => { + // 객체 선택 단계가 아니면 스냅·각도 가이드를 켠다 + setShouldDrawHelpers(useHelpers && step.kind === 'point'); + }, + on, + }; + }); + + return createMachine( + { + types: {} as { context: SequenceContext; events: StateEvent }, + context: { values: [] as SequenceValue[], picks: [] as (Point | null)[], type: tool }, + initial: INIT_STATE, + states, + }, + { + // 하위에서 돌리는 선택 도구의 액션 구현을 함께 넘긴다 + // biome-ignore lint/suspicious/noExplicitAny: 자식 머신의 컨텍스트 타입이 달라 그대로 넘긴다 + actions: { ...(selectToolStateMachine.implementations.actions as any) }, + } + ); +} diff --git a/B07_DesignDetail/openwebcad/src/tools/measurement-tool.ts b/B07_DesignDetail/openwebcad/src/tools/measurement-tool.ts deleted file mode 100644 index 4a7fe04c..00000000 --- a/B07_DesignDetail/openwebcad/src/tools/measurement-tool.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { type Point, Vector } from '@flatten-js/core'; -import { MeasurementEntity } from '../entities/MeasurementEntity'; -import { - addEntities, - getActiveLayerId, - getActiveLineColor, - getActiveLineDash, - getActiveLineWidth, - setAngleGuideOriginPoint, - setGhostHelperEntities, - setSelectedEntityIds, - setShouldDrawHelpers, -} from '../state'; -import { Tool } from '../tools'; -import { assign, createMachine } from 'xstate'; -import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types'; -import { MEASUREMENT_DEFAULT_OFFSET, TO_RADIANS } from '../App.consts'; -import { getPointFromEvent } from '../helpers/get-point-from-event.ts'; -import { isPointEqual } from '../helpers/is-point-equal.ts'; - -export interface MeasurementContext extends ToolContext { - startPoint: Point | null; - endPoint: Point | null; -} - -export enum MeasurementState { - INIT = 'INIT', - WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT', - WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT', - WAITING_FOR_OFFSET = 'WAITING_FOR_OFFSET', -} - -export enum MeasurementAction { - INIT_MEASUREMENT_TOOL = 'INIT_MEASUREMENT_TOOL', - RECORD_START_POINT = 'RECORD_START_POINT', - RECORD_END_POINT = 'RECORD_END_POINT', - DRAW_TEMP_MEASUREMENT = 'DRAW_TEMP_MEASUREMENT', - DRAW_FINAL_MEASUREMENT = 'DRAW_FINAL_MEASUREMENT', -} - -export const measurementToolStateMachine = createMachine( - { - types: {} as { - context: MeasurementContext; - events: StateEvent; - }, - context: { - startPoint: null, - endPoint: null, - type: Tool.MEASUREMENT, - }, - initial: MeasurementState.INIT, - states: { - [MeasurementState.INIT]: { - description: 'Initializing the line tool', - always: { - actions: MeasurementAction.INIT_MEASUREMENT_TOOL, - target: MeasurementState.WAITING_FOR_START_POINT, - }, - }, - [MeasurementState.WAITING_FOR_START_POINT]: { - description: 'Select the start point of the measurement', - meta: { - instructions: 'Select the start point of the measurement', - }, - on: { - MOUSE_CLICK: { - actions: MeasurementAction.RECORD_START_POINT, - target: MeasurementState.WAITING_FOR_END_POINT, - }, - ABSOLUTE_POINT_INPUT: { - actions: MeasurementAction.RECORD_START_POINT, - target: MeasurementState.WAITING_FOR_END_POINT, - }, - }, - }, - [MeasurementState.WAITING_FOR_END_POINT]: { - description: 'Select the end point of the measurement', - meta: { - instructions: 'Select the end point of the measurement', - }, - on: { - DRAW: { - actions: MeasurementAction.DRAW_TEMP_MEASUREMENT, - }, - MOUSE_CLICK: { - actions: MeasurementAction.RECORD_END_POINT, - target: MeasurementState.WAITING_FOR_OFFSET, - }, - NUMBER_INPUT: { - actions: MeasurementAction.RECORD_END_POINT, - target: MeasurementState.WAITING_FOR_OFFSET, - }, - ABSOLUTE_POINT_INPUT: { - actions: MeasurementAction.RECORD_END_POINT, - target: MeasurementState.WAITING_FOR_OFFSET, - }, - RELATIVE_POINT_INPUT: { - actions: MeasurementAction.RECORD_END_POINT, - target: MeasurementState.WAITING_FOR_OFFSET, - }, - ESC: { - target: MeasurementState.INIT, - }, - }, - }, - [MeasurementState.WAITING_FOR_OFFSET]: { - description: 'Select the offset to display the measurement at', - meta: { - instructions: 'Select the offset to display the measurement at', - }, - on: { - DRAW: { - actions: MeasurementAction.DRAW_TEMP_MEASUREMENT, - }, - MOUSE_CLICK: { - actions: MeasurementAction.DRAW_FINAL_MEASUREMENT, - target: MeasurementState.INIT, - }, - NUMBER_INPUT: { - actions: MeasurementAction.DRAW_FINAL_MEASUREMENT, - target: MeasurementState.INIT, - }, - ABSOLUTE_POINT_INPUT: { - actions: MeasurementAction.DRAW_FINAL_MEASUREMENT, - target: MeasurementState.INIT, - }, - RELATIVE_POINT_INPUT: { - actions: MeasurementAction.DRAW_FINAL_MEASUREMENT, - target: MeasurementState.INIT, - }, - ESC: { - target: MeasurementState.INIT, - }, - }, - }, - }, - }, - { - actions: { - [MeasurementAction.INIT_MEASUREMENT_TOOL]: assign(() => { - setShouldDrawHelpers(true); - setSelectedEntityIds([]); - setGhostHelperEntities([]); - setAngleGuideOriginPoint(null); - return { - startPoint: null, - endPoint: null, - }; - }), - [MeasurementAction.RECORD_START_POINT]: assign(({ event }) => { - const startPoint = getPointFromEvent(null, event as PointInputEvent); - setAngleGuideOriginPoint(startPoint); - return { - startPoint, - }; - }), - [MeasurementAction.RECORD_END_POINT]: assign(({ context, event }) => { - const endPoint = getPointFromEvent(context.startPoint, event as PointInputEvent); - setAngleGuideOriginPoint(endPoint); - return { - ...context, - endPoint, - }; - }), - [MeasurementAction.DRAW_TEMP_MEASUREMENT]: ({ context, event }) => { - const startPoint = context.startPoint as Point; - - let endPoint: Point; - let offsetPoint: Point; - if (!context.endPoint) { - // User has drawn startPoint, but not yet endPoint - // Endpoint should be the mouse location and offset should be MEASUREMENT_DEFAULT_OFFSET to either direction - endPoint = (event as DrawEvent).drawController.getWorldMouseLocation(); - - if (isPointEqual(startPoint, endPoint)) { - return; // Cannot draw temp measurement when start and endpoint are equal - } - - const normalVector = new Vector(startPoint, endPoint) - .rotate(-90 * TO_RADIANS) - .normalize(); - // Pixel constant → world units so the default offset is zoom-independent - const worldFactor = (event as DrawEvent).drawController.getScreenScale() || 1; - offsetPoint = startPoint - .clone() - .translate(normalVector.multiply(MEASUREMENT_DEFAULT_OFFSET / worldFactor)); - } else { - // User has already selected a startPoint and endPoint - // The offsetPoint should be set to the mouse location - endPoint = context.endPoint as Point; - offsetPoint = (event as DrawEvent).drawController.getWorldMouseLocation(); - } - - const activeMeasurement = new MeasurementEntity( - getActiveLayerId(), - context.startPoint as Point, - endPoint, - offsetPoint - ); - activeMeasurement.lineColor = getActiveLineColor(); - activeMeasurement.lineWidth = getActiveLineWidth(); - activeMeasurement.lineDash = getActiveLineDash(); - setGhostHelperEntities([activeMeasurement]); - }, - [MeasurementAction.DRAW_FINAL_MEASUREMENT]: ({ context, event }) => { - const offsetPoint = getPointFromEvent(context.endPoint, event as PointInputEvent); - const activeMeasurement = new MeasurementEntity( - getActiveLayerId(), - context.startPoint as Point, - context.endPoint as Point, - offsetPoint - ); - activeMeasurement.lineColor = getActiveLineColor(); - activeMeasurement.lineWidth = getActiveLineWidth(); - activeMeasurement.lineDash = getActiveLineDash(); - addEntities([activeMeasurement], true); - }, - }, - } -); diff --git a/B07_DesignDetail/openwebcad/src/tools/modify/corner-tools.ts b/B07_DesignDetail/openwebcad/src/tools/modify/corner-tools.ts new file mode 100644 index 00000000..892de32a --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/modify/corner-tools.ts @@ -0,0 +1,113 @@ +/** 모깎기·모따기·곡선 혼합·끊기 (조사표 2절) */ +import { toast } from 'react-toastify'; +import type { Entity } from '../../entities/Entity'; +import { addEntities, deleteEntities } from '../../state'; +import { Tool } from '../../tools'; +import { createSequenceTool } from '../factories/sequence-tool'; +import { blendEntities, breakEntity, chamferLines, filletLines } from './corner.helpers'; +import type { CornerResult } from './corner.helpers'; + +function applyCorner(first: Entity, second: Entity, result: CornerResult | null): void { + if (!result) { + toast.warn('두 직선을 선택해야 합니다. 두 선이 평행하면 처리할 수 없습니다.'); + return; + } + deleteEntities([first, second], false); + addEntities([...result.trimmed, ...(result.corner ? [result.corner] : [])], true); +} + +export const filletToolStateMachine = createSequenceTool({ + tool: Tool.FILLET, + helpers: false, + steps: [ + { kind: 'number', instructions: '모깎기 반지름을 입력하십시오 <0>.', defaultValue: 0 }, + { kind: 'entity', instructions: '첫 번째 객체를 남길 쪽에서 선택하십시오.' }, + { kind: 'entity', instructions: '두 번째 객체를 남길 쪽에서 선택하십시오.' }, + ], + commit: (input) => { + const first = input.entity(1); + const second = input.entity(2); + applyCorner( + first, + second, + filletLines(first, input.pick(1), second, input.pick(2), input.number(0)) + ); + }, +}); + +export const chamferToolStateMachine = createSequenceTool({ + tool: Tool.CHAMFER, + helpers: false, + steps: [ + { kind: 'number', instructions: '첫 번째 모따기 거리를 입력하십시오 <1>.', defaultValue: 1 }, + { kind: 'number', instructions: '두 번째 모따기 거리를 입력하십시오 <1>.', defaultValue: 1 }, + { kind: 'entity', instructions: '첫 번째 객체를 남길 쪽에서 선택하십시오.' }, + { kind: 'entity', instructions: '두 번째 객체를 남길 쪽에서 선택하십시오.' }, + ], + commit: (input) => { + const first = input.entity(2); + const second = input.entity(3); + applyCorner( + first, + second, + chamferLines( + first, + input.pick(2), + second, + input.pick(3), + input.number(0), + input.number(1) + ) + ); + }, +}); + +export const blendToolStateMachine = createSequenceTool({ + tool: Tool.BLEND, + helpers: false, + steps: [ + { kind: 'entity', instructions: '혼합할 첫 번째 곡선을 선택하십시오.' }, + { kind: 'entity', instructions: '혼합할 두 번째 곡선을 선택하십시오.' }, + ], + commit: (input) => { + const blend = blendEntities(input.entity(0), input.entity(1)); + if (!blend) { + toast.warn('두 객체를 이을 수 없습니다.'); + return; + } + addEntities([blend], true); + }, +}); + +export const breakToolStateMachine = createSequenceTool({ + tool: Tool.BREAK, + steps: [ + { kind: 'entity', instructions: '끊을 객체를 선택하십시오.' }, + { kind: 'point', instructions: '첫 번째 끊기 점을 지정하십시오.' }, + { kind: 'point', instructions: '두 번째 끊기 점을 지정하십시오.' }, + ], + commit: (input) => { + const entity = input.entity(0); + const pieces = breakEntity(entity, input.point(1), input.point(2)); + deleteEntities([entity], false); + addEntities(pieces, true); + }, +}); + +export const breakAtPointToolStateMachine = createSequenceTool({ + tool: Tool.BREAK_AT_POINT, + steps: [ + { kind: 'entity', instructions: '나눌 객체를 선택하십시오.' }, + { kind: 'point', instructions: '나눌 점을 지정하십시오.' }, + ], + commit: (input) => { + const entity = input.entity(0); + const pieces = breakEntity(entity, input.point(1)); + if (pieces.length < 2) { + toast.warn('이 위치에서는 나눌 수 없습니다.'); + return; + } + deleteEntities([entity], false); + addEntities(pieces, true); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/modify/corner.helpers.ts b/B07_DesignDetail/openwebcad/src/tools/modify/corner.helpers.ts new file mode 100644 index 00000000..4aec33db --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/modify/corner.helpers.ts @@ -0,0 +1,268 @@ +/** 모깎기·모따기·곡선 혼합·끊기의 기하 계산과 객체 생성 */ +import { Point, Segment } from '@flatten-js/core'; +import { ArcEntity } from '../../entities/ArcEntity'; +import type { Entity } from '../../entities/Entity'; +import { LineEntity } from '../../entities/LineEntity'; +import { PolyLineEntity } from '../../entities/PolyLineEntity'; +import { + pointAtDistance, + polylineLength, + sampleEntityPoints, +} from '../../helpers/geometry/sample-entity'; +import { intersectLines, splinePoints } from '../../helpers/geometry/shape-points'; +import { copyStyle } from './modify.helpers'; + +interface LinePick { + entity: Entity; + segment: Segment; + pick: Point; +} + +const unit = (from: Point, to: Point): { x: number; y: number } => { + const dx = to.x - from.x; + const dy = to.y - from.y; + const length = Math.hypot(dx, dy) || 1; + return { x: dx / length, y: dy / length }; +}; + +const makeLine = (source: Entity, start: Point, end: Point): LineEntity => + copyStyle(source, new LineEntity(source.layerId, start, end)); + +/** 선을 집은 쪽에서 살아남는 끝점 (교차점 반대편 끝) */ +function keepEndpoint(segment: Segment, corner: Point, pick: Point): Point { + const towardPick = unit(corner, pick); + const towardStart = unit(corner, segment.start); + const startAligned = towardPick.x * towardStart.x + towardPick.y * towardStart.y; + return startAligned > 0 ? segment.start : segment.end; +} + +function asLinePick(entity: Entity, pick: Point): LinePick | null { + const shape = entity.getShape(); + return shape instanceof Segment ? { entity, segment: shape, pick } : null; +} + +export interface CornerResult { + /** 잘려서 새로 만들어진 두 선 */ + trimmed: Entity[]; + /** 모서리에 새로 놓이는 객체 (호 또는 선) */ + corner: Entity | null; +} + +/** FILLET — 두 선을 반지름 radius의 호로 잇는다 */ +export function filletLines( + first: Entity, + firstPick: Point, + second: Entity, + secondPick: Point, + radius: number +): CornerResult | null { + const a = asLinePick(first, firstPick); + const b = asLinePick(second, secondPick); + if (!a || !b) return null; + + const corner = intersectLines(a.segment.start, a.segment.end, b.segment.start, b.segment.end); + if (!corner) return null; + + const keepA = keepEndpoint(a.segment, corner, a.pick); + const keepB = keepEndpoint(b.segment, corner, b.pick); + const ua = unit(corner, keepA); + const ub = unit(corner, keepB); + + const angle = Math.acos(Math.min(1, Math.max(-1, ua.x * ub.x + ua.y * ub.y))); + if (!Number.isFinite(angle) || angle < 1e-6 || Math.abs(angle - Math.PI) < 1e-6) return null; + + if (radius <= 0) { + // 반지름 0 = 두 선을 모서리에서 딱 맞춘다 + return { + trimmed: [makeLine(a.entity, keepA, corner), makeLine(b.entity, keepB, corner)], + corner: null, + }; + } + + const tangentDistance = radius / Math.tan(angle / 2); + const tangentA = new Point( + corner.x + ua.x * tangentDistance, + corner.y + ua.y * tangentDistance + ); + const tangentB = new Point( + corner.x + ub.x * tangentDistance, + corner.y + ub.y * tangentDistance + ); + + const bisector = unit(new Point(0, 0), new Point(ua.x + ub.x, ua.y + ub.y)); + const centerDistance = radius / Math.sin(angle / 2); + const center = new Point( + corner.x + bisector.x * centerDistance, + corner.y + bisector.y * centerDistance + ); + + const startAngle = Math.atan2(tangentA.y - center.y, tangentA.x - center.x); + const endAngle = Math.atan2(tangentB.y - center.y, tangentB.x - center.x); + const cross = + (tangentA.x - center.x) * (tangentB.y - center.y) - + (tangentA.y - center.y) * (tangentB.x - center.x); + + const arc = copyStyle( + a.entity, + new ArcEntity(a.entity.layerId, center, radius, startAngle, endAngle, cross > 0) + ); + + return { + trimmed: [makeLine(a.entity, keepA, tangentA), makeLine(b.entity, keepB, tangentB)], + corner: arc, + }; +} + +/** CHAMFER — 두 선을 직선 모따기로 잇는다 */ +export function chamferLines( + first: Entity, + firstPick: Point, + second: Entity, + secondPick: Point, + firstDistance: number, + secondDistance: number +): CornerResult | null { + const a = asLinePick(first, firstPick); + const b = asLinePick(second, secondPick); + if (!a || !b) return null; + + const corner = intersectLines(a.segment.start, a.segment.end, b.segment.start, b.segment.end); + if (!corner) return null; + + const keepA = keepEndpoint(a.segment, corner, a.pick); + const keepB = keepEndpoint(b.segment, corner, b.pick); + const ua = unit(corner, keepA); + const ub = unit(corner, keepB); + + const cutA = new Point(corner.x + ua.x * firstDistance, corner.y + ua.y * firstDistance); + const cutB = new Point(corner.x + ub.x * secondDistance, corner.y + ub.y * secondDistance); + + return { + trimmed: [makeLine(a.entity, keepA, cutA), makeLine(b.entity, keepB, cutB)], + corner: makeLine(a.entity, cutA, cutB), + }; +} + +/** BLEND — 두 객체의 가까운 끝점을 부드러운 곡선으로 잇는다 */ +export function blendEntities(first: Entity, second: Entity): Entity | null { + const firstPoints = sampleEntityPoints(first); + const secondPoints = sampleEntityPoints(second); + if (firstPoints.length < 2 || secondPoints.length < 2) return null; + + // 서로 가장 가까운 끝점 쌍을 고른다 + const candidates: [Point, Point, Point, Point][] = [ + [firstPoints[1], firstPoints[0], secondPoints[0], secondPoints[1]], + [ + firstPoints[1], + firstPoints[0], + secondPoints[secondPoints.length - 1], + secondPoints[secondPoints.length - 2], + ], + [ + firstPoints[firstPoints.length - 2], + firstPoints[firstPoints.length - 1], + secondPoints[0], + secondPoints[1], + ], + [ + firstPoints[firstPoints.length - 2], + firstPoints[firstPoints.length - 1], + secondPoints[secondPoints.length - 1], + secondPoints[secondPoints.length - 2], + ], + ]; + const best = candidates.reduce((chosen, candidate) => + candidate[1].distanceTo(candidate[2])[0] < chosen[1].distanceTo(chosen[2])[0] + ? candidate + : chosen + ); + + const curve = splinePoints([best[0], best[1], best[2], best[3]], 16); + const segments: Entity[] = []; + for (let index = 1; index < curve.length; index++) { + segments.push(makeLine(first, curve[index - 1], curve[index])); + } + return copyStyle(first, new PolyLineEntity(first.layerId, segments)); +} + +/** + * BREAK — 두 점 사이를 지운다. 한 점만 주면 그 자리에서 둘로 나눈다. + * 선·호는 원래 형상을 유지하고, 그 밖의 객체는 점렬로 나눈다. + */ +export function breakEntity(entity: Entity, first: Point, second?: Point): Entity[] { + const cutPoints = second ? [first, second] : [first]; + + const cuttable = entity as Entity & { cutAtPoints?: (points: Point[]) => Entity[] }; + if (typeof cuttable.cutAtPoints === 'function') { + const pieces = cuttable.cutAtPoints(cutPoints).map((piece) => copyStyle(entity, piece)); + if (!second) return pieces; + const middle = new Point((first.x + second.x) / 2, (first.y + second.y) / 2); + return pieces.filter((piece) => !piece.containsPointOnShape(middle)); + } + + const points = sampleEntityPoints(entity); + if (points.length < 2) return [entity]; + const total = polylineLength(points); + const firstDistance = distanceAlong(points, first); + const secondDistance = second === undefined ? firstDistance : distanceAlong(points, second); + const [from, to] = [firstDistance, secondDistance].sort((a, b) => a - b); + + const head = pointsUpTo(points, from); + const tail = pointsFrom(points, to, total); + const pieces: Entity[] = []; + for (const piece of [head, tail]) { + if (piece.length >= 2) { + const segments: Entity[] = []; + for (let index = 1; index < piece.length; index++) { + segments.push(makeLine(entity, piece[index - 1], piece[index])); + } + pieces.push(copyStyle(entity, new PolyLineEntity(entity.layerId, segments))); + } + } + return pieces; +} + +/** 점렬 시작점에서 target까지의 진행 거리 (가장 가까운 위치 기준) */ +function distanceAlong(points: Point[], target: Point): number { + let bestDistance = 0; + let bestGap = Number.POSITIVE_INFINITY; + let travelled = 0; + for (let index = 1; index < points.length; index++) { + const segment = new Segment(points[index - 1], points[index]); + const [gap, connector] = target.distanceTo(segment); + if (gap < bestGap) { + bestGap = gap; + bestDistance = travelled + points[index - 1].distanceTo(connector.end)[0]; + } + travelled += points[index - 1].distanceTo(points[index])[0]; + } + return bestDistance; +} + +function pointsUpTo(points: Point[], distance: number): Point[] { + const result: Point[] = []; + let travelled = 0; + result.push(points[0]); + for (let index = 1; index < points.length; index++) { + const step = points[index - 1].distanceTo(points[index])[0]; + if (travelled + step >= distance) break; + travelled += step; + result.push(points[index]); + } + const cut = pointAtDistance(points, distance); + if (cut) result.push(cut); + return result; +} + +function pointsFrom(points: Point[], distance: number, total: number): Point[] { + if (distance >= total) return []; + const result: Point[] = []; + const cut = pointAtDistance(points, distance); + if (cut) result.push(cut); + let travelled = 0; + for (let index = 1; index < points.length; index++) { + travelled += points[index - 1].distanceTo(points[index])[0]; + if (travelled > distance) result.push(points[index]); + } + return result; +} diff --git a/B07_DesignDetail/openwebcad/src/tools/modify/modify.helpers.ts b/B07_DesignDetail/openwebcad/src/tools/modify/modify.helpers.ts new file mode 100644 index 00000000..d551b413 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/modify/modify.helpers.ts @@ -0,0 +1,229 @@ +/** + * 수정 명령의 객체 조작 — 간격띄우기·연장·길이조정·분해·결합·중복정리. + * 기하 계산은 helpers/geometry의 순수 함수를 쓰고, 여기서는 엔티티로 바꾼다. + */ +import { Circle, Point, Segment } from '@flatten-js/core'; +import { ArcEntity } from '../../entities/ArcEntity'; +import { CircleEntity } from '../../entities/CircleEntity'; +import { type Entity, EntityName } from '../../entities/Entity'; +import type { HatchEntity } from '../../entities/HatchEntity'; +import { LineEntity } from '../../entities/LineEntity'; +import { PolyLineEntity } from '../../entities/PolyLineEntity'; +import type { RectangleEntity } from '../../entities/RectangleEntity'; +import { dedupeConsecutive, sampleEntityPoints } from '../../helpers/geometry/sample-entity'; +import { intersectLines, offsetPolylinePoints } from '../../helpers/geometry/shape-points'; +import { polygonToSegments } from '../../helpers/polygon-to-segments'; +import { getActiveLayerId } from '../../state'; + +/** 원본 객체의 표시 특성을 새 객체에 옮긴다 */ +export function copyStyle(source: Entity, target: T): T { + target.lineColor = source.lineColor; + target.lineWidth = source.lineWidth; + target.lineDash = source.lineDash; + target.layerId = source.layerId; + return target; +} + +const makeLine = (source: Entity, start: Point, end: Point): LineEntity => + copyStyle(source, new LineEntity(source.layerId || getActiveLayerId(), start, end)); + +const makePolyLine = (source: Entity, points: Point[]): PolyLineEntity | null => { + if (points.length < 2) return null; + const segments: Entity[] = []; + for (let index = 1; index < points.length; index++) { + segments.push(makeLine(source, points[index - 1], points[index])); + } + return copyStyle(source, new PolyLineEntity(source.layerId || getActiveLayerId(), segments)); +}; + +/** 점이 선의 어느 쪽에 있는지 (+1 왼쪽, -1 오른쪽) */ +function sideOfLine(start: Point, end: Point, point: Point): number { + const cross = (end.x - start.x) * (point.y - start.y) - (end.y - start.y) * (point.x - start.x); + return cross >= 0 ? 1 : -1; +} + +/** OFFSET — 객체를 distance만큼 sidePoint 쪽으로 민 새 객체 */ +export function offsetEntity(entity: Entity, distance: number, sidePoint: Point): Entity | null { + const shape = entity.getShape(); + + if (shape instanceof Segment) { + const side = sideOfLine(shape.start, shape.end, sidePoint); + const points = offsetPolylinePoints([shape.start, shape.end], distance * side); + return makeLine(entity, points[0], points[points.length - 1]); + } + + if (shape instanceof Circle) { + const outward = sidePoint.distanceTo(shape.center)[0] > Number(shape.r); + const radius = Number(shape.r) + (outward ? distance : -distance); + if (radius <= 0) return null; + return copyStyle(entity, new CircleEntity(entity.layerId, shape.center, radius)); + } + + if (entity.getType() === EntityName.Arc) { + const arcShape = entity.getShape() as unknown as { + center: Point; + r: number; + startAngle: number; + endAngle: number; + counterClockwise: boolean; + }; + const outward = sidePoint.distanceTo(arcShape.center)[0] > Number(arcShape.r); + const radius = Number(arcShape.r) + (outward ? distance : -distance); + if (radius <= 0) return null; + return copyStyle( + entity, + new ArcEntity( + entity.layerId, + arcShape.center, + radius, + arcShape.startAngle, + arcShape.endAngle, + arcShape.counterClockwise + ) + ); + } + + // 폴리선·사각형·해치 등은 점렬을 밀어서 만든다 + const points = sampleEntityPoints(entity); + if (points.length < 2) return null; + const side = sideOfLine(points[0], points[1], sidePoint); + return makePolyLine(entity, offsetPolylinePoints(points, distance * side)); +} + +/** EXTEND — 대상 선을 경계 객체와 만나는 곳까지 늘린다 */ +export function extendLineToBoundary(target: Entity, boundary: Entity): Entity | null { + const shape = target.getShape(); + if (!(shape instanceof Segment)) return null; + + const boundaryPoints = sampleEntityPoints(boundary); + if (boundaryPoints.length < 2) return null; + + let best: { point: Point; distance: number; fromStart: boolean } | null = null; + for (let index = 1; index < boundaryPoints.length; index++) { + const crossing = intersectLines( + shape.start, + shape.end, + boundaryPoints[index - 1], + boundaryPoints[index] + ); + if (!crossing) continue; + // 교점이 경계 선분 안에 있어야 한다 + if (!isBetween(crossing, boundaryPoints[index - 1], boundaryPoints[index])) continue; + + const fromEnd = shape.end.distanceTo(crossing)[0]; + const fromStart = shape.start.distanceTo(crossing)[0]; + const useStart = fromStart < fromEnd; + const distance = Math.min(fromStart, fromEnd); + if (!best || distance < best.distance) { + best = { point: crossing, distance, fromStart: useStart }; + } + } + if (!best) return null; + return best.fromStart + ? makeLine(target, best.point, shape.end) + : makeLine(target, shape.start, best.point); +} + +/** 점이 두 점 사이 선분 위(오차 허용)에 있는가 */ +export function isBetween(point: Point, start: Point, end: Point, tolerance = 1e-6): boolean { + const minX = Math.min(start.x, end.x) - tolerance; + const maxX = Math.max(start.x, end.x) + tolerance; + const minY = Math.min(start.y, end.y) - tolerance; + const maxY = Math.max(start.y, end.y) + tolerance; + return point.x >= minX && point.x <= maxX && point.y >= minY && point.y <= maxY; +} + +/** LENGTHEN — 선의 끝을 delta만큼 늘리거나(양수) 줄인다(음수) */ +export function lengthenLine(entity: Entity, delta: number, nearPoint: Point): Entity | null { + const shape = entity.getShape(); + if (!(shape instanceof Segment)) return null; + const atStart = shape.start.distanceTo(nearPoint)[0] < shape.end.distanceTo(nearPoint)[0]; + const length = shape.start.distanceTo(shape.end)[0] || 1; + const dx = (shape.end.x - shape.start.x) / length; + const dy = (shape.end.y - shape.start.y) / length; + if (atStart) { + return makeLine( + entity, + new Point(shape.start.x - dx * delta, shape.start.y - dy * delta), + shape.end + ); + } + return makeLine(entity, shape.start, new Point(shape.end.x + dx * delta, shape.end.y + dy * delta)); +} + +/** EXPLODE — 복합 객체를 구성요소로 나눈다. 나눌 게 없으면 빈 배열 */ +export function explodeEntity(entity: Entity): Entity[] { + if (entity.getType() === EntityName.PolyLine) { + return (entity as PolyLineEntity).getEntities().map((child) => copyStyle(entity, child)); + } + if (entity.getType() === EntityName.Rectangle) { + const polygon = (entity as RectangleEntity).getShape(); + if (!polygon) return []; + return polygonToSegments(polygon as never).map((segment) => + makeLine(entity, segment.start, segment.end) + ); + } + if (entity.getType() === EntityName.Hatch) { + const boundary = makePolyLine(entity, (entity as HatchEntity).getPoints()); + return boundary ? [boundary] : []; + } + return []; +} + +/** JOIN — 끝점이 맞닿는 객체들을 하나의 폴리선으로 잇는다 */ +export function joinEntities(entities: Entity[], tolerance = 1e-3): PolyLineEntity | null { + const chains = entities + .map((entity) => sampleEntityPoints(entity)) + .filter((points) => points.length >= 2); + if (chains.length < 2) return null; + + const near = (a: Point, b: Point) => + Math.abs(a.x - b.x) <= tolerance && Math.abs(a.y - b.y) <= tolerance; + + const remaining = [...chains]; + let joined = remaining.shift() as Point[]; + let progress = true; + while (remaining.length && progress) { + progress = false; + const head = joined[0]; + const tail = joined[joined.length - 1]; + for (let index = 0; index < remaining.length; index++) { + const chain = remaining[index]; + const start = chain[0]; + const end = chain[chain.length - 1]; + if (near(start, tail)) joined = [...joined, ...chain.slice(1)]; + else if (near(end, tail)) joined = [...joined, ...[...chain].reverse().slice(1)]; + else if (near(end, head)) joined = [...chain.slice(0, -1), ...joined]; + else if (near(start, head)) joined = [...[...chain].reverse().slice(0, -1), ...joined]; + else continue; + remaining.splice(index, 1); + progress = true; + break; + } + } + if (remaining.length === chains.length - 1) return null; // 하나도 못 이었다 + return makePolyLine(entities[0], dedupeConsecutive(joined)); +} + +/** REVERSE — 방향을 뒤집은 새 객체 */ +export function reverseEntity(entity: Entity): Entity | null { + const shape = entity.getShape(); + if (shape instanceof Segment) return makeLine(entity, shape.end, shape.start); + const points = sampleEntityPoints(entity); + if (points.length < 2) return null; + return makePolyLine(entity, [...points].reverse()); +} + +/** OVERKILL — 형상이 같은 객체의 중복분을 골라낸다 */ +export function findDuplicateEntities(entities: Entity[], precision = 4): Entity[] { + const seen = new Set(); + const duplicates: Entity[] = []; + for (const entity of entities) { + const signature = `${entity.getType()}|${sampleEntityPoints(entity) + .map((point) => `${point.x.toFixed(precision)},${point.y.toFixed(precision)}`) + .join(';')}`; + if (seen.has(signature)) duplicates.push(entity); + else seen.add(signature); + } + return duplicates; +} diff --git a/B07_DesignDetail/openwebcad/src/tools/modify/property-tools.ts b/B07_DesignDetail/openwebcad/src/tools/modify/property-tools.ts new file mode 100644 index 00000000..e5194d35 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/modify/property-tools.ts @@ -0,0 +1,154 @@ +/** 특성 일치·그리기 순서·ByLayer·방향 반전·중복 정리·해치 편집 (조사표 2절) */ +import { toast } from 'react-toastify'; +import type { Entity } from '../../entities/Entity'; +import { EntityName } from '../../entities/Entity'; +import type { HatchEntity, HatchStyle } from '../../entities/HatchEntity'; +import { + addEntities, + deleteEntities, + getEntities, + getLayerById, + setEntities, + setSelectedEntityIds, +} from '../../state'; +import { Tool } from '../../tools'; +import { createSequenceTool } from '../factories/sequence-tool'; +import { findDuplicateEntities, reverseEntity } from './modify.helpers'; + +export const matchPropToolStateMachine = createSequenceTool({ + tool: Tool.MATCHPROP, + helpers: false, + steps: [ + { kind: 'entity', instructions: '특성을 가져올 원본 객체를 선택하십시오.' }, + { kind: 'entity', instructions: '특성을 적용할 대상 객체를 선택하십시오.' }, + ], + commit: (input) => { + const source = input.entity(0); + const target = input.entity(1); + target.lineColor = source.lineColor; + target.lineWidth = source.lineWidth; + target.lineDash = source.lineDash; + target.layerId = source.layerId; + setEntities([...getEntities()], true); + }, +}); + +/** 선택 객체를 목록 맨 앞(뒤)으로 옮겨 그리기 순서를 바꾼다 */ +function reorder(selected: Entity[], toFront: boolean): void { + if (!selected.length) return; + const ids = new Set(selected.map((entity) => entity.id)); + const rest = getEntities().filter((entity) => !ids.has(entity.id)); + setEntities(toFront ? [...rest, ...selected] : [...selected, ...rest], true); +} + +export const drawOrderFrontToolStateMachine = createSequenceTool({ + tool: Tool.DRAWORDER_FRONT, + helpers: false, + steps: [{ kind: 'selection', instructions: '맨 앞으로 보낼 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + reorder(input.entities(0), true); + setSelectedEntityIds([]); + }, +}); + +export const drawOrderBackToolStateMachine = createSequenceTool({ + tool: Tool.DRAWORDER_BACK, + helpers: false, + steps: [{ kind: 'selection', instructions: '맨 뒤로 보낼 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + reorder(input.entities(0), false); + setSelectedEntityIds([]); + }, +}); + +export const setByLayerToolStateMachine = createSequenceTool({ + tool: Tool.SETBYLAYER, + helpers: false, + steps: [{ kind: 'selection', instructions: 'ByLayer로 되돌릴 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + let applied = 0; + for (const entity of input.entities(0)) { + const layer = getLayerById(entity.layerId); + if (!layer) continue; + if (layer.color) entity.lineColor = layer.color; + if (layer.lineWidth) entity.lineWidth = layer.lineWidth; + entity.lineDash = layer.lineDash ? [...layer.lineDash] : undefined; + applied += 1; + } + setEntities([...getEntities()], true); + setSelectedEntityIds([]); + toast.success(`${applied}개 객체를 도면층 특성으로 되돌렸습니다.`); + }, +}); + +export const reverseToolStateMachine = createSequenceTool({ + tool: Tool.REVERSE, + helpers: false, + steps: [{ kind: 'selection', instructions: '방향을 뒤집을 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + const originals = input.entities(0); + const reversed: Entity[] = []; + const consumed: Entity[] = []; + for (const entity of originals) { + const flipped = reverseEntity(entity); + if (flipped) { + reversed.push(flipped); + consumed.push(entity); + } + } + if (!consumed.length) { + toast.info('방향을 뒤집을 수 있는 객체가 없습니다.'); + return; + } + deleteEntities(consumed, false); + addEntities(reversed, true); + setSelectedEntityIds([]); + }, +}); + +export const overkillToolStateMachine = createSequenceTool({ + tool: Tool.OVERKILL, + helpers: false, + steps: [{ kind: 'selection', instructions: '중복을 정리할 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + const selected = input.entities(0); + const target = selected.length ? selected : getEntities(); + const duplicates = findDuplicateEntities(target); + if (!duplicates.length) { + toast.info('중복된 객체가 없습니다.'); + return; + } + deleteEntities(duplicates, true); + setSelectedEntityIds([]); + toast.success(`중복 객체 ${duplicates.length}개를 삭제했습니다.`); + }, +}); + +const HATCH_STYLES: HatchStyle[] = ['solid', 'pattern', 'cross', 'gradient']; + +export const hatchEditToolStateMachine = createSequenceTool({ + tool: Tool.HATCHEDIT, + helpers: false, + steps: [ + { kind: 'entity', instructions: '편집할 해치를 선택하십시오.' }, + { + kind: 'text', + instructions: '패턴을 입력하십시오 (SOLID · PATTERN · CROSS · GRADIENT) .', + defaultValue: 'PATTERN', + }, + ], + commit: (input) => { + const entity = input.entity(0); + if (entity.getType() !== EntityName.Hatch) { + toast.warn('해치 객체를 선택하십시오.'); + return; + } + const requested = input.text(1).trim().toLowerCase() as HatchStyle; + if (!HATCH_STYLES.includes(requested)) { + toast.warn('SOLID · PATTERN · CROSS · GRADIENT 중 하나를 입력하십시오.'); + return; + } + (entity as HatchEntity).options.style = requested; + setEntities([...getEntities()], true); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/modify/structure-tools.ts b/B07_DesignDetail/openwebcad/src/tools/modify/structure-tools.ts new file mode 100644 index 00000000..b49e1bd3 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/modify/structure-tools.ts @@ -0,0 +1,81 @@ +/** 연장·결합·분해·지우기 (조사표 2절) */ +import { toast } from 'react-toastify'; +import type { Entity } from '../../entities/Entity'; +import { addEntities, deleteEntities, setSelectedEntityIds } from '../../state'; +import { Tool } from '../../tools'; +import { createSequenceTool } from '../factories/sequence-tool'; +import { explodeEntity, extendLineToBoundary, joinEntities } from './modify.helpers'; + +export const extendToolStateMachine = createSequenceTool({ + tool: Tool.EXTEND, + helpers: false, + steps: [ + { kind: 'entity', instructions: '경계로 쓸 객체를 선택하십시오.' }, + { kind: 'entity', instructions: '연장할 선을 선택하십시오.' }, + ], + commit: (input) => { + const boundary = input.entity(0); + const target = input.entity(1); + const extended = extendLineToBoundary(target, boundary); + if (!extended) { + toast.warn('경계와 만나도록 연장할 수 없습니다.'); + return; + } + deleteEntities([target], false); + addEntities([extended], true); + }, +}); + +export const joinToolStateMachine = createSequenceTool({ + tool: Tool.JOIN, + helpers: false, + steps: [{ kind: 'selection', instructions: '결합할 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + const entities = input.entities(0); + const joined = joinEntities(entities); + if (!joined) { + toast.warn('끝점이 맞닿는 객체가 없어 결합하지 못했습니다.'); + return; + } + deleteEntities(entities, false); + addEntities([joined], true); + setSelectedEntityIds([]); + }, +}); + +export const explodeToolStateMachine = createSequenceTool({ + tool: Tool.EXPLODE, + helpers: false, + steps: [{ kind: 'selection', instructions: '분해할 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + const entities = input.entities(0); + const exploded: Entity[] = []; + const consumed: Entity[] = []; + for (const entity of entities) { + const parts = explodeEntity(entity); + if (parts.length) { + exploded.push(...parts); + consumed.push(entity); + } + } + if (!consumed.length) { + toast.info('분해할 수 있는 복합 객체가 없습니다.'); + return; + } + deleteEntities(consumed, false); + addEntities(exploded, true); + setSelectedEntityIds([]); + }, +}); + +export const eraseToolStateMachine = createSequenceTool({ + tool: Tool.ERASE, + helpers: false, + steps: [{ kind: 'selection', instructions: '지울 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + const entities = input.entities(0); + if (!entities.length) return; + deleteEntities(entities, true); + setSelectedEntityIds([]); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/modify/transform-tools.ts b/B07_DesignDetail/openwebcad/src/tools/modify/transform-tools.ts new file mode 100644 index 00000000..be4cc0a1 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/modify/transform-tools.ts @@ -0,0 +1,144 @@ +/** 대칭·정렬·간격띄우기·신축·길이조정 (조사표 2절) */ +import { Point } from '@flatten-js/core'; +import { toast } from 'react-toastify'; +import type { Entity } from '../../entities/Entity'; +import { LineEntity } from '../../entities/LineEntity'; +import { addEntities, deleteEntities, getActiveLayerId, setSelectedEntityIds } from '../../state'; +import { Tool } from '../../tools'; +import { createSequenceTool } from '../factories/sequence-tool'; +import { copyStyle, lengthenLine, offsetEntity } from './modify.helpers'; + +/** 원본 특성을 유지한 복사본 */ +const cloneStyled = (entity: Entity): Entity => copyStyle(entity, entity.clone()); + +export const mirrorToolStateMachine = createSequenceTool({ + tool: Tool.MIRROR, + steps: [ + { kind: 'selection', instructions: '대칭 복사할 객체를 선택한 뒤 ENTER.' }, + { kind: 'point', instructions: '대칭축의 첫 점을 지정하십시오.' }, + { kind: 'point', instructions: '대칭축의 두 번째 점을 지정하십시오.' }, + ], + preview: (input) => { + const points = input.points(); + if (points.length !== 1) return []; + return [new LineEntity(getActiveLayerId(), points[0], input.cursor)]; + }, + commit: (input) => { + const [first, second] = input.points(); + const axis = new LineEntity(getActiveLayerId(), first, second); + const mirrored = input.entities(0).map((entity) => { + const copy = cloneStyled(entity); + copy.mirror(axis); + return copy; + }); + if (mirrored.length) addEntities(mirrored, true); + setSelectedEntityIds([]); + }, +}); + +export const alignToolStateMachine = createSequenceTool({ + tool: Tool.ALIGN, + steps: [ + { kind: 'selection', instructions: '정렬할 객체를 선택한 뒤 ENTER.' }, + { kind: 'point', instructions: '첫 번째 원본점을 지정하십시오.' }, + { kind: 'point', instructions: '첫 번째 대상점을 지정하십시오.' }, + { kind: 'point', instructions: '두 번째 원본점을 지정하십시오.' }, + { kind: 'point', instructions: '두 번째 대상점을 지정하십시오.' }, + ], + commit: (input) => { + const [source1, target1, source2, target2] = input.points(); + const sourceAngle = Math.atan2(source2.y - source1.y, source2.x - source1.x); + const targetAngle = Math.atan2(target2.y - target1.y, target2.x - target1.x); + const rotation = targetAngle - sourceAngle; + + // 원본을 그대로 두고 사본을 변환해 교체한다 (실행취소가 원본을 되살릴 수 있어야 한다) + const originals = input.entities(0); + const aligned = originals.map((entity) => { + const copy = cloneStyled(entity); + copy.move(target1.x - source1.x, target1.y - source1.y); + copy.rotate(target1, rotation); + return copy; + }); + if (originals.length) { + deleteEntities(originals, false); + addEntities(aligned, true); + } + setSelectedEntityIds([]); + }, +}); + +export const offsetToolStateMachine = createSequenceTool({ + tool: Tool.OFFSET, + steps: [ + { kind: 'number', instructions: '간격띄우기 거리를 입력하십시오 <1>.', defaultValue: 1 }, + { kind: 'entity', instructions: '간격띄우기할 객체를 선택하십시오.' }, + { kind: 'point', instructions: '간격을 띄울 방향의 점을 지정하십시오.' }, + ], + commit: (input) => { + const result = offsetEntity(input.entity(1), input.number(0), input.point(2)); + if (!result) { + toast.warn('이 객체는 지정한 거리로 간격을 띄울 수 없습니다.'); + return; + } + addEntities([result], true); + }, +}); + +export const stretchToolStateMachine = createSequenceTool({ + tool: Tool.STRETCH, + steps: [ + { kind: 'selection', instructions: '신축할 객체를 선택한 뒤 ENTER.' }, + { kind: 'point', instructions: '기준점을 지정하십시오.' }, + { kind: 'point', instructions: '이동할 위치를 지정하십시오.' }, + ], + commit: (input) => { + const [base, destination] = input.points(); + const dx = destination.x - base.x; + const dy = destination.y - base.y; + const replaced: Entity[] = []; + const removed: Entity[] = []; + + for (const entity of input.entities(0)) { + const shape = entity.getShape(); + // 선은 기준점에 가까운 끝점만 끌어당긴다. 그 밖의 객체는 통째로 옮긴다. + if (shape && 'start' in shape && 'end' in shape) { + const start = (shape as { start: Point }).start; + const end = (shape as { end: Point }).end; + const moveStart = start.distanceTo(base)[0] < end.distanceTo(base)[0]; + const newLine = new LineEntity( + entity.layerId, + moveStart ? new Point(start.x + dx, start.y + dy) : start, + moveStart ? end : new Point(end.x + dx, end.y + dy) + ); + replaced.push(copyStyle(entity, newLine)); + removed.push(entity); + } else { + const copy = cloneStyled(entity); + copy.move(dx, dy); + replaced.push(copy); + removed.push(entity); + } + } + if (removed.length) deleteEntities(removed, false); + addEntities(replaced, true); + setSelectedEntityIds([]); + }, +}); + +export const lengthenToolStateMachine = createSequenceTool({ + tool: Tool.LENGTHEN, + steps: [ + { kind: 'entity', instructions: '길이를 바꿀 선을 늘릴 쪽 끝 근처에서 선택하십시오.' }, + { kind: 'number', instructions: '증분 길이를 입력하십시오 (음수는 단축) <10>.', defaultValue: 10 }, + ], + commit: (input) => { + const entity = input.entity(0); + const changed = lengthenLine(entity, input.number(1), input.pick(0)); + if (!changed) { + toast.warn('선만 길이를 조정할 수 있습니다.'); + return; + } + deleteEntities([entity], false); + addEntities([changed], true); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/select-tool.helpers.ts b/B07_DesignDetail/openwebcad/src/tools/select-tool.helpers.ts index cf076847..54b2ab9c 100644 --- a/B07_DesignDetail/openwebcad/src/tools/select-tool.helpers.ts +++ b/B07_DesignDetail/openwebcad/src/tools/select-tool.helpers.ts @@ -22,6 +22,7 @@ import { } from '../state'; import type {SelectContext} from './select-tool'; import type {MouseClickEvent} from './tool.types'; +import {expandSelectionWithGroups} from '../helpers/entity-groups'; export function handleFirstSelectionPoint( context: SelectContext, @@ -34,7 +35,8 @@ export function handleFirstSelectionPoint( // Select the entity close to the mouse const closestEntity = closestEntityInfo.entity; if (!event.holdingCtrl && !event.holdingShift) { - setSelectedEntityIds([closestEntity.id]); + // 그룹으로 묶인 객체는 하나만 집어도 함께 선택된다 (GROUP) + setSelectedEntityIds(expandSelectionWithGroups([closestEntity.id])); } else if (event.holdingCtrl) { // ctrl => toggle selection if (isEntitySelected(closestEntity)) { @@ -42,11 +44,15 @@ export function handleFirstSelectionPoint( setSelectedEntityIds(getSelectedEntityIds().filter((id) => id !== closestEntity.id)); } else { // Add the entity to the selection - setSelectedEntityIds([...getSelectedEntityIds(), closestEntity.id]); + setSelectedEntityIds( + expandSelectionWithGroups([...getSelectedEntityIds(), closestEntity.id]) + ); } } else { // shift => add to selection - setSelectedEntityIds([...getSelectedEntityIds(), closestEntity.id]); + setSelectedEntityIds( + expandSelectionWithGroups([...getSelectedEntityIds(), closestEntity.id]) + ); } return { ...context, @@ -116,7 +122,7 @@ export function selectEntitiesInsideRectangle( return null; }) ); - setSelectedEntityIds(newSelectedEntityIds); + setSelectedEntityIds(expandSelectionWithGroups(newSelectedEntityIds)); } export function drawTempSelectionRectangle(startPoint: Point, endPoint: Point) { diff --git a/B07_DesignDetail/openwebcad/src/tools/tool.consts.ts b/B07_DesignDetail/openwebcad/src/tools/tool.consts.ts deleted file mode 100644 index ed85f72a..00000000 --- a/B07_DesignDetail/openwebcad/src/tools/tool.consts.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type {StateMachine} from 'xstate'; /* eslint-disable @typescript-eslint/no-explicit-any */ -import {Tool} from '../tools'; -import {alignBottomToolStateMachine} from './align-bottom-tool.ts'; -import {alignCenterHorizontalToolStateMachine} from './align-center-horizontal-tool.ts'; -import {alignLeftToolStateMachine} from './align-left-tool.ts'; -import {alignCenterVerticalToolStateMachine} from './align-middle-vertical-tool.ts'; -import {alignRightToolStateMachine} from './align-right-tool.ts'; -import {alignTopToolStateMachine} from './align-top-tool.ts'; -import {arrayToolStateMachine} from './array-tool.ts'; -import {circleToolStateMachine} from './circle-tool'; -import {copyToolStateMachine} from './copy-tool.ts'; -import {eraserToolStateMachine} from './eraser-tool'; -import {imageImportToolStateMachine} from './image-import-tool'; -import {lineToolStateMachine} from './line-tool'; -import {measurementToolStateMachine} from './measurement-tool'; -import {moveToolStateMachine} from './move-tool'; -import {rectangleToolStateMachine} from './rectangle-tool'; -import {rotateToolStateMachine} from './rotate-tool'; -import {scaleToolStateMachine} from './scale-tool'; -import {selectToolStateMachine} from './select-tool'; -import {peditToolStateMachine} from "./pedit-tool.ts"; - -export const TOOL_STATE_MACHINES: Record< - Partial, - StateMachine< - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any, - // biome-ignore lint/suspicious/noExplicitAny: - any - > -> = { - [Tool.LINE]: lineToolStateMachine, - [Tool.RECTANGLE]: rectangleToolStateMachine, - [Tool.CIRCLE]: circleToolStateMachine, - [Tool.SELECT]: selectToolStateMachine, - [Tool.ERASER]: eraserToolStateMachine, - [Tool.MOVE]: moveToolStateMachine, - [Tool.COPY]: copyToolStateMachine, - [Tool.SCALE]: scaleToolStateMachine, - [Tool.ROTATE]: rotateToolStateMachine, - [Tool.IMAGE_IMPORT]: imageImportToolStateMachine, - [Tool.MEASUREMENT]: measurementToolStateMachine, - [Tool.ALIGN_LEFT]: alignLeftToolStateMachine, - [Tool.ALIGN_CENTER_HORIZONTAL]: alignCenterHorizontalToolStateMachine, - [Tool.ALIGN_RIGHT]: alignRightToolStateMachine, - [Tool.ALIGN_TOP]: alignTopToolStateMachine, - [Tool.ALIGN_CENTER_VERTICAL]: alignCenterVerticalToolStateMachine, - [Tool.ALIGN_BOTTOM]: alignBottomToolStateMachine, - [Tool.ARRAY]: arrayToolStateMachine, - [Tool.PEDIT]: peditToolStateMachine, -}; diff --git a/B07_DesignDetail/openwebcad/src/tools/utility/clipboard-tools.ts b/B07_DesignDetail/openwebcad/src/tools/utility/clipboard-tools.ts new file mode 100644 index 00000000..b20d8ecd --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/utility/clipboard-tools.ts @@ -0,0 +1,91 @@ +/** 클립보드 명령 — 잘라내기·복사·붙여넣기 (조사표 3절 클립보드 패널) */ +import { toast } from 'react-toastify'; +import { copyToClipboard, hasClipboardContent, pasteFromClipboard } from '../../helpers/cad-clipboard'; +import { + addEntities, + deleteEntities, + getSelectedEntities, + setSelectedEntityIds, +} from '../../state'; +import { Tool } from '../../tools'; +import { createSequenceTool } from '../factories/sequence-tool'; + +/** COPYCLIP — 선택 객체를 클립보드로 복사 */ +export function copySelectionToClipboard(): string { + const count = copyToClipboard(getSelectedEntities()); + if (!count) { + toast.info('복사할 객체를 먼저 선택하십시오.'); + return '선택 없음'; + } + toast.success(`${count}개 객체를 복사했습니다.`); + return `복사 ${count}개`; +} + +/** CUTCLIP — 선택 객체를 클립보드로 옮기고 도면에서 지운다 */ +export function cutSelectionToClipboard(): string { + const selected = getSelectedEntities(); + const count = copyToClipboard(selected); + if (!count) { + toast.info('잘라낼 객체를 먼저 선택하십시오.'); + return '선택 없음'; + } + deleteEntities(selected, true); + setSelectedEntityIds([]); + toast.success(`${count}개 객체를 잘라냈습니다.`); + return `잘라내기 ${count}개`; +} + +/** PASTEORIG — 복사한 좌표 그대로 붙여넣기 */ +export function pasteAtOriginalCoordinates(): string { + const pasted = pasteFromClipboard(); + if (!pasted.length) { + toast.info('클립보드가 비어 있습니다.'); + return '클립보드 비어 있음'; + } + addEntities(pasted, true); + toast.success(`${pasted.length}개 객체를 원래 좌표에 붙여넣었습니다.`); + return `붙여넣기 ${pasted.length}개`; +} + +/** PASTEBLOCK — 붙여넣으면서 하나의 그룹으로 묶는다 (블록 대체) */ +export function pasteAsGroup(): string { + const pasted = pasteFromClipboard(); + if (!pasted.length) { + toast.info('클립보드가 비어 있습니다.'); + return '클립보드 비어 있음'; + } + const groupId = crypto.randomUUID(); + for (const entity of pasted) { + entity.groupId = groupId; + } + addEntities(pasted, true); + toast.success(`${pasted.length}개 객체를 그룹으로 붙여넣었습니다.`); + return `그룹 붙여넣기 ${pasted.length}개`; +} + +export const copyBaseToolStateMachine = createSequenceTool({ + tool: Tool.COPYBASE, + steps: [ + { kind: 'selection', instructions: '복사할 객체를 선택한 뒤 ENTER.' }, + { kind: 'point', instructions: '기준점을 지정하십시오.' }, + ], + commit: (input) => { + const count = copyToClipboard(input.entities(0), input.point(1)); + setSelectedEntityIds([]); + toast.success(`기준점과 함께 ${count}개 객체를 복사했습니다.`); + }, +}); + +export const pasteToolStateMachine = createSequenceTool({ + tool: Tool.PASTECLIP, + steps: [{ kind: 'point', instructions: '붙여넣을 위치를 지정하십시오.' }], + preview: (input) => (hasClipboardContent() ? pasteFromClipboard(input.cursor) : []), + commit: (input) => { + const pasted = pasteFromClipboard(input.point(0)); + if (!pasted.length) { + toast.info('클립보드가 비어 있습니다.'); + return; + } + addEntities(pasted, true); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/utility/inquiry-tools.ts b/B07_DesignDetail/openwebcad/src/tools/utility/inquiry-tools.ts new file mode 100644 index 00000000..84706b43 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/utility/inquiry-tools.ts @@ -0,0 +1,161 @@ +/** 조회 명령 — 거리·반지름·각도·면적·좌표·리스트·계산기 (조사표 3절 유틸리티) */ +import { Circle, type Point } from '@flatten-js/core'; +import { toast } from 'react-toastify'; +import { EntityName } from '../../entities/Entity'; +import { entitiesToLoop } from '../../helpers/geometry/entity-loop'; +import { polylineLength, sampleEntityPoints } from '../../helpers/geometry/sample-entity'; +import { getSelectedEntities, setSelectedEntityIds } from '../../state'; +import { Tool } from '../../tools'; +import { createSequenceTool } from '../factories/sequence-tool'; + +const format = (value: number, digits = 3): string => value.toFixed(digits); +const toDegrees = (radians: number): number => (radians * 180) / Math.PI; + +/** 닫힌 점렬의 면적 (신발끈 공식) */ +export function polygonArea(points: Point[]): number { + let total = 0; + for (let index = 0; index < points.length; index++) { + const current = points[index]; + const next = points[(index + 1) % points.length]; + total += current.x * next.y - next.x * current.y; + } + return Math.abs(total) / 2; +} + +export const distanceToolStateMachine = createSequenceTool({ + tool: Tool.DIST, + steps: [ + { kind: 'point', instructions: '거리를 잴 첫 점을 지정하십시오.' }, + { kind: 'point', instructions: '두 번째 점을 지정하십시오.' }, + ], + commit: (input) => { + const [first, second] = input.points(); + const distance = first.distanceTo(second)[0]; + const angle = toDegrees(Math.atan2(second.y - first.y, second.x - first.x)); + toast.info( + `거리 ${format(distance)} · X 증분 ${format(second.x - first.x)} · Y 증분 ${format( + second.y - first.y + )} · 각도 ${format(angle, 2)}°` + ); + }, +}); + +export const radiusToolStateMachine = createSequenceTool({ + tool: Tool.RADIUS_INQUIRY, + helpers: false, + steps: [{ kind: 'entity', instructions: '반지름을 잴 원 또는 호를 선택하십시오.' }], + commit: (input) => { + const shape = input.entity(0).getShape(); + const radius = + shape instanceof Circle + ? Number(shape.r) + : Number((shape as unknown as { r?: number })?.r ?? Number.NaN); + if (!Number.isFinite(radius)) { + toast.warn('원 또는 호를 선택하십시오.'); + return; + } + toast.info(`반지름 ${format(radius)} · 지름 ${format(radius * 2)}`); + }, +}); + +export const angleToolStateMachine = createSequenceTool({ + tool: Tool.ANGLE_INQUIRY, + steps: [ + { kind: 'point', instructions: '각의 꼭짓점을 지정하십시오.' }, + { kind: 'point', instructions: '첫 번째 방향의 점을 지정하십시오.' }, + { kind: 'point', instructions: '두 번째 방향의 점을 지정하십시오.' }, + ], + commit: (input) => { + const [vertex, first, second] = input.points(); + const angle = + toDegrees(Math.atan2(second.y - vertex.y, second.x - vertex.x)) - + toDegrees(Math.atan2(first.y - vertex.y, first.x - vertex.x)); + const normalized = ((angle % 360) + 360) % 360; + toast.info(`각도 ${format(normalized, 2)}° (보각 ${format(360 - normalized, 2)}°)`); + }, +}); + +export const areaToolStateMachine = createSequenceTool({ + tool: Tool.AREA, + helpers: false, + steps: [{ kind: 'selection', instructions: '면적을 잴 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + const loop = entitiesToLoop(input.entities(0)); + if (loop.length < 3) { + toast.warn('닫힌 경계를 이루는 객체를 선택하십시오.'); + return; + } + toast.info(`면적 ${format(polygonArea(loop), 2)} · 둘레 ${format(polylineLength(loop), 2)}`); + setSelectedEntityIds([]); + }, +}); + +export const idPointToolStateMachine = createSequenceTool({ + tool: Tool.ID_POINT, + steps: [{ kind: 'point', instructions: '좌표를 확인할 점을 지정하십시오.' }], + commit: (input) => { + const point = input.point(0); + toast.info(`X = ${format(point.x)} · Y = ${format(point.y)}`); + }, +}); + +/** LIST — 선택 객체의 요약 정보 */ +export function listSelectedEntities(): string { + const selected = getSelectedEntities(); + if (!selected.length) { + toast.info('객체를 먼저 선택하십시오.'); + return '선택 없음'; + } + const lines = selected.slice(0, 20).map((entity) => { + const points = sampleEntityPoints(entity); + const box = entity.getBoundingBox(); + const size = `${format(box.xmax - box.xmin, 2)}×${format(box.ymax - box.ymin, 2)}`; + const length = entity.getType() === EntityName.Point ? '-' : format(polylineLength(points), 2); + return `${entity.getType()} · 길이 ${length} · 크기 ${size} · 색 ${entity.lineColor}`; + }); + toast.info(lines.join('\n'), { autoClose: 8000 }); + return `리스트 ${selected.length}개`; +} + +const CALCULATOR_PATTERN = /^[0-9+\-*/(). %]+$/; + +export const quickCalcToolStateMachine = createSequenceTool({ + tool: Tool.QUICKCALC, + helpers: false, + steps: [{ kind: 'text', instructions: '계산할 수식을 입력하십시오 (예: 12*3.5).' }], + commit: (input) => { + const expression = input.text(0).replace(/\s/g, ''); + if (!CALCULATOR_PATTERN.test(expression)) { + toast.warn('숫자와 + - * / ( ) 만 사용할 수 있습니다.'); + return; + } + try { + // 위 정규식으로 숫자·연산자만 남긴 문자열이라 임의 코드가 들어올 수 없다 + const result = Function(`"use strict";return (${expression})`)() as number; + toast.success(`${expression} = ${result}`); + } catch { + toast.error('수식을 계산할 수 없습니다.'); + } + }, +}); + +export const measureGeomToolStateMachine = createSequenceTool({ + tool: Tool.MEASUREGEOM, + helpers: false, + steps: [ + { + kind: 'text', + instructions: '측정 항목을 입력하십시오 (DIST · RADIUS · ANGLE · AREA) .', + defaultValue: 'DIST', + }, + ], + commit: (input) => { + const mode = input.text(0).trim().toUpperCase(); + // 각 측정은 이미 개별 명령으로 있으므로 그쪽으로 넘긴다 + const target = ['DIST', 'RADIUS', 'ANGLE', 'AREA'].includes(mode) ? mode : 'DIST'; + // 이 명령이 끝나며 선택 도구로 돌아가는 전환보다 뒤에 실행되어야 한다 + void import('../../commands/run-command').then(({ runCommandInput }) => { + setTimeout(() => runCommandInput(target), 0); + }); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/utility/layer-tools.ts b/B07_DesignDetail/openwebcad/src/tools/utility/layer-tools.ts new file mode 100644 index 00000000..2003969b --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/utility/layer-tools.ts @@ -0,0 +1,265 @@ +/** 도면층 명령 (조사표 3절 도면층 패널) */ +import { toast } from 'react-toastify'; +import type { Layer } from '../../App.types'; +import { openInspector } from '../../components/ui-state'; +import type { Entity } from '../../entities/Entity'; +import { + popLayerHistory, + pushLayerHistory, + restoreLayerState, + saveLayerState, +} from '../../helpers/layer-history'; +import { + deleteEntities, + getActiveLayerId, + getEntities, + getLayers, + setActiveLayerId, + setEntities, + setLayers, + setSelectedEntityIds, +} from '../../state'; +import { Tool } from '../../tools'; +import { createSequenceTool } from '../factories/sequence-tool'; + +/** 도면층을 바꾸기 전에 직전 상태를 기록한다 (LAYERP가 되돌린다) */ +function updateLayers(mutate: (layers: Layer[]) => Layer[]): void { + const current = getLayers(); + pushLayerHistory(current); + setLayers(mutate(current.map((layer) => ({ ...layer })))); +} + +const layerNameOf = (layerId: string): string => + getLayers().find((layer) => layer.id === layerId)?.name ?? layerId; + +/** LAYER — 도면층 특성 관리자(좌측 팔레트)를 연다 */ +export function openLayerManager(): string { + openInspector('layers'); + return '도면층 관리자'; +} + +export const layerCurrentToolStateMachine = createSequenceTool({ + tool: Tool.LAYER_CURRENT, + helpers: false, + steps: [{ kind: 'entity', instructions: '현재 도면층으로 지정할 객체를 선택하십시오.' }], + commit: (input) => { + const layerId = input.entity(0).layerId; + setActiveLayerId(layerId); + toast.success(`현재 도면층: ${layerNameOf(layerId)}`); + }, +}); + +export const layerOffToolStateMachine = createSequenceTool({ + tool: Tool.LAYOFF, + helpers: false, + steps: [{ kind: 'entity', instructions: '끌 도면층의 객체를 선택하십시오.' }], + commit: (input) => { + const layerId = input.entity(0).layerId; + updateLayers((layers) => + layers.map((layer) => (layer.id === layerId ? { ...layer, isVisible: false } : layer)) + ); + toast.info(`도면층 끄기: ${layerNameOf(layerId)}`); + }, +}); + +/** LAYON — 모든 도면층 켜기 */ +export function turnAllLayersOn(): string { + updateLayers((layers) => layers.map((layer) => ({ ...layer, isVisible: true }))); + toast.success('모든 도면층을 켰습니다.'); + return '모든 도면층 켜기'; +} + +export const layerFreezeToolStateMachine = createSequenceTool({ + tool: Tool.LAYFRZ, + helpers: false, + steps: [{ kind: 'entity', instructions: '동결할 도면층의 객체를 선택하십시오.' }], + commit: (input) => { + const layerId = input.entity(0).layerId; + if (layerId === getActiveLayerId()) { + toast.warn('현재 도면층은 동결할 수 없습니다.'); + return; + } + updateLayers((layers) => + layers.map((layer) => (layer.id === layerId ? { ...layer, isFrozen: true } : layer)) + ); + toast.info(`도면층 동결: ${layerNameOf(layerId)}`); + }, +}); + +/** LAYTHW — 모든 도면층 동결 해제 */ +export function thawAllLayers(): string { + updateLayers((layers) => layers.map((layer) => ({ ...layer, isFrozen: false }))); + toast.success('모든 도면층을 동결 해제했습니다.'); + return '동결 해제'; +} + +export const layerLockToolStateMachine = createSequenceTool({ + tool: Tool.LAYLCK, + helpers: false, + steps: [{ kind: 'entity', instructions: '잠글 도면층의 객체를 선택하십시오.' }], + commit: (input) => { + const layerId = input.entity(0).layerId; + updateLayers((layers) => + layers.map((layer) => (layer.id === layerId ? { ...layer, isLocked: true } : layer)) + ); + toast.info(`도면층 잠금: ${layerNameOf(layerId)}`); + }, +}); + +export const layerUnlockToolStateMachine = createSequenceTool({ + tool: Tool.LAYULK, + helpers: false, + steps: [{ kind: 'entity', instructions: '잠금 해제할 도면층의 객체를 선택하십시오.' }], + commit: (input) => { + const layerId = input.entity(0).layerId; + updateLayers((layers) => + layers.map((layer) => (layer.id === layerId ? { ...layer, isLocked: false } : layer)) + ); + toast.success(`도면층 잠금 해제: ${layerNameOf(layerId)}`); + }, +}); + +export const layerIsolateToolStateMachine = createSequenceTool({ + tool: Tool.LAYISO, + helpers: false, + steps: [{ kind: 'entity', instructions: '남길 도면층의 객체를 선택하십시오.' }], + commit: (input) => { + const layerId = input.entity(0).layerId; + updateLayers((layers) => + layers.map((layer) => ({ ...layer, isVisible: layer.id === layerId })) + ); + toast.info(`도면층 분리: ${layerNameOf(layerId)}`); + }, +}); + +/** LAYUNISO — 도면층 분리 해제 */ +export function unisolateLayers(): string { + updateLayers((layers) => layers.map((layer) => ({ ...layer, isVisible: true }))); + toast.success('도면층 분리를 해제했습니다.'); + return '도면층 분리 해제'; +} + +/** LAYERP — 직전 도면층 상태로 되돌리기 */ +export function restorePreviousLayers(): string { + const previous = popLayerHistory(); + if (!previous) { + toast.info('되돌릴 도면층 상태가 없습니다.'); + return '되돌릴 상태 없음'; + } + setLayers(previous); + toast.success('직전 도면층 상태로 되돌렸습니다.'); + return '직전 도면층 상태'; +} + +export const layerStateSaveToolStateMachine = createSequenceTool({ + tool: Tool.LAYERSTATE, + helpers: false, + steps: [{ kind: 'text', instructions: '저장할 도면층 상태 이름을 입력하십시오.' }], + commit: (input) => { + saveLayerState(input.text(0), getLayers()); + toast.success(`도면층 상태 저장: ${input.text(0)}`); + }, +}); + +export const layerStateRestoreToolStateMachine = createSequenceTool({ + tool: Tool.LAYERSTATE_RESTORE, + helpers: false, + steps: [{ kind: 'text', instructions: '복원할 도면층 상태 이름을 입력하십시오.' }], + commit: (input) => { + const restored = restoreLayerState(input.text(0)); + if (!restored) { + toast.warn('그 이름으로 저장한 도면층 상태가 없습니다.'); + return; + } + pushLayerHistory(getLayers()); + setLayers(restored); + toast.success(`도면층 상태 복원: ${input.text(0)}`); + }, +}); + +export const layerMatchToolStateMachine = createSequenceTool({ + tool: Tool.LAYMCH, + helpers: false, + steps: [ + { kind: 'selection', instructions: '옮길 객체를 선택한 뒤 ENTER.' }, + { kind: 'entity', instructions: '대상 도면층의 객체를 선택하십시오.' }, + ], + commit: (input) => { + const layerId = input.entity(1).layerId; + for (const entity of input.entities(0)) { + entity.layerId = layerId; + } + setEntities([...getEntities()], true); + setSelectedEntityIds([]); + toast.success(`${input.entities(0).length}개 객체를 ${layerNameOf(layerId)}(으)로 옮겼습니다.`); + }, +}); + +export const layerToCurrentToolStateMachine = createSequenceTool({ + tool: Tool.LAYCUR, + helpers: false, + steps: [{ kind: 'selection', instructions: '현재 도면층으로 옮길 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + const layerId = getActiveLayerId(); + for (const entity of input.entities(0)) { + entity.layerId = layerId; + } + setEntities([...getEntities()], true); + setSelectedEntityIds([]); + toast.success(`${input.entities(0).length}개 객체를 현재 도면층으로 옮겼습니다.`); + }, +}); + +export const layerMergeToolStateMachine = createSequenceTool({ + tool: Tool.LAYMRG, + helpers: false, + steps: [ + { kind: 'entity', instructions: '병합할(사라질) 도면층의 객체를 선택하십시오.' }, + { kind: 'entity', instructions: '대상 도면층의 객체를 선택하십시오.' }, + ], + commit: (input) => { + const sourceId = input.entity(0).layerId; + const targetId = input.entity(1).layerId; + if (sourceId === targetId) { + toast.warn('같은 도면층입니다.'); + return; + } + for (const entity of getEntities()) { + if (entity.layerId === sourceId) entity.layerId = targetId; + } + setEntities([...getEntities()], true); + updateLayers((layers) => layers.filter((layer) => layer.id !== sourceId)); + if (getActiveLayerId() === sourceId) setActiveLayerId(targetId); + toast.success(`도면층을 ${layerNameOf(targetId)}(으)로 병합했습니다.`); + }, +}); + +export const layerDeleteToolStateMachine = createSequenceTool({ + tool: Tool.LAYDEL, + helpers: false, + steps: [{ kind: 'entity', instructions: '삭제할 도면층의 객체를 선택하십시오.' }], + commit: (input) => { + const layerId = input.entity(0).layerId; + if (getLayers().length <= 1) { + toast.warn('마지막 도면층은 삭제할 수 없습니다.'); + return; + } + const doomed: Entity[] = getEntities().filter((entity) => entity.layerId === layerId); + deleteEntities(doomed, true); + updateLayers((layers) => layers.filter((layer) => layer.id !== layerId)); + if (getActiveLayerId() === layerId) setActiveLayerId(getLayers()[0].id); + toast.success(`도면층과 객체 ${doomed.length}개를 삭제했습니다.`); + }, +}); + +/** LAYWALK — 부를 때마다 다음 도면층 하나만 보여 준다 */ +let walkIndex = -1; +export function walkLayers(): string { + const layers = getLayers(); + if (!layers.length) return '도면층 없음'; + walkIndex = (walkIndex + 1) % layers.length; + const target = layers[walkIndex]; + updateLayers((all) => all.map((layer) => ({ ...layer, isVisible: layer.id === target.id }))); + toast.info(`도면층 탐색: ${target.name} (${walkIndex + 1}/${layers.length})`); + return `도면층 탐색 ${target.name}`; +} diff --git a/B07_DesignDetail/openwebcad/src/tools/utility/property-tools.ts b/B07_DesignDetail/openwebcad/src/tools/utility/property-tools.ts new file mode 100644 index 00000000..32b97f24 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/utility/property-tools.ts @@ -0,0 +1,125 @@ +/** 특성 명령 — 투명도와 특성 팔레트 열기 (조사표 3절 특성 패널) */ +import { toast } from 'react-toastify'; +import { openInspector, setQuickPropertiesVisible, isQuickPropertiesVisible } from '../../components/ui-state'; +import { getEntities, setEntities, setSelectedEntityIds } from '../../state'; +import { Tool } from '../../tools'; +import { createSequenceTool } from '../factories/sequence-tool'; + +/** PROPERTIES — 좌측 특성 팔레트를 연다 */ +export function openPropertiesPalette(): string { + openInspector('properties'); + return '특성 팔레트'; +} + +/** QUICKPROPERTIES — 선택 객체 옆의 간이 특성 상자를 켜고 끈다 */ +export function toggleQuickProperties(): string { + const next = !isQuickPropertiesVisible(); + setQuickPropertiesVisible(next); + return next ? '빠른 특성 켜기' : '빠른 특성 끄기'; +} + +/** 이름·16진수 색을 모두 받는다 (AutoCAD의 색 이름 관행) */ +const NAMED_COLORS: Record = { + RED: '#ff0000', + YELLOW: '#ffff00', + GREEN: '#00ff00', + CYAN: '#00ffff', + BLUE: '#0000ff', + MAGENTA: '#ff00ff', + WHITE: '#ffffff', + BLACK: '#000000', + GRAY: '#808080', +}; + +const LINE_DASHES: Record = { + 실선: undefined, + SOLID: undefined, + 파선: [10, 5], + DASHED: [10, 5], + '1점쇄선': [12, 4, 2, 4], + DASHDOT: [12, 4, 2, 4], + 점선: [2, 4], + DOTTED: [2, 4], +}; + +export const colorToolStateMachine = createSequenceTool({ + tool: Tool.COLOR, + helpers: false, + steps: [ + { kind: 'selection', instructions: '색을 바꿀 객체를 선택한 뒤 ENTER.' }, + { kind: 'text', instructions: '색을 입력하십시오 (#ff0000 또는 RED).' }, + ], + commit: (input) => { + const raw = input.text(1).trim(); + const color = NAMED_COLORS[raw.toUpperCase()] ?? (raw.startsWith('#') ? raw : ''); + if (!color) { + toast.warn('#rrggbb 형식이나 색 이름을 입력하십시오.'); + return; + } + for (const entity of input.entities(0)) { + entity.lineColor = color; + } + setEntities([...getEntities()], true); + setSelectedEntityIds([]); + toast.success(`색상 ${color}`); + }, +}); + +export const lineTypeToolStateMachine = createSequenceTool({ + tool: Tool.LINETYPE, + helpers: false, + steps: [ + { kind: 'selection', instructions: '선종류를 바꿀 객체를 선택한 뒤 ENTER.' }, + { kind: 'text', instructions: '선종류를 입력하십시오 (실선·파선·1점쇄선·점선).', defaultValue: '실선' }, + ], + commit: (input) => { + const key = input.text(1).trim(); + if (!(key in LINE_DASHES) && !(key.toUpperCase() in LINE_DASHES)) { + toast.warn('실선·파선·1점쇄선·점선 중에서 입력하십시오.'); + return; + } + const dash = LINE_DASHES[key] ?? LINE_DASHES[key.toUpperCase()]; + for (const entity of input.entities(0)) { + entity.lineDash = dash ? [...dash] : undefined; + } + setEntities([...getEntities()], true); + setSelectedEntityIds([]); + toast.success(`선종류 ${key}`); + }, +}); + +export const lineWeightToolStateMachine = createSequenceTool({ + tool: Tool.LWEIGHT, + helpers: false, + steps: [ + { kind: 'selection', instructions: '선가중치를 바꿀 객체를 선택한 뒤 ENTER.' }, + { kind: 'number', instructions: '선 굵기를 입력하십시오 <1>.', defaultValue: 1 }, + ], + commit: (input) => { + const width = Math.max(1, Math.round(input.number(1))); + for (const entity of input.entities(0)) { + entity.lineWidth = width; + } + setEntities([...getEntities()], true); + setSelectedEntityIds([]); + toast.success(`선가중치 ${width}px`); + }, +}); + +export const transparencyToolStateMachine = createSequenceTool({ + tool: Tool.TRANSPARENCY, + helpers: false, + steps: [ + { kind: 'selection', instructions: '투명도를 바꿀 객체를 선택한 뒤 ENTER.' }, + { kind: 'number', instructions: '투명도를 입력하십시오 (0~90) <0>.', defaultValue: 0 }, + ], + commit: (input) => { + const percent = Math.min(90, Math.max(0, input.number(1))); + for (const entity of input.entities(0)) { + entity.opacity = 1 - percent / 100; + } + setEntities([...getEntities()], true); + setSelectedEntityIds([]); + toast.success(`투명도 ${percent}%를 적용했습니다.`); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/utility/selection-tools.ts b/B07_DesignDetail/openwebcad/src/tools/utility/selection-tools.ts new file mode 100644 index 00000000..6988f0f9 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/utility/selection-tools.ts @@ -0,0 +1,122 @@ +/** 선택·표시 명령 — 빠른 선택·유사 선택·객체 분리·그룹 (조사표 3절) */ +import { toast } from 'react-toastify'; +import type { Entity } from '../../entities/Entity'; +import { hideEntities, isolateEntities, showAllEntities } from '../../helpers/visibility'; +import { getEntities, setEntities, setSelectedEntityIds } from '../../state'; +import { Tool } from '../../tools'; +import { createSequenceTool } from '../factories/sequence-tool'; + +export const qSelectToolStateMachine = createSequenceTool({ + tool: Tool.QSELECT, + helpers: false, + steps: [ + { + kind: 'text', + instructions: '선택할 객체 유형을 입력하십시오 (Line · Circle · Arc · Text · PolyLine · Hatch).', + }, + ], + commit: (input) => { + const wanted = input.text(0).trim().toLowerCase(); + const matched = getEntities().filter( + (entity) => entity.getType().toLowerCase() === wanted + ); + if (!matched.length) { + toast.info('조건에 맞는 객체가 없습니다.'); + return; + } + setSelectedEntityIds(matched.map((entity) => entity.id)); + toast.success(`${matched.length}개 객체를 선택했습니다.`); + }, +}); + +export const selectSimilarToolStateMachine = createSequenceTool({ + tool: Tool.SELECTSIMILAR, + helpers: false, + steps: [{ kind: 'entity', instructions: '기준이 될 객체를 선택하십시오.' }], + commit: (input) => { + const reference = input.entity(0); + const similar = getEntities().filter( + (entity) => + entity.getType() === reference.getType() && + entity.lineColor === reference.lineColor && + entity.layerId === reference.layerId + ); + setSelectedEntityIds(similar.map((entity) => entity.id)); + toast.success(`유사 객체 ${similar.length}개를 선택했습니다.`); + }, +}); + +export const isolateObjectsToolStateMachine = createSequenceTool({ + tool: Tool.ISOLATEOBJECTS, + helpers: false, + steps: [{ kind: 'selection', instructions: '남길 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + const selected = input.entities(0); + if (!selected.length) return; + isolateEntities(selected, getEntities()); + setSelectedEntityIds([]); + toast.info(`${selected.length}개 객체만 표시합니다.`); + }, +}); + +export const hideObjectsToolStateMachine = createSequenceTool({ + tool: Tool.HIDEOBJECTS, + helpers: false, + steps: [{ kind: 'selection', instructions: '숨길 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + const selected = input.entities(0); + if (!selected.length) return; + hideEntities(selected); + setSelectedEntityIds([]); + toast.info(`${selected.length}개 객체를 숨겼습니다.`); + }, +}); + +/** UNISOLATEOBJECTS — 숨긴 객체를 모두 되살린다 */ +export function unhideAllObjects(): string { + showAllEntities(); + toast.success('숨긴 객체를 모두 표시했습니다.'); + return '객체 분리 종료'; +} + +export const groupToolStateMachine = createSequenceTool({ + tool: Tool.GROUP, + helpers: false, + steps: [{ kind: 'selection', instructions: '그룹으로 묶을 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + const selected = input.entities(0); + if (selected.length < 2) { + toast.warn('두 개 이상의 객체를 선택하십시오.'); + return; + } + const groupId = crypto.randomUUID(); + for (const entity of selected) { + entity.groupId = groupId; + } + setEntities([...getEntities()], true); + setSelectedEntityIds([]); + toast.success(`${selected.length}개 객체를 그룹으로 묶었습니다.`); + }, +}); + +export const ungroupToolStateMachine = createSequenceTool({ + tool: Tool.UNGROUP, + helpers: false, + steps: [{ kind: 'selection', instructions: '그룹을 해제할 객체를 선택한 뒤 ENTER.' }], + commit: (input) => { + const selected: Entity[] = input.entities(0); + const groupIds = new Set(selected.map((entity) => entity.groupId).filter(Boolean)); + if (!groupIds.size) { + toast.info('그룹으로 묶인 객체가 없습니다.'); + return; + } + for (const entity of getEntities()) { + if (entity.groupId && groupIds.has(entity.groupId)) { + entity.groupId = undefined; + } + } + setEntities([...getEntities()], true); + setSelectedEntityIds([]); + toast.success('그룹을 해제했습니다.'); + }, +}); diff --git a/B07_DesignDetail/openwebcad/test/helpers/init-application.ts b/B07_DesignDetail/openwebcad/test/helpers/init-application.ts index b16704d5..3d99fee4 100644 --- a/B07_DesignDetail/openwebcad/test/helpers/init-application.ts +++ b/B07_DesignDetail/openwebcad/test/helpers/init-application.ts @@ -4,7 +4,7 @@ import type {ScreenCanvasDrawController} from '../../src/drawControllers/screenC import {InputController} from '../../src/inputController/input-controller'; import {setActiveToolActor, setEntities, setInputController, setScreenCanvasDrawController,} from '../../src/state'; import {Tool} from '../../src/tools'; -import {TOOL_STATE_MACHINES} from '../../src/tools/tool.consts'; +import {TOOL_STATE_MACHINES} from '../../src/commands/registry'; import {ScreenCanvasDrawController as ScreenCanvasDrawControllerMock} from '../mocks/drawControllers/screenCanvas.drawController'; import {CANVAS_HEIGHT, CANVAS_WIDTH} from './tests.consts'; diff --git a/B07_DesignDetail/openwebcad/test/helpers/replay-recording.ts b/B07_DesignDetail/openwebcad/test/helpers/replay-recording.ts index 4d929143..a6711309 100644 --- a/B07_DesignDetail/openwebcad/test/helpers/replay-recording.ts +++ b/B07_DesignDetail/openwebcad/test/helpers/replay-recording.ts @@ -12,7 +12,7 @@ const DATA_ID_TO_TOOL_NAME: Record = { 'move-button': Tool.MOVE, 'scale-button': Tool.SCALE, 'rotate-button': Tool.ROTATE, - 'measurement-button': Tool.MEASUREMENT, + 'measurement-button': Tool.DIMALIGNED, 'undo-button': null, 'redo-button': null, 'delete-segment-button': Tool.ERASER, diff --git a/B07_DesignDetail/openwebcad/test/helpers/set-active-tool.ts b/B07_DesignDetail/openwebcad/test/helpers/set-active-tool.ts index 38cbbfb3..83b5cb3f 100644 --- a/B07_DesignDetail/openwebcad/test/helpers/set-active-tool.ts +++ b/B07_DesignDetail/openwebcad/test/helpers/set-active-tool.ts @@ -1,7 +1,7 @@ import type { Tool } from '../../src/tools'; import { getActiveToolActor, setActiveToolActor } from '../../src/state'; import { Actor } from 'xstate'; -import { TOOL_STATE_MACHINES } from '../../src/tools/tool.consts'; +import { TOOL_STATE_MACHINES } from '../../src/commands/registry'; export function setActiveTool(toolName: Tool) { getActiveToolActor()?.stop(); diff --git a/B07_DesignDetail/openwebcad/vitest.tmp.config.ts b/B07_DesignDetail/openwebcad/vitest.tmp.config.ts new file mode 100644 index 00000000..125ea4bd --- /dev/null +++ b/B07_DesignDetail/openwebcad/vitest.tmp.config.ts @@ -0,0 +1,20 @@ +/** + * 저장소 tmp/tests에 있는 CAD 테스트 실행용 설정 (테스트 코드는 git 추적 제외). + * npx vitest run --config vitest.tmp.config.ts + * tmp에는 node_modules가 없어 라이브러리를 openwebcad 쪽으로 이어 준다. + */ +import { resolve } from 'node:path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: resolve('../..'), + resolve: { + alias: { + '@flatten-js/core': resolve('node_modules/@flatten-js/core'), + 'es-toolkit': resolve('node_modules/es-toolkit'), + }, + }, + test: { + include: ['tmp/tests/cad/**/*.test.ts'], + }, +}); diff --git a/resources/임시_AutoCAD_리본_기능_조사.md b/resources/임시_AutoCAD_리본_기능_조사.md new file mode 100644 index 00000000..c2ed8bce --- /dev/null +++ b/resources/임시_AutoCAD_리본_기능_조사.md @@ -0,0 +1,383 @@ +# AutoCAD 2D 기능·명령 조사 (임시) + +- 조사 기준: AutoCAD 2024 Windows의 기본 2D 제도 기능 +- 조사일: 2026-08-29 +- 추가검증: 2026-08-29, Autodesk 공식 명령 참조·2024 신규/변경 명령·상태막대 참조와 재대조 +- 용도: B07 상세설계 웹 CAD 기능 검토용 임시 참고자료 +- 기본 입력 기준: AutoCAD 2024의 명령 별칭(`acad.pgp`)과 키 조합·기능 키(CUIx)를 `단축키` 열에 함께 기록했다. `—`는 공식 기본 입력을 확인하지 못했거나 리본·상황별 조작만 있는 기능이다. 사용자 설정에 따라 값이 달라질 수 있다. +- 자료 성격: 아래 표는 기본 2D 제도 기능과 명령을 작업 기준으로 분류한 **기능 인벤토리**이며, 실제 리본에 표시되는 아이콘의 전수 목록이 아니다. +- 아이콘 주의: 리본은 제품·작업공간·화면 폭·CUI 사용자화에 따라 탭, 패널, 버튼, 드롭다운 및 슬라이드아웃 구성이 달라진다. 실제 아이콘 목록으로 사용하려면 AutoCAD 2024 기본 `제도 및 주석` 작업공간의 CUIx 또는 실행 화면을 기준으로 버튼 단위 검증이 추가로 필요하다. +- 반영 등급: `A` B07 웹 CAD 우선 구현 · `B` 후속·조건부(엔진·구조 확장이 선행되어야 함) · `C` 미반영(DWG·데스크톱·클라우드 종속이거나 임도 설계에 불필요) +- 반영: `☑` 반영 완료 · `◐` 부분 반영 · `☐` 미반영. `기존`은 이번 AutoCAD 대응 작업 이전부터 있던 기능이다. 구현할 때마다 이 열을 갱신한다. +- 제외 범위: 3D 모델링, Architecture 등 전문화 도구 세트, Express Tools 및 타사 애드인. 같은 기능이 여러 위치에 나타나는 경우 최초 한 번만 기재했다. + +## 1. 홈 탭 — 그리기 + +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | 반영 등급 | 반영 | +|---|---|---|---|---|---| +| 그리기 | 선 `LINE` | L | 두 점 사이에 직선 세그먼트를 작성한다. | A | ☑ 기존 | +| 그리기 | 폴리선 `PLINE` | PL | 연결된 선·호를 하나의 객체로 작성한다. | A | ☑ | +| 그리기 | 원 `CIRCLE` | C | 중심·반지름 등 여러 조건으로 원을 작성한다. | A | ☑ 기존 | +| 그리기 | 호 `ARC` | A | 3점, 중심, 시작·끝점 등의 조건으로 호를 작성한다. | A | ☑ | +| 그리기 | 직사각형 `RECTANG` | REC | 직사각형 형태의 닫힌 폴리선을 작성한다. | A | ☑ 기존 | +| 그리기 | 다각형 `POLYGON` | POL | 지정한 변 수의 정다각형을 작성한다. | A | ☑ | +| 그리기 | 타원 `ELLIPSE` | EL | 중심 또는 축 길이로 타원·타원호를 작성한다. | A | ◐ | +| 그리기 | 스플라인 `SPLINE` | SPL | 맞춤점 또는 조정 정점으로 부드러운 곡선을 작성한다. | B | ☑ | +| 그리기 | 다중선 `MLINE` | ML | 여러 평행선으로 구성된 다중선 객체를 작성한다. | B | ◐ | +| 그리기 | 다중선 스타일 `MLSTYLE` | — | 다중선의 요소 수·간격·선종류와 끝막음을 관리한다. | B | ☑ | +| 그리기 | 구성선 `XLINE` | XL | 양방향으로 무한한 기준선을 작성한다. | B | ◐ | +| 그리기 | 광선 `RAY` | — | 한 방향으로 무한한 기준선을 작성한다. | B | ◐ | +| 그리기 | 점 `POINT` | PO | 점 객체를 작성한다. | A | ☑ | +| 그리기 | 도넛 `DONUT` | DO | 채워진 원 또는 링 모양 폴리선을 작성한다. | C | ◐ | +| 그리기 | 등분 `DIVIDE` | DIV | 객체를 자르지 않고 동일 간격의 점 또는 블록을 지정 개수만큼 배치한다. | A | ☑ | +| 그리기 | 길이분할 `MEASURE` | ME | 객체를 자르지 않고 지정 거리마다 점 또는 블록을 배치한다. | A | ☑ | +| 그리기 | 해치 `HATCH` | H / BH | 닫힌 경계에 패턴·솔리드·그라데이션 채움을 작성한다. | A | ☑ | +| 그리기 | 그라데이션 `GRADIENT` | GD | 닫힌 영역에 색상 그라데이션을 적용한다. | C | ☑ | +| 그리기 | 경계 `BOUNDARY` | BO | 닫힌 영역에서 폴리선 또는 영역 객체를 만든다. | B | ☑ | +| 그리기 | 영역 `REGION` | REG | 닫힌 평면 객체를 영역 객체로 변환한다. | C | ◐ | +| 그리기 | 와이프아웃 `WIPEOUT` | — | 뒤쪽 객체를 가리는 마스크 영역을 작성한다. | B | ☑ | + +## 2. 홈 탭 — 수정 + +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | 반영 등급 | 반영 | +|---|---|---|---|---|---| +| 수정 | 이동 `MOVE` | M | 선택 객체를 기준점에서 새 위치로 이동한다. | A | ☑ 기존 | +| 수정 | 복사 `COPY` | CO | 선택 객체를 하나 이상의 위치에 복제한다. | A | ☑ 기존 | +| 수정 | 회전 `ROTATE` | RO | 기준점을 중심으로 객체를 회전한다. | A | ☑ 기존 | +| 수정 | 축척 `SCALE` | SC | 기준점과 비율 또는 참조 길이로 크기를 변경한다. | A | ☑ 기존 | +| 수정 | 정렬 `ALIGN` | AL | 원본점과 대상점 쌍으로 객체를 이동·회전하고 선택적으로 축척한다. | B | ☑ | +| 수정 | 대칭 `MIRROR` | MI | 대칭축을 기준으로 객체를 반사 복사한다. | A | ☑ | +| 수정 | 간격띄우기 `OFFSET` | O | 평행선·동심원·등거리 곡선을 작성한다. | A | ☑ | +| 수정 | 배열 `ARRAY` | AR | 직사각형·경로·원형 패턴으로 객체를 반복 배치한다. | A | ☑ 기존 | +| 수정 | 자르기 `TRIM` | TR | 경계를 기준으로 불필요한 객체 부분을 자른다. | A | ☑ | +| 수정 | 연장 `EXTEND` | EX | 객체 끝을 지정 경계까지 연장한다. | A | ◐ | +| 수정 | 신축 `STRETCH` | S | 교차 선택한 정점과 객체 일부를 늘이거나 이동한다. | B | ◐ | +| 수정 | 모깎기 `FILLET` | F | 두 객체를 지정 반지름의 호로 연결한다. | A | ◐ | +| 수정 | 모따기 `CHAMFER` | CHA | 두 객체를 직선 모따기로 연결한다. | A | ◐ | +| 수정 | 곡선 혼합 `BLEND` | — | 두 곡선을 부드러운 스플라인으로 연결한다. | C | ☑ | +| 수정 | 끊기 `BREAK` | BR | 객체의 두 점 사이를 제거하거나 한 점에서 나눈다. | A | ☑ | +| 수정 | 점에서 끊기 `BREAKATPOINT` | — | 객체를 지정점에서 두 객체로 나눈다. | A | ☑ | +| 수정 | 결합 `JOIN` | J | 끝이 맞는 선·호·폴리선 등을 하나로 결합한다. | A | ☑ | +| 수정 | 분해 `EXPLODE` | X | 블록·폴리선 등 복합 객체를 구성요소로 분해한다. | A | ☑ | +| 수정 | 지우기 `ERASE` | E | 선택 객체를 삭제한다. | A | ☑ 기존 | +| 수정 | 길이조정 `LENGTHEN` | LEN | 선과 호의 길이 또는 끼인각을 변경한다. | B | ◐ | +| 수정 | 폴리선 편집 `PEDIT` | PE | 폴리선 결합, 폭, 정점, 곡선 맞춤 등을 편집한다. | A | ☑ 기존 | +| 수정 | 해치 편집 `HATCHEDIT` | HE | 기존 해치의 패턴, 축척, 각도, 경계를 수정한다. | B | ☑ | +| 수정 | 그리기 순서 `DRAWORDER` | DR | 객체의 앞뒤 표시 순서를 변경한다. | B | ☑ | +| 수정 | 특성 일치 `MATCHPROP` | MA | 원본 객체의 표시 특성을 다른 객체에 복사한다. | A | ☑ | +| 수정 | 중복 객체 삭제 `OVERKILL` | — | 중복·겹침 형상을 제거하고 이어진 선·호를 결합한다. | B | ☑ | +| 수정 | 방향 반전 `REVERSE` | — | 선·폴리선·스플라인 등의 시작점과 끝점 방향을 뒤집는다. | C | ☑ | +| 수정 | 특성을 ByLayer로 `SETBYLAYER` | — | 선택 객체의 특성 재지정을 ByLayer 값으로 변경한다. | B | ☑ | + +## 3. 홈 탭 — 도면층·특성·그룹·유틸리티 + +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | 반영 등급 | 반영 | +|---|---|---|---|---|---| +| 도면층 | 도면층 특성 `LAYER` | LA | 도면층을 만들고 이름·색상·선종류·출력 여부를 관리한다. | A | ☑ 기존 | +| 도면층 | 현재 도면층 설정 | — | 선택한 도면층을 새 객체의 기본 도면층으로 지정한다. | A | ☑ 기존 | +| 도면층 | 켜기/끄기 | — | 도면층 객체의 표시를 켜거나 끈다. | A | ☑ 기존 | +| 도면층 | 동결/동결해제 | — | 도면층을 재생성 및 표시 대상에서 제외하거나 복원한다. | C | ☑ | +| 도면층 | 잠금/잠금해제 | — | 도면층 객체의 편집 가능 여부를 전환한다. | A | ☑ 기존 | +| 도면층 | 분리 `LAYISO` | — | 선택 객체의 도면층만 남기고 나머지를 숨기거나 잠근다. | B | ☑ | +| 도면층 | 분리 해제 `LAYUNISO` | — | 도면층 분리 이전 상태를 복원한다. | B | ☑ | +| 도면층 | 이전 상태 `LAYERP` | — | 직전 도면층 설정 변경을 복원한다. | C | ☑ | +| 도면층 | 도면층 상태 `LAYERSTATE` | LAS | 도면층 설정 조합을 이름으로 저장하고 복원한다. | B | ☑ | +| 도면층 | 도면층 일치 `LAYMCH` | — | 선택 객체를 대상 객체의 도면층으로 이동한다. | A | ☑ | +| 도면층 | 현재 도면층으로 `LAYCUR` | — | 선택 객체를 현재 도면층으로 이동한다. | A | ☑ | +| 도면층 | 도면층 병합 `LAYMRG` | — | 한 도면층의 객체를 다른 도면층으로 옮기고 원래 도면층을 제거한다. | B | ☑ | +| 도면층 | 도면층 삭제 `LAYDEL` | — | 선택한 도면층과 그 객체를 삭제한다. | B | ☑ | +| 도면층 | 도면층 탐색 `LAYWALK` | — | 도면층을 선택적으로 표시하며 포함 객체를 확인한다. | C | ☑ | +| 특성 | 색상 `COLOR` | COL | 객체 색상을 직접 또는 `ByLayer`·`ByBlock`으로 지정한다. | A | ☑ | +| 특성 | 선종류 `LINETYPE` | LT | 실선·점선·중심선 등 객체 선종류를 지정한다. | A | ☑ | +| 특성 | 선가중치 `LWEIGHT` | LW | 화면 및 출력에 사용할 선 굵기를 지정한다. | A | ☑ | +| 특성 | 투명도 | — | 객체 또는 도면층의 투명도를 지정한다. | B | ☑ | +| 특성 | 특성 팔레트 `PROPERTIES` | Ctrl+1 / CH / MO / PR | 선택 객체의 형상·표시·데이터 특성을 조회하고 수정한다. | A | ☑ | +| 특성 | 빠른 특성 `QUICKPROPERTIES` | QP | 선택 객체 주변에 주요 특성만 간단히 표시한다. | B | ☑ | +| 그룹 | 그룹 `GROUP` | G | 여러 객체를 함께 선택할 수 있는 명명 그룹으로 묶는다. | B | ☑ | +| 그룹 | 그룹 해제 `UNGROUP` | — | 객체 그룹을 해제한다. | B | ☑ | +| 유틸리티 | 거리 `DIST` | DI | 두 점 사이의 거리와 각도 차이를 측정한다. | A | ☑ | +| 유틸리티 | 반지름 | — | 원 또는 호의 반지름을 측정한다. | A | ☑ | +| 유틸리티 | 각도 | — | 선·호 또는 지정점 사이의 각도를 측정한다. | A | ☑ | +| 유틸리티 | 면적 `AREA` | AA | 객체 또는 지정 경계의 면적과 둘레를 계산한다. | A | ☑ | +| 유틸리티 | 빠른 측정 `MEASUREGEOM` | MEA | 커서 주변의 치수·거리·각도와 닫힌 영역의 면적을 동적으로 표시한다. | B | ☑ | +| 유틸리티 | 점 좌표 `ID` | ID | 지정점의 X·Y·Z 좌표를 표시한다. | A | ☑ | +| 유틸리티 | 리스트 `LIST` | LI | 선택 객체의 상세 데이터와 기하 정보를 표시한다. | B | ☑ | +| 유틸리티 | 빠른 선택 `QSELECT` | — | 객체 유형과 특성 조건으로 선택 집합을 만든다. | B | ☑ | +| 유틸리티 | 계산기 `QUICKCALC` | QC / Ctrl+8 | 수식, 단위 변환 및 도면 값을 계산한다. | C | ☑ | +| 유틸리티 | 유사 선택 `SELECTSIMILAR` | — | 선택 객체와 지정 특성이 같은 객체를 모두 선택한다. | B | ☑ | +| 유틸리티 | 객체 분리 `ISOLATEOBJECTS` | — | 선택 객체만 표시하고 나머지 객체를 숨긴다. | B | ☑ | +| 유틸리티 | 객체 숨기기 `HIDEOBJECTS` | — | 선택 객체를 일시적으로 숨긴다. | B | ☑ | +| 유틸리티 | 객체 분리 종료 `UNISOLATEOBJECTS` | UNHIDE / UNISOLATE | 숨기거나 분리한 객체를 다시 표시한다. | B | ☑ | +| 클립보드 | 잘라내기·복사·붙여넣기 | Ctrl+X / Ctrl+C / Ctrl+V | 객체를 시스템 클립보드로 이동·복제한다. | A | ☑ | +| 클립보드 | 기준점과 함께 복사 | Ctrl+Shift+C | 지정 기준점을 포함해 객체를 클립보드에 복사한다. | B | ☑ | +| 클립보드 | 원래 좌표로 붙여넣기 | — | 복사한 객체를 원본 도면 좌표에 배치한다. | B | ☑ | +| 클립보드 | 블록으로 붙여넣기 | Ctrl+Shift+V | 클립보드 객체를 새 블록으로 삽입한다. | C | ◐ | + +## 4. 삽입 탭 — 블록·참조·데이터 + +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | 반영 등급 | 반영 | +|---|---|---|---|---|---| +| 블록 | 블록 삽입 `INSERT` | I | 현재 도면·최근 항목·라이브러리의 블록을 배치한다. | B | ☐ | +| 블록 | 스마트 블록 배치 | — | 블록 팔레트로 삽입할 때 기존 동일 블록을 바탕으로 위치·회전·축척을 제안한다. | C | ☐ | +| 블록 | 블록 교체 `BREPLACE` | — | 선택 블록을 제안 목록 또는 지정한 다른 블록으로 교체한다. | C | ☐ | +| 블록 정의 | 블록 작성 `BLOCK` | B | 선택 객체와 기준점으로 블록 정의를 만든다. | B | ☐ | +| 블록 정의 | 블록 편집기 `BEDIT` | BE | 블록 정의와 동적 블록 동작을 편집한다. | C | ☐ | +| 블록 정의 | 블록 쓰기 `WBLOCK` | W | 선택 객체 또는 블록을 별도 DWG로 저장한다. | C | ☐ | +| 블록 정의 | 속성 정의 `ATTDEF` | ATT | 블록에 포함할 문자 데이터 필드를 정의한다. | C | ☐ | +| 블록 정의 | 속성 관리 `BATTMAN` | — | 블록 속성의 순서·표시·기본값을 관리한다. | C | ☐ | +| 블록 정의 | 속성 동기화 `ATTSYNC` | — | 변경된 속성 정의를 기존 블록 참조에 반영한다. | C | ☐ | +| 블록 정의 | 향상된 속성 편집 `EATTEDIT` | — | 삽입된 블록 참조의 속성 값과 문자·표시 특성을 편집한다. | C | ☐ | +| 참조 | DWG 부착 `XATTACH` | XA | 다른 DWG를 외부 참조로 연결한다. | C | ☐ | +| 참조 | 이미지 부착 `IMAGEATTACH` | IAT | 래스터 이미지를 참조로 연결한다. | A | ☑ 기존 | +| 참조 | PDF 부착 `PDFATTACH` | — | PDF 페이지를 언더레이로 연결한다. | C | ☐ | +| 참조 | DWF/DGN 부착 | — | DWF 또는 DGN을 언더레이로 연결한다. | C | ☐ | +| 참조 | 점 구름 부착 | — | 스캔된 점 구름 데이터를 도면에 연결한다. | C | ☐ | +| 참조 | 외부 참조 팔레트 `XREF` | XR | 참조의 로드·언로드·재로드·경로·결합을 관리한다. | C | ☐ | +| 참조 | 참조 내부 편집 `REFEDIT` | — | 블록 또는 외부 참조를 현재 도면 안에서 직접 편집한다. | C | ☐ | +| 참조 | 외부 참조 자르기 `XCLIP` | XC | DWG 외부 참조 또는 블록의 표시 경계를 지정한다. | C | ☐ | +| 참조 | 이미지 자르기 `IMAGECLIP` | ICL | 부착 이미지의 표시 경계를 지정한다. | B | ☐ | +| 참조 | PDF 자르기 `PDFCLIP` | — | PDF 언더레이의 표시 경계를 지정한다. | C | ☐ | +| 가져오기 | PDF 가져오기 `PDFIMPORT` | — | PDF의 벡터 형상·문자·채움을 도면 객체로 변환한다. | C | ☐ | +| 가져오기 | 가져오기 `IMPORT` | IMP | 지원되는 다른 형식의 데이터를 현재 도면으로 가져온다. | B | ☐ | +| 링크 및 추출 | 데이터 링크 `DATALINK` | DL | Excel 등 외부 표 데이터와 도면 테이블을 연결한다. | C | ☐ | +| 링크 및 추출 | 데이터 추출 `DATAEXTRACTION` | DX | 객체·블록·속성 정보를 표 또는 외부 파일로 추출한다. | B | ☐ | +| 링크 및 추출 | 하이퍼링크 `HYPERLINK` | — | 객체에 웹·파일·도면 뷰 링크를 연결한다. | C | ☐ | +| 링크 및 추출 | 필드 `FIELD` | — | 날짜·도면·객체 특성 등 갱신 가능한 문자 값을 삽입한다. | C | ☐ | +| 위치 | 지리적 위치 `GEOGRAPHICLOCATION` | GEO / NORTH | 좌표계와 지도상의 도면 위치를 지정한다. | C | ☐ | + +## 5. 주석 탭 — 문자·치수·지시선·표·표식 + +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | 반영 등급 | 반영 | +|---|---|---|---|---|---| +| 문자 | 여러 줄 문자 `MTEXT` | T / MT | 서식과 줄바꿈을 지원하는 문단 문자를 작성한다. | A | ☑ | +| 문자 | 단일 행 문자 `TEXT` | DT | 각 행이 독립 객체인 간단한 문자를 작성한다. | A | ☑ | +| 문자 | 문자 스타일 `STYLE` | ST | 글꼴·높이·폭 비율·기울기 등의 스타일을 관리한다. | B | ◐ | +| 문자 | 문자 편집 `TEXTEDIT` | TEDIT | 기존 문자·치수 문자·속성 문자를 수정한다. | A | ☑ | +| 문자 | 찾기·대치 `FIND` | — | 도면의 문자 내용을 검색하고 필요한 문자열을 대치한다. | B | ☑ | +| 문자 | 맞춤법 검사 `SPELL` | SP | 도면 문자와 속성 값의 철자를 검사한다. | C | ☐ | +| 치수 | 빠른 치수 `QDIM` | — | 선택 객체에 여러 치수를 한 번에 작성한다. | B | ☑ | +| 치수 | 치수 `DIM` | — | 선택한 객체와 지정점에 적합한 치수 유형을 작성한다. | A | ☑ | +| 치수 | 선형 치수 `DIMLINEAR` | — | 수평·수직·회전 선형 치수를 작성한다. | A | ☑ | +| 치수 | 정렬 치수 `DIMALIGNED` | — | 두 점과 평행한 실제 길이 치수를 작성한다. | A | ☑ | +| 치수 | 각도 치수 `DIMANGULAR` | DAN | 두 선 또는 호의 각도를 기입한다. | A | ☑ | +| 치수 | 호 길이 치수 `DIMARC` | DAR | 호의 곡선 길이를 기입한다. | B | ☑ | +| 치수 | 반지름 `DIMRADIUS`·지름 `DIMDIAMETER` 치수 | DRA / DDI | 원과 호의 반지름 또는 지름을 기입한다. | A | ☑ | +| 치수 | 꺾기 치수 `DIMJOGGED` | DJO / JOG | 큰 반지름의 중심 위치를 축약해 표시한다. | C | ☐ | +| 치수 | 세로좌표 치수 `DIMORDINATE` | DOR | 기준 원점에 대한 X 또는 Y 좌표를 기입한다. | B | ☑ | +| 치수 | 기준선 `DIMBASELINE`·연속 `DIMCONTINUE` 치수 | DBA / DCO | 공통 기준 또는 앞 치수 끝점에서 연속 기입한다. | A | ☑ | +| 치수 | 중심 표식 `CENTERMARK` | — | 원 또는 호 중심에 연관 중심 표식을 작성한다. | B | ☑ | +| 치수 | 중심선 `CENTERLINE` | — | 두 선 또는 폴리선 세그먼트 사이에 연관 중심선을 작성한다. | B | ☑ | +| 치수 | 치수 스타일 `DIMSTYLE` | D | 치수선·화살표·문자·단위·공차 형식을 관리한다. | A | ☑ | +| 치수 | 치수 업데이트 | — | 변경한 치수 스타일을 기존 치수에 적용한다. | B | ☑ | +| 치수 | 치수 끊기 `DIMBREAK` | — | 교차 객체나 지정점에서 치수선·연장선을 끊어 표시한다. | C | ☐ | +| 치수 | 치수 간격 `DIMSPACE` | — | 평행 또는 동심 치수 사이의 간격을 조정한다. | B | ☑ | +| 치수 | 치수 재연관 `DIMREASSOCIATE` | DRE | 끊어진 치수를 실제 형상점과 다시 연관시킨다. | C | ☐ | +| 치수 | 검사 치수 `DIMINSPECT` | — | 검사 빈도·값 정보를 포함한 검사 치수를 작성한다. | C | ☐ | +| 기호 | 기하공차 `TOLERANCE` | TOL | 형상·자세·위치 공차 기호 프레임을 작성한다. | C | ☐ | +| 지시선 | 다중 지시선 `MLEADER` | MLD | 화살표와 문자 또는 블록 주석을 연결한다. | A | ☑ | +| 지시선 | 다중 지시선 스타일 `MLEADERSTYLE` | MLS | 다중 지시선의 형식과 기본 동작을 작성·수정한다. | B | ☑ | +| 지시선 | 지시선 추가·제거·정렬 | — | 기존 다중 지시선의 가지와 배치를 편집한다. | B | ◐ | +| 지시선 | 지시선 수집 `MLEADERCOLLECT` | MLC | 블록 내용의 여러 다중 지시선을 행 또는 열로 정렬한다. | C | ☐ | +| 표 | 테이블 `TABLE` | TB | 행·열과 셀 서식을 가진 도면 표를 작성한다. | A | ◐ | +| 표 | 테이블 스타일 `TABLESTYLE` | TS | 표의 제목·머리글·데이터 셀 형식을 관리한다. | B | ☑ | +| 표식 | 구름형 리비전 `REVCLOUD` | — | 변경 또는 검토 범위를 구름형 선으로 표시한다. | B | ☑ | +| 주석 축척 | 주석 객체·축척 | — | 여러 뷰포트 축척에서 일정한 출력 크기를 유지한다. | B | ☑ | +| 주석 축척 | 축척 리스트 편집 `SCALELISTEDIT` | — | 도면에서 사용할 주석·뷰포트 축척 목록을 관리한다. | C | ☐ | + +## 6. 파라메트릭 탭 — 구속조건 + +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | 반영 등급 | 반영 | +|---|---|---|---|---|---| +| 기하 구속 | 자동 구속 `AUTOCONSTRAIN` | — | 객체 관계를 분석해 여러 기하 구속을 자동 적용한다. | C | ☐ | +| 기하 구속 | 일치 | — | 두 점 또는 점과 객체를 같은 위치에 구속한다. | C | ☐ | +| 기하 구속 | 동일선상 | — | 객체들이 같은 무한선 위에 있도록 구속한다. | C | ☐ | +| 기하 구속 | 동심 | — | 원·호·타원이 같은 중심을 갖도록 구속한다. | C | ☐ | +| 기하 구속 | 고정 | — | 점 또는 객체의 위치·방향을 고정한다. | C | ☐ | +| 기하 구속 | 평행·직교 | — | 두 선을 평행 또는 직각으로 유지한다. | C | ☐ | +| 기하 구속 | 수평·수직 | — | 선 또는 두 점을 수평·수직으로 유지한다. | C | ☐ | +| 기하 구속 | 접선 | — | 두 곡선이 서로 접하도록 구속한다. | C | ☐ | +| 기하 구속 | 매끄럽게 | — | 스플라인과 다른 곡선을 부드럽게 연결한다. | C | ☐ | +| 기하 구속 | 대칭 | — | 객체를 지정 대칭축에 대해 대칭으로 유지한다. | C | ☐ | +| 기하 구속 | 같음 | — | 객체의 길이 또는 반지름을 같게 유지한다. | C | ☐ | +| 치수 구속 | 선형·정렬·수평·수직 | — | 거리 값을 매개변수로 지정해 형상을 제어한다. | C | ☐ | +| 치수 구속 | 반지름·지름·각도 | — | 크기와 각도를 매개변수로 지정한다. | C | ☐ | +| 치수 구속 | 변환 | — | 일반 치수를 연관 치수 구속으로 변환한다. | C | ☐ | +| 관리 | 매개변수 관리자 `PARAMETERS` | PAR | 이름 있는 변수, 값 및 수식을 관리한다. | C | ☐ | +| 관리 | 구속 표시·숨기기·삭제 | — | 구속 막대와 치수 구속의 표시 및 제거를 관리한다. | C | ☐ | + +## 7. 뷰 탭 — 탐색·뷰·뷰포트·팔레트 + +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | 반영 등급 | 반영 | +|---|---|---|---|---|---| +| 탐색 | 줌 `ZOOM` | Z | 확대·축소, 범위 전체, 윈도우 등의 방법으로 뷰를 변경한다. | A | ☑ 기존 | +| 탐색 | 초점이동 `PAN` | P | 배율을 유지한 채 화면의 관찰 위치를 이동한다. | A | ☑ 기존 | +| 탐색 | 탐색 막대 | — | 줌·초점이동 등 화면 탐색 도구를 모아 제공한다. | A | ☑ 기존 | +| 뷰 | 명명된 뷰 `VIEW` | V | 현재 화면·도면층·UCS 상태를 이름으로 저장하고 복원한다. | B | ☐ | +| 뷰 | 새 명명 뷰 `NEWVIEW` | NVIEW | 현재 화면 또는 지정 영역을 새 이름의 뷰로 저장한다. | B | ☐ | +| 뷰 | 재생성 `REGEN` | RE | 현재 뷰포트의 객체 표시를 다시 계산한다. | B | ☐ | +| 뷰 | 전체 재생성 `REGENALL` | REA | 모든 뷰포트의 객체 표시를 다시 계산한다. | C | ☐ | +| 뷰포트 | 모형 공간 뷰포트 | — | 도면 영역을 여러 독립 관찰 창으로 나눈다. | C | ☐ | +| 뷰포트 | 배치 뷰포트 `MVIEW` | MV | 종이 공간에 모형을 표시하는 출력 창을 작성한다. | B | ☐ | +| 뷰포트 | 뷰포트 잠금 | — | 배치 뷰포트의 축척과 관찰 상태 변경을 막는다. | C | ☐ | +| 좌표 | UCS `UCS` | — | 사용자 좌표계의 원점과 축 방향을 설정한다. | B | ☐ | +| 좌표 | 월드 UCS | — | 좌표계를 기본 WCS로 복원한다. | B | ☐ | +| 팔레트 | 도구 팔레트 `TOOLPALETTES` | TP / Ctrl+3 | 자주 쓰는 블록·해치·명령 도구 모음을 표시한다. | C | ☐ | +| 팔레트 | 개수 `COUNT` | — | 선택 객체·블록의 인스턴스를 세고 결과를 강조·검토한다. | B | ☐ | +| 팔레트 | DesignCenter `ADCENTER` | ADC / Ctrl+2 | 다른 도면의 블록·도면층·스타일을 탐색해 가져온다. | C | ☐ | +| 팔레트 | 시트 세트 관리자 | Ctrl+4 / SSM | 여러 도면 시트의 집합·번호·출력을 관리한다. | C | ☐ | +| 팔레트 | 명령행 | Ctrl+9 / CLI | 명령 입력, 옵션 선택 및 작업 기록을 표시한다. | A | ☑ 기존 | +| 팔레트 | 명령 매크로 | — | 반복 명령 시퀀스를 검토·저장·실행한다. | C | ☐ | +| 인터페이스 | 파일 탭 | Ctrl+Tab | 열려 있는 여러 도면 사이를 전환한다. | B | ☐ | +| 인터페이스 | 타일·계단식 배열 | — | 여러 열린 도면 창의 배치를 조정한다. | C | ☐ | +| 인터페이스 | 전체 화면 정리 `CLEANSCREENON` | Ctrl+0 | 도구막대와 팔레트를 숨겨 도면 영역을 최대화한다. | B | ☐ | + +## 8. 관리 탭 — 표준·사용자화·자동화 + +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | 반영 등급 | 반영 | +|---|---|---|---|---|---| +| 작업 기록 | 동작 기록기 `ACTRECORD` | ARR | 사용자 명령과 입력을 기록해 반복 재생한다. | C | ☐ | +| 사용자화 | 사용자 인터페이스 `CUI` | — | 리본·도구막대·메뉴·단축키·마우스 동작을 사용자화한다. | C | ☐ | +| 사용자화 | 별칭 편집 | — | 명령행에서 쓰는 짧은 명령 별칭을 관리한다. | C | ☐ | +| 사용자화 | 작업공간 | — | 리본·팔레트·도구막대 배치를 저장하고 전환한다. | C | ☐ | +| CAD 표준 | 표준 구성 `STANDARDS` | STA | 도면층·문자·치수·선종류 표준 파일을 연결한다. | C | ☐ | +| CAD 표준 | 표준 검사 `CHECKSTANDARDS` | CHK | 현재 도면이 연결된 CAD 표준과 다른 항목을 찾는다. | C | ☐ | +| CAD 표준 | 도면층 변환 `LAYTRANS` | — | 현재 도면층을 지정 표준 도면층 이름과 특성으로 매핑한다. | C | ☐ | +| 응용프로그램 | 응용프로그램 로드 `APPLOAD` | AP | AutoLISP·ObjectARX 등 확장 프로그램을 로드한다. | C | ☐ | +| 응용프로그램 | Visual LISP | — | AutoLISP 코드를 작성·검사하는 개발 환경을 연다. | C | ☐ | +| 스크립트 | 스크립트 실행 `SCRIPT` | SCR | 텍스트 파일에 기록된 명령 시퀀스를 실행한다. | C | ☐ | +| 정리 | 소거 `PURGE` | PU | 사용하지 않는 블록·도면층·스타일 정의를 제거한다. | B | ☐ | + +## 9. 출력 탭 — 배치·플롯·게시·내보내기 + +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | 반영 등급 | 반영 | +|---|---|---|---|---|---| +| 플롯 | 플롯 `PLOT` | PRINT / Ctrl+P | 프린터·용지·영역·축척·스타일을 설정해 출력한다. | A | ☐ | +| 플롯 | 미리보기 `PREVIEW` | PRE | 실제 출력 전 용지 결과를 확인한다. | A | ☐ | +| 플롯 | 페이지 설정 `PAGESETUP` | — | 배치별 출력 장치와 용지 설정을 저장한다. | B | ☐ | +| 플롯 | 플롯 스타일 | — | 색상·선가중치 등의 CTB/STB 출력 규칙을 관리한다. | C | ☐ | +| 플롯 | 플로터 관리자 `PLOTTERMANAGER` | — | 플로터 구성 파일과 장치 설정을 관리한다. | C | ☐ | +| 플롯 | 플롯 스타일 관리자 `STYLESMANAGER` | — | CTB·STB 플롯 스타일 테이블 파일을 관리한다. | C | ☐ | +| 게시 | 게시 `PUBLISH` | — | 여러 도면·배치를 한 번에 DWF·PDF·프린터로 출력한다. | B | ☐ | +| 게시 | 배치 플롯 | — | 시트 목록을 구성해 일괄 출력한다. | B | ☐ | +| 내보내기 | PDF 내보내기 `EXPORTPDF` | EPDF | 도면 또는 배치를 PDF 파일로 만든다. | A | ☐ | +| 내보내기 | DWF/DWFx 내보내기 | — | 검토·배포용 Autodesk 형식으로 내보낸다. | C | ☐ | +| 내보내기 | 기타 형식 `EXPORT` | EXP | 지원되는 다른 교환 파일 형식으로 저장한다. | A | ☑ 기존 | +| 전송 | 전자 전송 `ETRANSMIT` | ZIP | 도면과 참조·글꼴·플롯 설정을 하나의 전달 패키지로 묶는다. | C | ☐ | + +## 10. 공동작업·검토 + +| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | 반영 등급 | 반영 | +|---|---|---|---|---|---| +| 비교 | DWG 비교 `COMPARE` | — | 두 DWG의 추가·삭제·변경 객체를 색으로 구분한다. | C | ☐ | +| 비교 | 외부 참조 비교 | — | 부착된 외부 참조의 변경 사항을 현재 도면과 비교한다. | C | ☐ | +| 검토 | 추적 `TRACE` | — | 원도면을 바꾸지 않는 검토용 투명 오버레이를 만든다. | C | ☐ | +| 검토 | 추적에서 복사 `COPYFROMTRACE` | — | 추적에 있는 객체를 원도면의 같은 위치로 복사한다. | C | ☐ | +| 검토 | 표식 가져오기 `MARKUPIMPORT` | — | PDF·JPG·PNG 표식을 새 추적으로 가져와 원도면 위에 겹친다. | C | ☐ | +| 검토 | 표식 도우미 `MARKUPASSIST` | — | 표식의 문자·구름·지시 등을 인식해 도면 반영을 돕는다. | C | ☐ | +| 공유 | 뷰 공유 `SHAREDVIEWS` | — | 도면의 온라인 검토용 뷰를 생성하고 의견을 교환한다. | C | ☐ | +| 공유 | 도면 공유 `SHARE` | — | 현재 도면 복사본을 웹·모바일에서 보거나 편집하는 링크를 만든다. | C | ☐ | +| 이력 | 활동 정보 `ACTIVITYINSIGHTSOPEN` | AI OPEN | 도면 열기·저장·편집 등 작업 이벤트를 추적한다. | C | ☐ | +| 버전 | 버전 이력 `DWGHISTORY` | — | 지원되는 클라우드에 저장된 도면의 이전 버전을 조회·비교한다. | C | ☐ | + +## 11. 리본 밖의 핵심 제도 기능 + +| 영역 | 기능 | 단축키 | 짧은 설명 | 반영 등급 | 반영 | +|---|---|---|---|---|---| +| 선택 | 창 선택 | — | 왼쪽→오른쪽 사각형 안에 완전히 포함된 객체를 선택한다. | A | ☑ 기존 | +| 선택 | 교차 선택 | — | 오른쪽→왼쪽 영역에 포함되거나 걸치는 객체를 선택한다. | A | ☑ 기존 | +| 선택 | 올가미 선택 | — | 자유형 경계로 객체를 포함 또는 교차 선택한다. | B | ☐ | +| 선택 | 선택 순환 | — | 겹친 객체 목록에서 원하는 객체를 골라 선택한다. | B | ☐ | +| 직접 편집 | 그립 편집 | — | 선택 객체의 정점·중간점·반지름 등을 직접 끌어 수정한다. | A | ☐ | +| 직접 편집 | 다기능 그립 | — | 폴리선 정점 추가·제거, 호 전환 등 상황별 작업을 제공한다. | B | ☐ | +| 정확도 | 객체 스냅 `OSNAP` | OS / F3 | 끝점·중간점·중심·교차점·접점 등 정확한 점을 포착한다. | A | ☑ 기존 | +| 정확도 | 객체 스냅 추적 `(F11)` | F11 | 포착한 점에서 임시 정렬 경로를 추적한다. | B | ☐ | +| 정확도 | 극좌표 추적 `(F10)` | F10 | 지정 각도 증분을 따라 커서 이동을 안내한다. | B | ☐ | +| 정확도 | 직교 모드 `(F8)` | F8 | 커서 이동을 현재 UCS의 수평·수직 방향으로 제한한다. | A | ☑ 기존 | +| 정확도 | 그리드·스냅 | F7 / F9 | 화면 격자를 표시하고 커서 이동 간격을 제한한다. | A | ☑ 기존 | +| 정확도 | 동적 입력 `(F12, DYNMODE 시스템 변수)` | F12 | 커서 근처에서 좌표·거리·각도 및 명령 옵션을 입력한다. | A | ☑ 기존 | +| 정확도 | 직접 거리 입력 | — | 방향을 지정한 뒤 키보드로 정확한 거리를 입력한다. | A | ☑ 기존 | +| 좌표 | 절대·상대·극좌표 | — | 전역 좌표, 이전 점 기준 좌표, 거리·각도로 점을 입력한다. | A | ◐ 기존 | +| 작업 흐름 | 명령 직접 입력 | — | 명령 이름, 별칭, 옵션, 숫자와 좌표를 명령행에 연속 입력한다. | A | ☑ 기존 | +| 작업 흐름 | 명령 자동완성 | — | 입력 중인 명령·시스템 변수·콘텐츠를 검색해 제안한다. | A | ◐ 기존 | +| 작업 흐름 | 실행 취소·다시 실행 | Ctrl+Z / Ctrl+Y | 명령 단위로 작업 이력을 되돌리거나 복원한다. | A | ☑ 기존 | +| 작업 흐름 | 반복·최근 명령 | — | 직전 또는 최근 사용 명령을 다시 실행한다. | B | ☐ | +| 표시 | 선가중치 표시 | — | 실제 출력 선 굵기의 화면 표시를 전환한다. | B | ☐ | +| 표시 | 투명도 표시 | — | 객체·도면층 투명도의 화면 표시를 전환한다. | C | ☐ | +| 표시 | 주석 가시성 | — | 현재 축척을 지원하지 않는 주석의 표시 여부를 전환한다. | C | ☐ | +| 공간 | 모형·배치 | — | 실제 크기 설계 공간과 용지 출력 공간을 전환한다. | B | ☐ | +| 공간 | 뷰포트 축척 | — | 배치에서 모형 표시 축척을 정밀하게 지정한다. | B | ☐ | +| 파일 | 새 도면·열기·저장·다른 이름 저장 | Ctrl+N / Ctrl+O / Ctrl+S / Ctrl+Shift+S | DWG 작업 파일의 기본 수명주기를 관리한다. | A | ◐ 기존 | +| 파일 | 자동 저장·복구 | — | 비정상 종료 시 사용할 임시 저장과 복구 정보를 관리한다. | B | ☐ | +| 파일 | 감사 `AUDIT` | — | 응용프로그램 메뉴에서 열린 도면의 오류를 검사·수정한다. | C | ☐ | +| 파일 | 복구 `RECOVER` | — | 응용프로그램 메뉴에서 손상된 도면을 복구하면서 연다. | C | ☐ | +| 호환 | DWG 변환 `DWGCONVERT` | — | 여러 도면을 지정 DWG 버전으로 일괄 변환한다. | C | ☐ | +| 호환 | 단위 `UNITS` | UN | 길이·각도 형식, 정밀도 및 삽입 축척 단위를 설정한다. | B | ☐ | + +## 12. 2D 상황별 리본 탭 + +> 해당 객체를 선택하거나 편집 명령을 실행할 때만 나타나는 리본이다. + +| 상황별 탭 | 기능 | 단축키 | 짧은 설명 | 반영 등급 | 반영 | +|---|---|---|---|---|---| +| 해치 작성/편집 | 원점 설정 | — | 해치 패턴이 시작되는 기준점을 지정한다. | B | ☐ | +| 해치 작성/편집 | 연관·주석 | — | 경계 연동과 주석 축척 적용 여부를 설정한다. | B | ☐ | +| 해치 작성/편집 | 경계 재작성 | — | 해치에서 폴리선 또는 영역 경계를 다시 만든다. | C | ☐ | +| 문자 편집기 | 굵게·기울임·밑줄·색상 | — | 선택 문자 범위의 서식을 변경한다. | B | ☐ | +| 문자 편집기 | 정렬·들여쓰기·줄 간격 | — | 문단 배치와 간격을 조정한다. | B | ☐ | +| 문자 편집기 | 열 | — | 여러 줄 문자를 동적·정적 열로 나눈다. | C | ☐ | +| 블록 편집기 | 블록 테스트 | — | 동적 블록의 그립과 동작을 시험한다. | C | ☐ | +| 블록 편집기 | 매개변수·동작 | — | 동적 블록에 점·선형·회전 등의 매개변수와 동작을 지정한다. | C | ☐ | +| 블록 편집기 | 가시성 상태 | — | 하나의 블록에서 표시할 객체 조합을 관리한다. | C | ☐ | +| 배열 편집 | 항목 수·간격 | — | 배열 항목의 개수와 행·열 또는 경로 간격을 수정한다. | B | ☐ | +| 배열 편집 | 항목 교체·편집 | — | 배열의 원본 항목을 편집하거나 다른 객체로 바꾼다. | B | ☐ | +| 배열 편집 | 원본 편집·재설정 | — | 원본 객체를 편집하고 배열 재지정을 초기화한다. | C | ☐ | +| 외부 참조·언더레이 | 페이드·대비 | — | 외부 참조와 PDF·DWF·DGN 언더레이의 화면 특성을 조정한다. | C | ☐ | +| 외부 참조 | 참조 도면층 | — | 참조에 포함된 도면층 표시를 관리한다. | C | ☐ | +| 이미지 | 페이드·대비·밝기 | — | 래스터 이미지의 화면 특성을 조정한다. | B | ☐ | +| PDF/DWF/DGN 언더레이 | 단색·언더레이 도면층 | — | 단색 표시와 원본 도면층 가시성을 관리한다. | C | ☐ | +| 배치 | 명명된 뷰 삽입 | — | 저장된 모형 뷰를 배치 뷰포트로 배치한다. | C | ☐ | +| 테이블 셀 | 행·열 삽입·삭제 | — | 선택 셀 주변의 표 구조를 편집한다. | B | ☐ | +| 테이블 셀 | 병합·병합 해제 | — | 여러 셀을 합치거나 원래 셀로 되돌린다. | B | ☐ | +| 테이블 셀 | 셀 형식·정렬·테두리 | — | 데이터 형식과 표시 서식을 조정한다. | B | ☐ | + +## 13. 실제 리본 아이콘 조사 상태 + +| 확인 항목 | 현재 문서 반영 상태 | 판정 | +|---|---|---| +| 기능·명령명과 짧은 설명 | 반영 | B07 기능 후보 검토에 사용 가능 | +| 작업 기준 탭·패널 분류 | 반영 | 실제 기본 리본 배치와 다를 수 있음 | +| 리본 버튼의 실제 존재 여부 | 미검증 | 아이콘 목록으로 사용 불가 | +| 기본 표시·확장 패널·드롭다운 구분 | 미검증 | AutoCAD 2024 실행 화면 또는 CUIx 확인 필요 | +| 버튼 표시명·툴팁·CUI 명령 ID | 미수집 | 실제 아이콘 단위 조사 필요 | +| 아이콘 이미지·리소스명 | 미수집 | 실제 아이콘 자산 비교 필요 | +| 상황별 탭 표시 조건 | 일부 기능만 정리 | 객체별 실행 검증 필요 | + +## 14. 조사 근거 및 재검증 자료 + +| 구분 | 자료 | 확인 내용 | +|---|---|---| +| Autodesk 도움말 | [AutoCAD 2024 한국어 도움말](https://help.autodesk.com/view/ACD/2024/KOR/) | 기본 AutoCAD 명령·작업 절차·UI 기능 | +| Autodesk 공식 PDF | [AutoCAD 2024 Shortcuts Guide](https://damassets.autodesk.net/content/dam/autodesk/www/shortcuts/autocad/autocad-shortcut-guide-en.pdf) | 그리기·수정·주석·도면층·조회 등 주요 명령 교차 확인 | +| Autodesk 공식 블로그 | [Essential Keyboard Shortcuts and Commands](https://www.autodesk.com/blogs/autocad/work-faster-in-autocad-essential-keyboard-shortcuts-and-commands/) | 대표 명령의 작업별 분류와 짧은 기능 설명 교차 확인 | +| Autodesk 도움말 | [블록 삽입 정보](https://help.autodesk.com/cloudhelp/2024/KOR/AutoCAD-Core/files/GUID-BC0FD3C1-3BFC-4C5D-AB9A-BF480D5084BE.htm) | 홈 탭 블록 패널, 블록 갤러리와 팔레트 | +| Autodesk 도움말 | [외부 참조 경로로 작업하려면](https://help.autodesk.com/cloudhelp/2024/KOR/AutoCAD-Core/files/GUID-97F90017-E17A-4209-A6BD-1E205BF892E1.htm) | 뷰 탭 팔레트 패널과 외부 참조 관리 | +| Autodesk 도움말 | [도면 비교를 통해 작업하려면](https://help.autodesk.com/cloudhelp/2024/KOR/AutoCAD-Core/files/GUID-744D8A49-D582-4C06-80B1-6B0313E92CA2.htm) | 뷰·공동작업 탭의 DWG 비교 기능 | +| Autodesk 도움말 | [리본 사용자화 FAQ](https://help.autodesk.com/cloudhelp/2024/KOR/AutoCAD-Core/files/GUID-7711CB8D-C865-42C1-9F21-7179FA7BBE63.htm) | 리본 탭·패널·명령의 사용자화 가능 범위 | +| Autodesk 도움말 | [명령 매크로 정보](https://help.autodesk.com/cloudhelp/2024/KOR/AutoCAD-Core/files/GUID-6B27C0A2-6446-4C75-9853-6834F4FFFF89.htm) | 뷰·자동화 탭의 명령 매크로 팔레트 | +| Autodesk 도움말 | [점에서 끊기](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DidYouKnow/files/GUID-73B7DEA8-309E-4823-9F65-DB0A2E1FBC75.htm) | `BREAKATPOINT`의 지원 버전과 홈 탭 위치 | +| Autodesk 도움말 | [파라메트릭 도면과 구속조건](https://help.autodesk.com/cloudhelp/2023/ENU/AutoCAD-Core/files/GUID-899E008D-B422-4DF2-AC8D-1A4F5701ED4E.htm) | 기하·치수 구속의 범위와 동작 | +| Autodesk 도움말 | [Markup Import와 Markup Assist](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Platform/files/GUID-0CD6B2CE-EAF0-478B-B39B-F4821F9DC91D.htm) | 표식 가져오기·인식·반영 기능과 제약 | +| Autodesk 도움말 | [Activity Insights](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-WhatsNew/files/GUID-074F77C1-496D-49D2-8D64-FD545753CAB7.htm) | 2024 작업 이벤트 기록 기능과 리본 위치 | +| Autodesk 도움말 | [Drawing History 팔레트](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Core/files/GUID-6916CA9F-0BD1-49ED-9DA4-2FA3615998DD.htm) | 지원 클라우드 도면의 버전 조회·비교 기능 | +| Autodesk 도움말 | [Shared Views 명령](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Core/files/GUID-C9055F88-D079-441F-938B-37A7E0E390D9.htm) | 온라인 검토 뷰와 의견 교환 기능 | +| Autodesk 도움말 | [Action Macro 기록](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Customization/files/GUID-FEAD3614-CD33-4B60-BC00-4CBC98D8CBCB.htm) | 동작 기록기의 명령·입력 기록 및 재생 기능 | +| Autodesk 도움말 | [Purge 대화상자](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Core/files/GUID-C6B62BBD-C363-4DC7-AF43-036A7B287473.htm) | 미사용 명명 객체와 고아 데이터 정리 기능 | +| Autodesk 도움말 | [AutoCAD 2024 새 명령·시스템 변수](https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Core/files/GUID-B93A458E-1A7F-4090-A8CF-87A31C24E404.htm) | 2024의 Activity Insights·Smart Blocks·Trace·Markup 명령 대조 | +| Autodesk 도움말 | [AutoCAD 2024 변경 명령·시스템 변수](https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Core/files/GUID-4FEBA606-95E0-4DC4-A116-257ED86DCD58.htm) | `INSERT`·`COUNT`·`TRACE`·`SHARE` 변경 내용 대조 | +| Autodesk 도움말 | [COUNT 명령](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Core/files/GUID-3A0C3460-6ABC-4D13-BF1F-D2BFCD399851.htm) | 동일 객체·블록 개수 계산 기능 | +| Autodesk 도움말 | [객체 개수로 작업](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-27873872-0DB9-4EE1-97F5-43F91F9C3785.htm) | 뷰 탭 팔레트 패널의 Count 위치와 검토 기능 | +| Autodesk 도움말 | [OVERKILL 명령](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-44B9ECFC-752C-4CC5-9DA3-84DBF3B17CA6.htm) | 중복·겹침 객체 제거 및 결합 기능 | +| Autodesk 도움말 | [Smart Blocks 교체](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-WhatsNew/files/GUID-4F8E11D6-917A-43F3-B596-C8F296D32519.htm) | `BREPLACE`의 제안·교체와 특성 유지 동작 | +| Autodesk 도움말 | [Trace 업데이트](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-WhatsNew/files/GUID-F9C11306-0F53-4F1A-92C7-8D77EA42823A.htm) | `COPYFROMTRACE`와 추적 화면표시 설정 | +| Autodesk 도움말 | [상태막대 빠른 참조](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-E3B34B0A-EA98-45E9-937A-BF5FEF4152DB.htm) | 스냅·직교·극좌표·동적 입력 등 제도 토글의 기능 | +| Autodesk 도움말 | [열린 도면 복구](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-LT/files/GUID-7AFE414B-BCC6-470C-B0E9-8228A252EE7C.htm) | 응용프로그램 메뉴의 `AUDIT` 위치와 `RECOVER` 구분 | -- 2.54.0 From 465ff2b8ef7d492e89304073909eab2ac483f064 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 02:23:51 +0900 Subject: [PATCH 07/66] =?UTF-8?q?fix(B05):=20=EC=A2=85=EB=8B=A8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20=EB=B9=88=20=EC=B9=B8=EC=9D=98=20=EC=84=B8=EB=A1=9C?= =?UTF-8?q?=EC=84=A0=EC=9D=84=20=EC=A7=80=EC=9A=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구조물 측점 빈 칸의 세로선이 이웃 규칙 측점 값 한가운데를 지나고, 곡선 L/R 칸 경계와 x가 겹쳐 곡선 행 구분선이 위 행들까지 이어진 것처럼 보였다. 세로선을 곡선 행에만 남기고 값 행에서는 지운다 (2026-08-30 사용자 지시). 저장소 사고(2026-08-30 01:12)로 유실된 커밋 84d02b68의 작업물을 복원한 것이다. 원 작업자는 병렬 B05/B06 세션이며 워킹트리에 남아 있던 변경을 그대로 커밋했다. --- B05_Profile/B05_Profile_UI_Style.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/B05_Profile/B05_Profile_UI_Style.css b/B05_Profile/B05_Profile_UI_Style.css index cef484d2..665e004c 100644 --- a/B05_Profile/B05_Profile_UI_Style.css +++ b/B05_Profile/B05_Profile_UI_Style.css @@ -636,6 +636,13 @@ border-right: 1px solid var(--color-border); } +/* 구조물 측점 빈 칸의 세로선은 이웃 규칙 측점의 값 한가운데를 지나고, 곡선 L/R 칸 + 경계와 x가 겹쳐 곡선 행 구분선이 위 행들까지 이어진 것처럼 보인다. 곡선 행에만 + 남기고 값 행에서는 지운다(2026-08-30 사용자 지시). */ +.b05-profile-table__cell.is-structure { + border-left-color: transparent; +} + .b05-profile-table__row.is-cut .b05-profile-table__cell { color: rgb(220 38 38); } -- 2.54.0 From 14044de50e2fd9dc905b9a272799b1fac9d0e0e1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 02:23:52 +0900 Subject: [PATCH 08/66] =?UTF-8?q?fix(B06):=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EA=B8=B0=EC=8A=AD=EB=A7=89=EC=9D=B4=EB=A5=BC=20=EA=B3=A8?= =?UTF-8?q?=EB=9D=BC=EB=8F=84=20=EB=B6=80=EB=AA=A8=20=EB=B2=BD=EC=9D=98=20?= =?UTF-8?q?=EB=8B=A8=20=EC=88=98=C2=B7=EC=98=B5=EC=85=98=EC=9D=84=20?= =?UTF-8?q?=EC=84=B8=EC=9B=8C=20=EB=91=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 다단 벽을 고르면 단 수·옵션 행이 사라지던 문제. 두 값은 그 단이 딸린 부모 벽의 것이므로 다단 쪽으로 옮기지 않고 부모 칸에 남긴다. - Adjust_Dock: EXTRA_ROWS에서 단 수·옵션을 빼고, 행 이동을 moveRows()로 묶어 다단 선택 시 형태·높이·구간값은 다단 칸으로, 단 수·옵션은 부모 칸으로 보낸다. - Cross_Structure_Panel: extra/bextra 선택에서도 단 수 채널을 부모 쪽(outlet·basin) 으로 열어 둔다. 저장소 사고(2026-08-30 01:12)로 유실된 커밋 bc8a3477의 작업물을 복원한 것이다. 원 작업자는 병렬 B05/B06 세션이며 워킹트리에 남아 있던 변경을 그대로 커밋했다. --- B06_Section/B06_Section_UI_Adjust_Dock.ts | 40 +++++++++++-------- .../B06_Section_UI_Cross_Structure_Panel.ts | 13 +++++- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/B06_Section/B06_Section_UI_Adjust_Dock.ts b/B06_Section/B06_Section_UI_Adjust_Dock.ts index 52ddbb81..18a83ce5 100644 --- a/B06_Section/B06_Section_UI_Adjust_Dock.ts +++ b/B06_Section/B06_Section_UI_Adjust_Dock.ts @@ -30,13 +30,12 @@ export function setAdjustSideSlots(next: SideSlots): void { sideSlots = next; } -/** 다단 벽에서 옮겨 오는 행 — 폼에 자기 칸이 없어 형태·높이·구간값까지 전부 온다. */ +/** 다단 벽에서 옮겨 오는 행 — 폼에 자기 칸이 없어 형태·높이·구간값이 온다. + * 단 수·옵션은 부모 벽의 값이라 여기 오지 않고 부모 칸에 남는다(2026-08-30 사용자). */ const EXTRA_ROWS = [ ".b06-structure-panel__struct--form", ".b06-structure-panel__struct--height", ".b06-structure-panel__struct--span", - ".b06-structure-panel__struct--tier", - ".b06-structure-panel__struct--options", ].join(", "); /** 유입구·유출구는 폼이 이미 가진 항목 말고 단 수·옵션만 받는다. */ @@ -97,6 +96,18 @@ function clearSideSlots(): void { } } +/** 행 DOM을 제자리 표식과 함께 옮긴다 — 숨은 행은 그대로 둔다. */ +function moveRows(panelRoot: HTMLElement, selector: string, target: HTMLElement): void { + panelRoot.querySelectorAll(selector).forEach((row) => { + if (row.classList.contains("is-hidden")) return; + // 제자리에 표식을 남기고 옮긴다 — 선택이 바뀌면 이 자리로 되돌린다. + const anchor = document.createComment("adjust-row"); + row.parentNode?.insertBefore(anchor, row); + movedRows.push({ row, anchor }); + target.append(row); + }); +} + /** * 조정창 사본의 단 수·옵션 행을 고른 벽 쪽(유입/유출) 그룹으로 옮긴다. 행 DOM을 * 그대로 옮기므로 배선·값 동기는 손대지 않는다. 자리가 없으면 사본에 그대로 둔다. @@ -111,21 +122,18 @@ function relocateSideRows(panelRoot: HTMLElement): void { if (side === "extra") { const group = target.closest("fieldset"); if (!group) return; - const owner = slotOf(panelRoot.dataset.extraSide === "inlet" ? "inlet" : "outlet").closest( - "fieldset", - ); + const ownerSlot = slotOf(panelRoot.dataset.extraSide === "inlet" ? "inlet" : "outlet"); + const owner = ownerSlot.closest("fieldset"); if (owner && owner !== group) owner.after(group); group.hidden = false; - } else if (!slotUsable(target)) return; - const rows = panelRoot.querySelectorAll(side === "extra" ? EXTRA_ROWS : SIDE_ROWS); - rows.forEach((row) => { - if (row.classList.contains("is-hidden")) return; - // 제자리에 표식을 남기고 옮긴다 — 선택이 바뀌면 이 자리로 되돌린다. - const anchor = document.createComment("adjust-row"); - row.parentNode?.insertBefore(anchor, row); - movedRows.push({ row, anchor }); - target.append(row); - }); + moveRows(panelRoot, EXTRA_ROWS, target); + // 단 수·옵션은 그 단이 딸린 부모 벽의 값이라 부모 칸에 그대로 세운다 — 다단을 + // 고를 때 그 두 행이 사라지던 것을 막는다(2026-08-30 사용자). + if (slotUsable(ownerSlot)) moveRows(panelRoot, SIDE_ROWS, ownerSlot); + } else { + if (!slotUsable(target)) return; + moveRows(panelRoot, SIDE_ROWS, target); + } // 고른 구조물이 어느 쪽인지 그룹 테두리로 알린다(2026-08-29 사용자 지시 3). const group = target.closest("fieldset"); if (!group) return; diff --git a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts index 3349554f..b360e64d 100644 --- a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts +++ b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts @@ -569,7 +569,15 @@ export function buildStructurePanel( // 유출 벽과 독립 기슭막이 유입 벽에서는 **항상** 보인다(2026-08-22 사용자 ⑤ — // 상태 따라 나타났다 사라지면 레이아웃이 널뛴다). 집수정 계류측은 둘 자리가 // 있을 때만 뜬다(배관 유입 기슭막이는 다단 채널 자체가 없다). - tierChannel = key === "outlet" ? "outlet" : key === "inlet" ? "basin" : null; + // 다단 벽을 고른 동안에도 단 수는 **그 단이 딸린 부모 벽**의 값이다 — 채널을 + // 부모 쪽으로 열어 두고 행은 부모 칸에 남긴다(2026-08-30 사용자: 추가 기슭막이를 + // 고르면 단 수·옵션이 사라지던 것). + tierChannel = + key === "outlet" || key.startsWith("extra") + ? "outlet" + : key === "inlet" || key.startsWith("bextra") + ? "basin" + : null; // 좌측 [횡단 조정] 사본의 단 수·옵션 행이 어느 그룹(유입구·유출구)으로 옮겨 // 붙을지 알린다(2026-08-29 지시 4). 다단 벽은 자기 채널 쪽으로 간다. // 다단(추가) 벽은 유입·유출 어느 칸에도 자기 자리가 없다 — 유입구·유출구 아래 @@ -596,7 +604,8 @@ export function buildStructurePanel( // 기슭막이 기준벽(배관 유입·유출, 독립 좌·우)에서는 **항상** 보인다(2026-08-30 // 사용자). 링크 카드에서 만져도 제어기가 소유 측점 값을 고치므로 안전하다. // 집수정 유입은 계류측 단이 설 자리가 있을 때만 — 벽이 아니라 구체다. - const revetWall = (key === "inlet" || key === "outlet") && !isBasin; + // 다단 벽도 포함한다 — 행은 부모 칸에 서 있고, 그 값(부모 단 수·옵션)을 그대로 쓴다. + const revetWall = ((key === "inlet" || key === "outlet") && !isBasin) || isExtra; extraRow.classList.toggle( "is-hidden", !revetWall && !(isBasin && tierChannel === "basin" && (tiers.canAdd || tiers.count > 0)), -- 2.54.0 From 03840ff3d670977d82ea28dcb8eb9baf1af403e5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 11:44:54 +0900 Subject: [PATCH 09/66] =?UTF-8?q?feat(B06):=20=EC=84=B8=EC=9B=94=EA=B5=90?= =?UTF-8?q?=20=EC=A2=8C=EC=B8=A1=20=E3=80=8C=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EB=B0=B0=EC=B9=98=E3=80=8D=20=ED=8F=BC=EC=9D=84=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95=EC=B0=BD=20=EB=A1=9C=EC=A7=81=EC=97=90=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 좌측 폼의 세월교 상세 항목(관종·관경·수량·월류 폭·날개벽 유입/유출)이 도면에 전혀 반영되지 않았다. `applyPipeOptionsToCache`가 `section.culvert`만 보고 세월교 측점(`section.ford`)에서는 즉시 return했기 때문이다. 프론트는 구조물 배치의 상세 UI를 쓰고 로직은 조정창 제어기 것을 쓴다 (2026-08-30 사용자 확정). - `applyFordFormOptions()` 신설 — 폼 옵션 키를 `FordControl.setPipe`/`setWing` 인자로 옮긴다. 캐시 반영·바닥판 연장 재계산·정본 예약·카드 갱신은 제어기에 이미 있어 새로 만들지 않았다. - `FordControl.setPipe` 확장 — `pipe_kind`(관종)·`ford_width_m`(월류 폭 = `span_m`) 추가. 두 값은 조정창에 칸이 없고 폼만 보낸다. - `withFordSpec()` — 폼 기본값을 `section.ford` 스펙에서 읽는다. 두 창이 같은 스펙을 보므로 어느 쪽에서 만졌든 값이 갈리지 않는다(수량(련) 빈 칸 해소). 월류 높이(`ford_height_m`)는 백엔드 `_ford_set`이 읽지 않는다 — 세월교는 구체 위 노면이라 파임이 없고, 물넘이포장 전용 값이다. 폼에는 남되 도면 반영처는 없다. 자체검증(공용 브라우저 5174, 측점 7+9.7 세월교): - tsc --noEmit 통과. - 폼 날개 길이(유입) 2→4 → 조정창 「날개 길이 4.0m」, 바닥판 7.63m→9.04m (= +2×cos45°). 수정 전에는 둘 다 그대로였다. - 폼 수량 3 → 조정창 「3련」, 폼 관경 Ø1200 → 조정창 select 1200. - 재선택 왕복: 월류폭 12·수량 3·관경 1200·날개길이 4 그대로 되읽힘(스펙 정본). - 진입 시 수량(련) 칸 = 1(수정 전 빈 칸), 관종 파형강관. - 조작값은 원래 값(10/1/1000/2)으로 되돌려 놓았다. Co-Authored-By: Claude Opus 5 (1M context) --- .../B06_Section_UI_Cross_Ford_Panel.ts | 13 ++++- B06_Section/B06_Section_UI_Page.ts | 12 ++++- .../B06_Section_UI_Page_Ford_Controls.ts | 48 +++++++++++++++++++ .../B06_Section_UI_Page_Structures_Panel.ts | 30 ++++++++++++ 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts b/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts index 96bd8a1a..ca395b77 100644 --- a/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts +++ b/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts @@ -277,8 +277,17 @@ export interface FordControl { adjustFor: (chainageM: number) => FordAdjust; update: (chainageM: number, role: FordWallRole, patch: Partial) => void; reset: (chainageM: number, role: FordWallRole) => void; - /** 관경(mm)·수량(련) 저장 — B05 정본(`pipe_points`)으로 간다. */ - setPipe: (chainageM: number, patch: { pipe_diameter_mm?: number; pipe_count?: number }) => void; + /** 관경(mm)·수량(련)·관종·월류 폭 저장 — B05 정본(`pipe_points`)으로 간다. + * 관종·월류 폭은 조정창에 칸이 없고 좌측 「구조물 배치」 폼만 보낸다(2026-08-30 사용자). */ + setPipe: ( + chainageM: number, + patch: { + pipe_diameter_mm?: number; + pipe_count?: number; + pipe_kind?: string; + ford_width_m?: number; + }, + ) => void; /** 날개벽 제원 저장(2026-08-25 사용자 — 조정창에서 직접 제어). 같은 정본으로 간다. */ setWing: ( chainageM: number, diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 8ae7da6f..85535789 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -31,6 +31,7 @@ import { } from "./B06_Section_Api_Fetch"; import { flushPendingStructures } from "../B05_Profile/B05_Profile_Api_Structures"; import { createStationControls } from "./B06_Section_UI_Page_Station_Controls"; +import { applyFordFormOptions } from "./B06_Section_UI_Page_Ford_Controls"; import { buildCrossPatches } from "./B06_Section_UI_Page_Patches"; import { maxToeFitHalfWidth } from "./B06_Section_UI_Cross_Fit"; import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit"; @@ -194,8 +195,15 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { const owner = sectionDetail?.cross_sections.find( (section) => Math.abs(section.chainage_m - chainageM) < 0.51, ); - const culvert = owner?.culvert; - if (!owner || !culvert) return; + if (!owner) return; + // 세월교는 스펙 자리(`section.ford`)가 배수관과 달라 조정창 제어기로 보낸다 — + // 로직은 그쪽 것을 쓰고 프론트만 폼 UI다(2026-08-30 사용자 확정). + if (owner.ford) { + applyFordFormOptions(stationControls.ford, owner.chainage_m, patch); + return; + } + const culvert = owner.culvert; + if (!culvert) return; const num = (key: string): number | undefined => { const value = Number(patch[key]); return Number.isFinite(value) ? value : undefined; diff --git a/B06_Section/B06_Section_UI_Page_Ford_Controls.ts b/B06_Section/B06_Section_UI_Page_Ford_Controls.ts index ef97eaec..4be840fb 100644 --- a/B06_Section/B06_Section_UI_Page_Ford_Controls.ts +++ b/B06_Section/B06_Section_UI_Page_Ford_Controls.ts @@ -114,6 +114,9 @@ export function createFordControls(deps: FordControlDeps): FordControls { // 캐시를 먼저 고쳐 즉시 반영한다 — 저장은 늦게 묶어서 간다. if (patch.pipe_diameter_mm) spec.diameter_m = patch.pipe_diameter_mm / 1000; if (patch.pipe_count) spec.pipe_count = patch.pipe_count; + if (patch.pipe_kind) spec.pipe_kind = patch.pipe_kind; + // 월류 폭 = 구체의 도로 진행 방향 길이(`span_m`) — 백엔드 `_ford_set`과 같은 자리. + if (patch.ford_width_m) spec.span_m = patch.ford_width_m; } deps.queuePipeOptions(chainageM, patch); deps.refreshCard(chainageM); @@ -160,6 +163,51 @@ export function createFordControls(deps: FordControlDeps): FordControls { }; } +/** + * 좌측 「구조물 배치」 폼이 낸 세월교 옵션을 **조정창 제어기**로 흘려보낸다 + * (2026-08-30 사용자 확정: 프론트는 구조물 배치의 상세 UI, 로직은 조정창 것을 쓴다). + * + * 폼 옵션 키(`pipe_*`·`ford_width_m`·`wing_in*`·`wing_out*`)를 제어기 인자로 옮기기만 + * 한다 — 캐시 반영·바닥판 연장 재계산·정본 예약·카드 갱신은 제어기 안에 이미 있다. + * 값이 온 항목만 보낸다(빈 patch는 부르지 않는다). + */ +export function applyFordFormOptions( + control: FordControl, + chainageM: number, + patch: Record, +): void { + const num = (key: string): number | undefined => { + if (patch[key] === undefined) return undefined; + const value = Number(patch[key]); + return Number.isFinite(value) ? value : undefined; + }; + const pipe: Parameters[1] = {}; + const diameterMm = num("pipe_diameter_mm"); + if (diameterMm !== undefined) pipe.pipe_diameter_mm = diameterMm; + const count = num("pipe_count"); + if (count !== undefined) pipe.pipe_count = count; + if (typeof patch.pipe_kind === "string") pipe.pipe_kind = patch.pipe_kind; + const widthM = num("ford_width_m"); + if (widthM !== undefined) pipe.ford_width_m = widthM; + if (Object.keys(pipe).length) control.setPipe(chainageM, pipe); + + for (const [role, prefix] of [ + ["inlet", "wing_in"], + ["outlet", "wing_out"], + ] as const) { + const wing: Parameters[2] = {}; + // 설치 값은 폼이 "있음"/"없음" 문자열로 낸다(백엔드 `_wing_spec`과 같은 규약). + if (patch[prefix] !== undefined) wing.installed = patch[prefix] !== "없음"; + const heightM = num(`${prefix}_height_m`); + if (heightM !== undefined) wing.height_m = heightM; + const lengthM = num(`${prefix}_length_m`); + if (lengthM !== undefined) wing.length_m = lengthM; + const angleDeg = num(`${prefix}_angle_deg`); + if (angleDeg !== undefined) wing.angle_deg = angleDeg; + if (Object.keys(wing).length) control.setWing(chainageM, role, wing); + } +} + /** * BOX암거 구체 조작값 제어 — 세월교와 같은 흐름(세션 사본 + 캐시 `design.box_adjust`). * 좌·우 끝을 따로 잡으며 길이는 바깥으로만, 표고는 양방향으로 움직인다. diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 60543377..f8da0eae 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -102,6 +102,32 @@ function coversChainage(structure: StructureInstance, chainageM: number): boolea return Math.abs(structureAnchorM(structure) - chainageM) < PIPE_MATCH_M; } +/** + * 세월교 폼 옵션을 **지금 그려진 스펙**(`section.ford`)으로 덮는다 — 조정창 조작이 + * 이 스펙을 제자리에서 고치므로, 폼이 저장된 옵션을 붙들면 두 창이 갈린다 + * (2026-08-30 사용자: 수량(련) 칸이 비어 있고 조정창은 1련이었다). + */ +function withFordSpec( + options: Record | undefined, + ford: NonNullable, +): Record { + const merged: Record = { ...(options ?? {}) }; + if (ford.pipe_kind) merged.pipe_kind = ford.pipe_kind; + merged.pipe_diameter_mm = Math.round(ford.diameter_m * 1000); + merged.pipe_count = ford.pipe_count; + merged.ford_width_m = ford.span_m; + for (const [wing, prefix] of [ + [ford.wing_in, "wing_in"], + [ford.wing_out, "wing_out"], + ] as const) { + merged[prefix] = wing.installed ? "있음" : "없음"; + if (wing.height_m !== null) merged[`${prefix}_height_m`] = wing.height_m; + if (wing.length_m !== null) merged[`${prefix}_length_m`] = wing.length_m; + if (wing.angle_deg !== null) merged[`${prefix}_angle_deg`] = wing.angle_deg; + } + return merged; +} + /** * 관 지점 옵션에 **횡단 스펙 값**을 채워 넣는다 — 조정창이 보여주던 값과 폼이 * 어긋나지 않게 한다(2026-08-29 사용자). 옵션에 이미 값이 있으면 그대로 두고, @@ -117,6 +143,10 @@ function withSpecDefaults( const owner = detail?.cross_sections.find( (section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M, ); + // 세월교는 폼과 조정창이 **같은 캐시**(`section.ford`)를 본다 — 어느 쪽에서 만졌든 + // 그 스펙이 지금 그려진 값이라 저장된 옵션보다 앞선다(2026-08-30 사용자: 프론트는 + // 구조물 배치 상세 UI, 로직·값은 조정창 것). + if (owner?.ford) return withFordSpec(options, owner.ford); const culvert = owner?.culvert; if (!culvert) return options; const merged: Record = { ...(options ?? {}) }; -- 2.54.0 From e78feab5ed433832f20673d5ebead5fa0da79125 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 12:05:22 +0900 Subject: [PATCH 10/66] =?UTF-8?q?feat(B06):=20=EC=84=B8=EC=9B=94=EA=B5=90?= =?UTF-8?q?=20=EC=A2=8C=EC=B8=A1=20=ED=8F=BC=EC=9D=84=20=EB=B0=B0=EA=B4=80?= =?UTF-8?q?=20=EC=B8=A1=EC=A0=90=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=EC=96=91?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=A7=9E=EC=B6=98=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 지시 3건(2026-08-30). 1) 횡단도에서 세월교 측벽을 고르면 좌측 폼의 그 측 칸이 선다 — 배관 기슭막이가 쓰던 경로를 그대로 탄다. `SideSlots`에 `wingInlet`·`wingOutlet`을 더하고 `slotOf()`가 배관 칸 → 독립 기슭막이 칸 → 날개벽 칸 순으로 지금 서 있는 쪽을 고른다. 세월교 조정창은 `dataset.side`·`dataset.panelTitle`을 세워 어느 칸을 가리키는지 알린다. 벽 선택 뒤 `refreshAdjustSlots()`도 배관과 같게 붙였다. 2) 월류 폭·높이·수량·개략 단면 결과를 관경 바로 다음으로 옮겼다(바닥 경사도 같은 묶음에 둔다 — 물넘이포장 전용이라 세월교에서는 숨는다). 개략 단면 안내문은 읽는 글이라 10px·opacity 0.75로 줄였다. 3) 세월교·날개벽 숫자 칸을 기슭막이·집수정과 같은 [-][값][+] 묶음으로 바꿨다 (월류 폭·높이·바닥 경사·수량·날개 높이·길이·각도). `stepper()`에 표시 자릿수 인자를 더해 련·각도는 정수로 보인다("1.0련"·"45.0°" 방지). 기본값 1이라 기존 호출부는 그대로다. 자체검증(공용 브라우저 5174, 측점 7+9.7 세월교): - tsc --noEmit 통과. - 유입 측벽 선택 → `날개벽(유입) (세월교)` 칸 강조, 유출 측벽 → `날개벽(유출) (세월교)`. 한 번에 한 칸만 선다. - 폼 항목 순서 실측: 관종 > 관경 > 월류 폭 > 월류 높이 > 수량 > 날개벽(유입) … 안내문 font-size 10px, 보이는 스텝 묶음 9개. - 수량 + → 폼 "2"·조정창 "2련", 각도 + → 폼 "50"·조정창 "50°" (소수점 없음). - 회귀: 배관 측점 기슭막이 선택 → `유출구 (기슭막이(유출))` 강조 + 높이·길이· 기준측점 전/후 행 이식 정상. - 조작값은 원래대로(수량 1·각도 45) 되돌려 놓았다. Co-Authored-By: Claude Opus 5 (1M context) --- .../B05_Profile_UI_Drainage_Facility.ts | 26 +++++++++++++------ ...B05_Profile_UI_Drainage_Facility_Fields.ts | 25 +++++++++++++----- .../B05_Profile_UI_Style_Structures.css | 9 +++++++ B06_Section/B06_Section_UI_Adjust_Dock.ts | 16 +++++++++--- .../B06_Section_UI_Cross_Ford_Panel.ts | 5 ++++ .../B06_Section_UI_Page_Structures_Panel.ts | 5 ++++ 6 files changed, 68 insertions(+), 18 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 6a9c33ad..8469ecd8 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -187,6 +187,10 @@ export interface FacilityOptionsForm { /** 독립 기슭막이 좌·우 칸의 이식 자리 — 배관 유입구·유출구 자리와 같은 몫이다. */ revetInletSlot: HTMLElement; revetOutletSlot: HTMLElement; + /** 세월교·BOX암거 날개벽 칸의 이식 자리 — 세월교 유입·유출 측벽을 고르면 이 칸이 + * 선다(2026-08-30 사용자 지시 1: 배관 측점·기슭막이와 같게). */ + wingInSlot: HTMLElement; + wingOutSlot: HTMLElement; /** 유입구 "구조"에 합쳐진 B06 형식 값(2026-08-29 지시 5). B06이 조정창 값과 맞춘다. */ inletStructure: () => InletStructureKind; setInletStructure: (value: InletStructureKind) => void; @@ -356,7 +360,9 @@ export function createFacilityOptionsForm( // ── 세월교 — 구체 내 배관은 배수관과 같은 관종·관경 칸을 쓰고(2026-08-17 사용자 // 지시) 수량만 따로 받는다. const fordCount = numberInput("1", "1", "련"); - const fordRow = grid(labeled("수량 (련)", fordCount)); + // 숫자 칸은 기슭막이·집수정과 같은 [-][값][+] 묶음(2026-08-30 사용자 지시 3). + // 련은 정수라 소수 자릿수를 두지 않는다. + const fordRow = grid(labeled("수량 (련)", stepper(fordCount, 1, 0))); // ── 물넘이·세월교 개략 단면 — 월류 폭 + 월류 높이 한 행(2026-08-18 사용자 지시). // 폭 기본값은 세월교 10m·물넘이 포장 5m(사용자 확정 — 지식DB 폭 수치 근거 없음). @@ -365,14 +371,14 @@ export function createFacilityOptionsForm( const fordWidth = numberInput("0.1"); const fordHeight = numberInput("0.01"); const fordWidthRow = grid( - labeled("월류 폭 (m)", fordWidth), - labeled("월류 높이 (m)", fordHeight), + labeled("월류 폭 (m)", stepper(fordWidth, 0.1)), + labeled("월류 높이 (m)", stepper(fordHeight, 0.1)), ); // 물넘이 바닥은 유입(상류)이 높고 유출이 낮게 기운다(2026-08-28 사용자 확정). // 비우면 그 측점의 **노면 횡단경사**를 그대로 쓴다 — 횡단도가 판단한다. const fordSlope = numberInput("0.1"); fordSlope.placeholder = "노면 기울기"; - const fordSlopeRow = grid(labeled("바닥 경사 유입→유출 (%)", fordSlope)); + const fordSlopeRow = grid(labeled("바닥 경사 유입→유출 (%)", stepper(fordSlope, 0.1))); const fordSummary = document.createElement("p"); fordSummary.className = "b05-drainage__facility-note"; /** 담당 유역 설계유량(㎥/s) — 개략 단면의 입력. 유역이 없으면 null. */ @@ -423,8 +429,14 @@ export function createFacilityOptionsForm( emit(); }); + // 세월교·물넘이 항목은 **관종·관경 바로 다음**에 둔다(2026-08-30 사용자 지시 2) — + // 월류 폭·높이 → 바닥 경사 → 수량 → 개략 단면 결과. 다른 시설에서는 전부 숨는다. root.append( pipeRow, + fordWidthRow, + fordSlopeRow, + fordRow, + fordSummary, inletGroup.root, outletGroup.root, extraGroup.root, @@ -434,10 +446,6 @@ export function createFacilityOptionsForm( boxWrap, wingInFields.root, wingOutFields.root, - fordWidthRow, - fordSlopeRow, - fordRow, - fordSummary, ); let current: PipeFacility | null = null; @@ -537,6 +545,8 @@ export function createFacilityOptionsForm( extraSlot, revetInletSlot: revetInlet.slot, revetOutletSlot: revetOutlet.slot, + wingInSlot: wingInFields.slot, + wingOutSlot: wingOutFields.slot, setRevetSideLabels(labels) { revetSideLabels = labels; syncVisibility(); diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts index 803c24a8..0bead6cd 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -84,7 +84,9 @@ export function numberInput(step: string, min = "0", placeholder = ""): HTMLInpu /** 스텝 묶음 입력에 붙일 일련번호 — 라벨이 가리킬 대상을 명시하는 데 쓴다. */ let stepperSeq = 0; -export function stepper(input: HTMLInputElement, stepM: number): HTMLElement { +/** `decimals` — 표시 자릿수. 련·각도처럼 정수로 세는 칸은 0을 준다(2026-08-30 사용자: + * 세월교 상세도 같은 양식으로 맞추되 "1.0련"·"45.0°"로 보이면 안 된다). */ +export function stepper(input: HTMLInputElement, stepM: number, decimals = 1): HTMLElement { const wrap = document.createElement("div"); wrap.className = "b05-structure__stepper"; // 라벨이 이 입력을 가리키게 id를 붙인다. 없으면 + +