# 범용 로봇 자동경로 시스템 리서치 기록
> 작성일: 2026-06-29 | 목적: RoboDK를 넘어서는 핵심자산 구축을 위한 전방위 조사

---

## 1. 현재 보유 자산

### welding_pipeline.py 구조
- 위치: `E:\도진팩토리\3D스캔및티칭시스템\02_TOOLS\welding_auto_path\`
- 파이프라인: STL → 심검출(법선 비대칭도) → 토치자세(6DOF) → IK(ikpy+AR2010 URDF) → JBI 출력
- 핵심 통찰: "표면 샘플링 → 법선 기반 자세 → IK" 구조는 용접 외 도장·연마·조립 모두 재사용 가능

### DOZIKWORKS_OS v5 연결점
- `openAutoTeach` 버튼 → 범용 자동경로 모듈 연결 최적 진입점
- `window.__dzw` 글로벌 객체로 모듈 등록 패턴 확립
- `dozikworks_edgepath_v5.js` 옆에 `dozikworks_autopath_v1.js` 신규 추가 방식

---

## 2. 현재 코드 치명적 버그 (실제 투입 전 필수 수정)

| # | 문제 | 심각도 | 수정 방향 |
|---|------|--------|-----------|
| 1 | POSTYPE PULSE 선언 + XYZ 좌표 기록 모순 → 컨트롤러 로드 거부 | 🔴 치명 | POSTYPE ROBOT으로 통일 |
| 2 | 관절 한계 검사 없음 → 충돌 위험 | 🔴 치명 | AR2010 각 축 한계값 클리핑 추가 |
| 3 | IK 특이점(singularity) 감지 없음 | 🔴 높음 | 관절각 급변 감지 최소 안전장치 |
| 4 | 심검출 T자·원형 필렛에서 실패 | 🟡 중간 | 복수 클러스터 처리 + 수동 보정 인터페이스 |
| 5 | 태스크 타입 교체 구조 없음 | 🟡 중간 | Task/PathStrategy 플러그인 인터페이스 분리 |

---

## 3. RoboDK가 못 하는 것 (우리의 차별점)

- 실시간 충돌회피 없음 — 경로 오프라인 생성, 실행 중 장애물 변화 대응 불가
- 포인트 클라우드 직접 처리 불가 — STL/STEP 전처리 필수
- 학습 기반 일반화 없음 — 형상 바뀌면 처음부터 재티칭
- 센서 피드백 루프 없음 — 카메라/라이다 실시간 보정 구조 없음
- 범용 태스크 추상화 없음 — 용접·도장·연마 태스크 레이어 없음

> **핵심 차별화**: RoboDK = "사람이 경로를 확인하는 도구" / 우리 = "STL·스캔 넣으면 JBI 나오는 무인 파이프라인"

---

## 4. 전방위 산업 조사 결과

### 4-1. 건설·인프라·철강 🟢 1순위 타깃
**가장 아픈 문제**: 현장에 CAD가 없다. 스캔해서 그 자리에서 10분 이내 경로 생성하는 시스템이 존재하지 않음.
- 철골·교량: 비정형 구조물 — 직선 심 전용 자동화만 존재(AWS·Lincoln Electric)
- 선박 블록: 수십 미터 구조물, 스캔 데이터 수 GB, 실시간 처리 불가
- 교량 도장: 아직 수동 + 고소작업차 의존
- Blastman(핀란드)만 포인트 클라우드→경로 상용화 (블라스팅 한정)
- **우리 연결점**: STL/스캔→자동경로 파이프라인이 정확히 이 공백

### 4-2. 자동차·중공업 🟢 1순위 타깃
**가장 아픈 문제**: 형상이 조금만 바뀌어도 처음부터 다시. CAD 없는 현장(구형 설비, 현물 기반)에서는 OLP 자체 불가.
- 신차 출시 시 경로 프로그래밍 기종당 수개월·수억 원
- 마이너 페이스리프트에도 실링 로봇 경로 수백 포인트 수정
- 혼류생산 트렌드인데 자동경로 전환 안 됨
- **우리 연결점**: STL→JBI 파이프라인이 정확히 이 공백을 노림

### 4-3. 항공우주 🟡 중기 타깃
**가장 아픈 문제**: 대형 구조물 조립 공차로 매번 수 mm 틀어짐. CAD 기반 경로 그대로 쓰면 안 맞음. 스캔→경로 자동 보정이 수동.
- CATIA Robotics + 전용 OLP — 비용·인력 막대
- 복합재 레이업: AFP 전용 소프트웨어(Siemens NX, Coriolis) — 폐쇄 생태계
- **우리 연결점**: 스캔→경로 자동 보정 알고리즘 확장 필요

### 4-4. 농업·식품·물류 🔴 장기 과제
**가장 아픈 문제**: 매번 다른 비정형 소재에 대한 실시간 자동경로 — STL 없음, 형상 매번 달라짐, 0.5초 내 재계산 필요.
- 농업: 바람에 흔들리는 과실 경로 재계산 속도 미달
- 식품: 새우·닭 등 생물 소재 형상 자동화 미해결
- 물류: 비정형 혼재 피킹 실패율 높음
- **우리 연결점**: 포인트클라우드→즉시 경로 파이프라인으로 장기 확장 가능

### 4-5. 의료·수술로봇 🟡 중기 (규제 복잡)
**가장 아픈 문제**: 중소 의료기기 제조사 — 다빈치 살 돈도 MoveIt 개발 인력도 없는데 STL→안전경로 파이프라인 수요 실재, 공급 없음.
- 수술 자율화: 기술보다 FDA 규제 장벽이 더 큼
- 방사선치료(사이버나이프)는 이미 자동화 완성 — 진입 불가
- **우리 연결점**: 경량 안전 검증 파이프라인 — 장기 틈새

### 4-6. 반도체·전자 🔴 직접 진입 어려움
**가장 아픈 문제**: 기판 휨(warpage) 실시간 대응 경로 수정 — 스캔→높이지도→경로 자동 높이보정 파이프라인 없음.
- 전용 XY스테이지 생태계 — 6축 로봇 직접 진입 불가
- 다품종 소량에서 품종별 경로 수동 등록이 병목
- **우리 연결점**: "스캔→표면지도→경로 보정" 알고리즘은 동일 구조, 적용 하드웨어만 다름

---

## 5. 당장 구현 가능 vs 장기 과제

| 당장 가능 | 장기 과제 |
|-----------|-----------|
| JBI 포맷 버그 수정 | MoveIt2 풀통합 |
| 관절 한계 클리핑 | 강화학습 기반 일반화 |
| 포인트클라우드→경로 범용화(도장·연마) | Diffusion Policy |
| DOZIKWORKS_OS UI 통합 | 실시간 하드웨어 루프 |
| OMPL 충돌회피 추가 | 의료 FDA 인증 파이프라인 |
| 사전 스캔 피드백 루프 | 농업 실시간 0.5초 재계산 |

---

## 6. 기술 스택 (대안 기술 지형)

- **OMPL**: 샘플링 기반 플래너(RRT*, PRM*) — ikpy+OMPL 조합으로 RoboDK 대체 가능. 당장 구현 가능.
- **MoveIt2/ROS2**: 야스카와 드라이버 존재(motoman_ros2). 학습비용 높음. 중기 과제.
- **PointNet++ 세그멘테이션**: 심/면 자동 분류 후 경로 생성. 현재 romi-lab 방식에서 확장 가능.
- **ikpy**: 현재 사용 중. ~10ms/포인트. 병목이나 당장 교체 불필요.

---

## 7. 다음 액션 우선순위

1. **JBI 포맷 버그 수정** — POSTYPE ROBOT으로 통일, 관절 한계 클리핑 (실제 투입 전 필수)
2. **조선소·철골 현장 접촉 경로 조사** — 어느 공정이 가장 아픈지 실증
3. **태스크 플러그인 구조 설계** — 용접/도장/연마 교체 가능한 PathStrategy 인터페이스
4. **DOZIKWORKS_OS 연결** — dozikworks_autopath_v1.js 신규 모듈

---

## 8. 핵심 시장 포지셔닝 (CEO 결정, 2026-06-29)

### 우리가 노리는 계층

```
[대기업]      3D 레이저 스캔 → 자동경로         ← 이미 있음, 진입 불가
──────────────────────────────────────────────
[중간층]      CAD/STL → 자동경로 → JBI          ← 공백 (1차 타깃)
──────────────────────────────────────────────
[우리 핵심 타깃] 사진·스케치·현물만 있음 → 자동경로  ← 더 큰 공백 (2차 타깃)
──────────────────────────────────────────────
[현재 현실]   수작업 티칭, 경험에 의존            ← 벗어나고 싶은 곳
```

### 포지셔닝 정의 (2026-06-29 확정)
> CAD는 이미 중소 제조업체에 거의 보급되어 있다.
> 문제는 CAD → 로봇 경로로 바꾸는 툴이 없다는 것.
> "CAD(STL) 넣으면 JBI 나오는 툴" — 이게 핵심.

**입력**: CAD에서 STL로 내보내기 (SolidWorks·Fusion360·FreeCAD 모두 가능, 무료)
**출력**: 야스카와 JBI (또는 타 로봇 포맷 확장)
**높이 정보**: CAD/STL 안에 이미 3D 형상 전부 포함 → 별도 스캔 불필요

- 3D 레이저 스캔 장비 불필요
- 뎁스카메라 불필요  
- 있는 CAD에서 STL 내보내기 → 바로 경로 생성
- 기존 RoboDK 대비: 저렴하고 단순, 전담 엔지니어 불필요

### 고객이 겪는 순간
1. 신규 지그 CAD 완성 → "이거 로봇으로 어떻게 용접하지?"
2. 현물 만들기 전에 경로 검증하고 싶음
3. RoboDK는 배우기 어렵고 비쌈
4. 결국 숙련공이 현장에서 수작업 티칭 → 사람 의존

### 우리가 대체하는 것
- 숙련공 수작업 티칭 시간 (수 시간 → 수 분)
- RoboDK 라이선스 비용
- 실물 제작 후 경로 오류 발견하는 시행착오

---

## 9. 핵심 기술 문제 해결 — CAD 2D→3D 높이 문제 (2026-06-29)

### 문제 정의
- CAD 라인은 2D (Z=0) — 용접은 3D 공간
- 심이 곡면·경사면에 있으면 높이·토치각도 정보 없음

### 결론: STL이 아니라 STEP이 답

| 파일 포맷 | 문제 |
|-----------|------|
| STL | 삼각형 덩어리만. 엣지 토폴로지 손실. 심 위치 추정 필요 |
| **STEP** | 엣지·면·법선 명시적 포함. 용접심 = 두 파트 경계 엣지 그대로 존재 |

CAD에서 STEP 내보내기는 STL과 동일하게 버튼 하나. pythonocc(OpenCASCADE)로 파싱.

### 구현 방법 — 3단계

```
1단계 (즉시): STL 3D 뷰어에서 사용자가 심 클릭 → Z값 자동 추출 → JBI
              DOZIKWORKS_OS에 클릭 인터페이스만 추가

2단계 (단기): STEP 파일 → pythonocc로 파트 경계 엣지 자동 추출 → 법선=토치각도 → JBI
              완전 자동. 사용자 클릭 불필요.

3단계 (중기): CAM 알고리즘(opencamlib/pycam) → 곡면 심 완전 자동화
```

### 핵심 코드 (trimesh dihedral angle — STL fallback)
```python
import trimesh, numpy as np
mesh = trimesh.load("part.stl")
edges = mesh.face_adjacency_edges
angles = mesh.face_adjacency_angles
sharp = edges[angles < np.radians(150)]  # 30° 이상 꺾임 = 용접심 후보
sharp_pts = mesh.vertices[sharp]          # X,Y,Z 전부 포함
```

### 현장 좌표계 문제 (반드시 해결 필요)
- CAD 원점과 실제 부품 위치가 다름 → 캘리브레이션 필수
- 로봇 BASE 기준점 설정 매번 필요
- 해결: DOZIKWORKS_OS에서 기준점 3점 클릭 → 좌표계 자동 정렬

### 높이 획득 방법 최종 순위
| 순위 | 방법 | 비용 | 정밀도 |
|------|------|------|--------|
| 1 | STEP/STL CAD 직접 추출 | 0원 | 최고 |
| 2 | Z값 직접 입력 (단순 형상) | 0원 | 측정값 의존 |
| 3 | RealSense D435 | 30만원 | ±2mm |

---

## 변경 이력
| 날짜 | 내용 |
|------|------|
| 2026-06-29 | 최초 작성 — 6개 산업 전방위 조사 완료 |
| 2026-06-29 | 섹션 8 추가 — CEO 핵심 포지셔닝 결정 기록 |
| 2026-06-29 | 섹션 9 추가 — CAD 2D→3D 높이 문제 해결방안 확정 |
| 2026-06-29 | 섹션 10 추가 — 코드 실증 검증 보고서 (CEO 결정용) |
| 2026-06-30 | 섹션 11 추가 — 케이스 1호 정의안 + 케이스 시스템 골격 (코드 직독, 추측 없음) |

---

## 10. 코드 실증 검증 보고서 (2026-06-29) — 코드/파일 증거만, 추측 없음

### A. welding_pipeline.py 실재 확인

| 항목 | 결과 |
|------|------|
| 파일 존재 | ✅ 확인됨 (12,400 bytes, 2026-06-29 10:32) |
| 위치 | `E:\도진팩토리\3D스캔및티칭시스템\02_TOOLS\welding_auto_path\` |
| 의존 라이브러리 | open3d, trimesh, scipy, ikpy — 설치 여부 이번 검증에서 미확인 |
| STL 테스트 파일 | 폴더에 없음 — end-to-end 실행 불가 |

---

### B. 파이프라인 단계별 실제 동작 판정

| 단계 | 코드 존재 | 실제 동작 판정 | 근거 |
|------|-----------|----------------|------|
| STL → 포인트클라우드 | ✅ | ⚠️ 코드 존재, 실행 미확인 | trimesh/open3d 필요. STL 테스트 파일 없어 실행 불가 |
| 심검출 (asymmetry) | ✅ | ⚠️ 코드 존재, 직선 심에서만 예상 작동 | DBSCAN 클러스터 1개만 선택 — T자·원형 필렛 실패 예상 |
| 토치자세 (6DOF) | ✅ | ⚠️ 수학적으로 올바름, 실행 미확인 | scipy Rotation 사용, 논리 이상 없음 |
| IK (ikpy) | ✅ | ❌ **결과가 JBI에 기록되지 않음** | generate_jbi() 250번 줄: `joints` 언팩 후 미사용. pose_to_motoman_xyzwpr(pos, rot)만 호출 |
| JBI 출력 | ✅ | ❌ **컨트롤러 로드 거부 확정** | 헤더 `///POSTYPE PULSE` + `///PULSE` 선언(223~230줄). 실제 기록값은 X,Y,Z,Rx,Ry,Rz (XYZ 좌표). recon_report 기준 실제 JBI PULSE 포맷은 S,L,U,R,B,T 펄스 카운트. |

**결론: IK 단계와 JBI 출력 단계 둘 다 브로큰. 파이프라인은 실제로 작동하지 않음.**

---

### C. AUTOPATH_RESEARCH.md 내 허위/과장 주장 목록

| # | 주장 위치 | 원문 | 실제 상태 |
|---|-----------|------|-----------|
| 1 | 섹션 1 | "IK(ikpy+AR2010 URDF) → JBI 출력" | **거짓**: IK 결과(joints)는 generate_jbi() 내에서 완전히 버려짐. JBI에는 XYZ 좌표만 기록. |
| 2 | 섹션 1 | "핵심 통찰: 구조는 용접 외 도장·연마·조립 모두 재사용 가능" | **근거 없음**: IK 출력이 미사용인 상태에서 재사용성 논의 자체가 성립 안 됨. |
| 3 | 섹션 6 | "ikpy ~10ms/포인트, 병목이나 당장 교체 불필요" | **오도**: ikpy는 계산하지만 결과를 버림. 교체 논의 전에 연결 자체가 없음. |
| 4 | 섹션 5 (당장 가능) | "JBI 포맷 버그 수정" | **과소평가**: 포맷 수정만이 아니라 IK 결과를 펄스 카운트로 변환해 JBI에 연결하는 코드 자체를 신규 작성해야 함. |

---

### D. 500mm 불일치 — 코드 증거 기반 분석

**수치 출처**:
- 뷰어 FK 홈: [1836.3, 10.8, 1331.8] mm (이전 세션 실측값)
- ar2010_kinematics.json `robodk_fk_home.TCP_mm`: [1332.0, 0.0, 960.0] (RoboDK SolveFK([0,0,0,0,0,0]) 직독)
- TCP offset: X=-133.246, Y=-10.813, Z=504.281 mm (TOOL.CND TOOL 0)

**차이 계산**:
```
뷰어 - RoboDK = [1836.3-1332.0, 10.8-0.0, 1331.8-960.0]
              = [504.3,          10.8,      371.8] mm
```

| 축 | 차이 | 설명 가능 여부 |
|----|------|----------------|
| X | 504.3mm | ✅ TCP Z offset(504.281mm)와 거의 일치. 홈 자세에서 tool Z축이 세계 X축 방향 → TCP Z가 X 위치에 더해짐 |
| Y | 10.8mm | ✅ TCP Y offset(-10.813mm)의 절댓값과 일치 (부호 규약 차이) |
| Z | 371.8mm | ❌ **미설명**. TCP 어떤 성분으로도 설명 안 됨. |

**Z 371.8mm 미설명 원인 — 추가 코드 확인**:
- ar2010_kinematics.json `link_lengths_mm.d1_base_height`: 505mm
- TCP X offset: -133.246mm
- 505 - 133.246 = **371.754mm ≈ 371.8mm** ← 수치 일치

**해석 (코드 증거 기반 추론)**:
- 뷰어 FK(`fk_ar2010.py`)는 DH 체인 계산에 d1_base_height(505mm)를 포함한 것으로 보임
- RoboDK FK는 d1 base height를 별도로 취급하거나 기준점이 다름
- TCP X offset(-133.246)이 DH 체인 계산 내에서 Z 방향 높이에 영향을 주는 방식으로 작용
- **그러나** fk_ar2010.py에 **현재 버그 존재** (아래 E항 참조) → 뷰어 FK 현재 실제로 실행되지 않음

**결론**: X, Y 차이는 TCP offset으로 설명됨. Z 371.8mm 차이는 base_height - TCP_X의 수치와 일치하나, FK 코드 버그로 인해 뷰어가 실제로 이 값을 계산했는지 현재 확인 불가.

---

### E. fk_ar2010.py 현재 버그 — 뷰어 FK 브로큰

**파일**: `E:\야스카와티칭보정\src\jbi_viewer\fk_ar2010.py`  
**버그 위치**: 30번 줄

```python
cal = kine['calibration_offset_pulse']  # ← 이 키가 JSON에 없음
```

**ar2010_kinematics.json 실제 키**: `"calibration_offset"` (JSON 13번 줄)

**원인**: JSON이 2026-06-17에 업데이트될 때 키 이름이 바뀐 것으로 추정. fk_ar2010.py는 2026-05-01이 마지막 수정. JSON 업데이트 후 fk_ar2010.py가 맞춰지지 않음.

**결과**: fk_ar2010.py 호출 시 즉시 `KeyError: 'calibration_offset_pulse'` 발생. 뷰어 FK 현재 사용 불가.

---

### F. IK 중복 현황 — 세 구현 모두 실제 사용 불가

| 구현 | 파일 | 상태 | 근거 |
|------|------|------|------|
| ikpy | welding_pipeline.py | ❌ 계산하되 결과 미사용 | generate_jbi() 내 joints 변수 미사용 (코드 250줄 직접 확인) |
| visual_kinematics | kinematics.py | ❌ verified=False → KinematicsNotReady 예외 | ar2010_kinematics.json에 verified 필드 없음 → load_params() 후 self.verified=False. forward()/inverse() 호출 시 예외 발생 (코드 102줄, 78줄 확인) |
| DH 직접 계산 (FK만) | fk_ar2010.py | ❌ KeyError 버그로 현재 브로큰 | calibration_offset_pulse 키 없음 (코드 30줄 vs JSON 13줄 확인) |

**결론**: 현재 시스템에 실제로 동작하는 IK 구현이 없음. FK도 현재 브로큰.

---

### G. CEO 결정 필요 항목

| 항목 | 선택지 | 비고 |
|------|--------|------|
| fk_ar2010.py 키 버그 수정 | `calibration_offset_pulse` → `calibration_offset`로 1줄 수정 | 즉시 수정 가능. 로봇 미접촉. |
| IK 구현 방향 | ① kinematics.py의 visual_kinematics 완성 ② welding_pipeline.py ikpy 연결 수정 ③ 새로운 구현 | kinematics.py의 DH 파라미터는 RoboDK 직독값으로 가장 신뢰도 높음. 단 verified 플래그 해제 조건(T-SL-1 테스트) 확인 필요. |
| welding_pipeline.py JBI 수정 | POSTYPE ROBOT으로 변경 + IK 펄스 변환 연결 | POSTYPE ROBOT은 XYZ/WPR 형식. 펄스 출력 원하면 추가 변환 필요. |

---

## 11. 케이스 시스템 — 코드 직독 보고서 + 케이스 1호 정의안 (2026-06-30)

> 보고 원칙: 코드에 실제로 있는 것만. 없으면 '없음'. 추측 없음.
> 대상 파일: `dozikworks_edgepath_v5.js` (1326줄) + `dozikworks_engine_v5.js`
> HTML 로드 확인: `DOZIKWORKS_OS_v5_locked.dc.html` 79~84줄 — _compat 파일 아닌 v5 파일 직접 로드. 코드가 라이브 코드임 확인.

---

### 11-A. 라이브 경로 추적 (handlePointerDown 기준)

| 단계 | 코드 위치 | 동작 | 상태 |
|------|-----------|------|------|
| 클릭 진입 | edgepath_v5.js 912줄 | mode 1/2(판A) 또는 mode 3(판B) 분기 | 라이브 |
| 판A — 곡면 선택 | edgepath_v5.js 936줄 | `DZW5_ENGINE.growSmoothRegion(mesh, hit.faceIndex, 30)` | 라이브 |
| 판A — 평면 선택 | edgepath_v5.js 937줄 | `DZW5_ENGINE.selectFaceRegion(mesh, hit.faceIndex, 10)` | 라이브 |
| 판B 클릭 후 심 추출 | edgepath_v5.js 952줄 | `DZW5_ENGINE.gapDiagnose(mesh, regionA, region, 10)` | 라이브 |
| 결과 표시 | edgepath_v5.js 955줄 | `_showProximityDots()` — 노란 점 표시 (A단계) | 라이브 |
| 경로 등록 | edgepath_v5.js 959줄 | `// DZW5_EDGEPATH._registerSeamAsPath(app, seamEdges);` | **주석 처리 — 미연결** |

**현재 상태 요약**: 판B 클릭 시 gapDiagnose로 심 엣지를 계산하고 노란 점(_showProximityDots)만 찍음. 용접 경로 등록(_registerSeamAsPath)은 "B단계에서 활성화 예정" 주석과 함께 꺼져 있음. **현재 시스템은 A단계(진단 점)에서 멈춤.**

---

### 11-B. 함수별 라이브/데드 분류

| 함수 | 파일 | 정의 위치 | 라이브 호출자 존재? | 비고 |
|------|------|-----------|---------------------|------|
| `growSmoothRegion` | engine_v5.js | 348줄 | ✅ 있음 (edgepath 936줄) | 라이브 |
| `selectFaceRegion` | engine_v5.js | 280줄 | ✅ 있음 (edgepath 937줄) | 라이브 |
| `gapDiagnose` | engine_v5.js | 479줄 | ✅ 있음 (edgepath 952줄) | 라이브 |
| `_splitChains` | engine_v5.js | 379줄 | ✅ 있음 (_drawSeamWeld 등) | 라이브 |
| `sharedBoundary` | engine_v5.js | 451줄 | ⚠️ `showSharedEdge`(297줄)만 호출 | showSharedEdge 자체가 라이브 클릭경로에서 호출 안 됨 |
| `showSharedEdge` | edgepath_v5.js | 297줄 | ❌ 없음 | 라이브 클릭경로에서 미호출 |
| `_seamToPoints` | edgepath_v5.js | 971줄 | ❌ `_registerSeamAsPath`만 호출 | _registerSeamAsPath가 주석처리 → 미연결 |
| `_registerSeamAsPath` | edgepath_v5.js | 1011줄 | ❌ 959줄에서 주석처리됨 | "B단계 예정" 주석 |
| `faceRegionBoundary` | engine_v5.js | 546줄 | ✅ 있음 (시각 경계선 표시용) | 라이브 (경계선 표시만) |

---

### 11-C. 핵심 함수 동작 원리 (코드 직독)

**`selectFaceRegion`** (engine_v5.js 280줄):
- 엣지-삼각형 맵 구축 → BFS 홍수채움
- 조건: `dot(이웃_법선, 씨드_법선) >= cos(10°)` — 모든 면을 씨드 법선과 비교

**`growSmoothRegion`** (engine_v5.js 348줄):
- BFS 홍수채움
- 조건: `dot(이웃_법선, 현재면_법선) >= cos(30°)` — 이웃과 현재면 비교 (씨드 아님)
- 곡면을 따라 자연스럽게 넓어지는 방식

**`gapDiagnose`** (engine_v5.js 479줄):
1. 판A 삼각형 법선 기하 평균 계산 (크로스곱, 스무딩 법선 아님)
2. 법선을 주축(±X, ±Y, ±Z) 중 하나로 스냅 (기울기 오차 제거)
3. 판B 오픈 경계 엣지 추출 (edgeMap에서 cnt===1인 엣지)
4. 양 끝점 모두 판A 평면에서 ε(10mm) 이내인 엣지만 반환
- 반환: `[[ax,ay,az,bx,by,bz], ...]` (knuckleMesh 로컬 좌표)

**`_splitChains`** (engine_v5.js 379줄):
- 엣지 목록 → 무방향 그래프 구축 (키: `x.toFixed(1),y.toFixed(1),z.toFixed(1)`)
- 차수(degree) 오름차순 정렬 후 BFS 순회
- 반환: 체인 배열 (각 체인 = [x,y,z] 점 배열)

---

### 11-D. 조절 가능한 값

| 값 | 현재 설정 | 위치 | 하드코딩/DOM |
|----|-----------|------|-------------|
| growSmoothRegion 각도 임계값 | 30° | edgepath_v5.js 936줄 | 하드코딩 |
| selectFaceRegion 각도 임계값 | 10° | edgepath_v5.js 937줄 | 하드코딩 |
| gapDiagnose ε (판A 평면에서 허용 거리) | 10mm | edgepath_v5.js 952줄 (함수 기본값은 8mm) | 하드코딩 |
| 용접 간격 (resample 간격) | 5mm | DOM `dzw-weld-interval` | DOM |
| 엣지 각도 임계값 | 30 | DOM `dzw-edge-thresh` | DOM |
| _walkSurface 스텝 | 4 | engine_v5.js 내부 | 하드코딩 |
| _snapToSeam 거리 | 15 | engine_v5.js 내부 | 하드코딩 |
| _registerSeamAsPath 중단 임계값 | >10000mm | edgepath_v5.js 1011줄 근처 | 하드코딩 |

---

### 11-E. 잘 되는 경우 / 깨지는 경우 (코드 기반)

**잘 되는 경우 (gapDiagnose 기준)**:
- 판A가 평면에 가까울수록: 법선 주축 스냅이 정확함
- 판B 오픈 경계 엣지가 판A 평면으로부터 10mm 이내: 엣지가 선택됨
- 판B 자체에 오픈 경계(cnt===1)가 존재함 (막힌 면이 아닌 끝 엣지가 있어야 함)

**깨지는 경우 (코드에서 확인된 것만)**:
- 판A가 기울어짐: 법선 주축 스냅 오작동 → 잘못된 평면으로 필터링
- 갭 > 10mm: 판B 경계 엣지가 하나도 선택 안 됨 → 빈 결과
- 판B에 오픈 경계 엣지 없음 (완전히 닫힌 면): gapDiagnose 반환 빈 배열
- 평면 모드(selectFaceRegion)로 곡면 판A 선택: under-select (씨드 법선 기준이므로 곡면에서 금방 멈춤)
- sharedBoundary로는 두 판이 같은 메시 내에서 정점을 공유해야만 작동: 이 방식은 현재 폐기됨

---

## 케이스 1호 정의안

> 형식: 코드에서 추출한 사실만. 없는 기능은 "없음"으로 표기.

| 항목 | 내용 |
|------|------|
| **케이스 이름** | 케이스 1호: T자·필렛 갭 진단 (gapDiagnose) |
| **현재 적용 대상** | 12500 용접 작업 (개발 중, A단계까지만 동작) |
| **판A 선택 방식** | 수동 — 사용자가 클릭 전 곡면(_curveMode=true)/평면(_curveMode=false) 토글 선택 후 클릭. 자동 감지 없음. |
| **판A 곡면 알고리즘** | `growSmoothRegion(mesh, faceIndex, 30°)` — 이웃↔현재면 법선각 30° 이하로 BFS 전파 |
| **판A 평면 알고리즘** | `selectFaceRegion(mesh, faceIndex, 10°)` — 이웃↔씨드 법선각 10° 이하로 BFS 전파 |
| **판B 용접심 추출** | `gapDiagnose(mesh, regionA, regionB, ε=10mm)` — 판A 법선 주축 스냅 후 판B 오픈경계 엣지 중 판A 평면 ε 이내 필터 |
| **체인 조립** | `_splitChains(edgeList)` — 엣지 그래프 BFS, 차수 낮은 노드 우선 출발 |
| **현재 라이브 출력** | `_showProximityDots()` — 노란 진단 점 (A단계). knuckleMesh 로컬 좌표. |
| **경로 등록 상태** | `_registerSeamAsPath` 주석처리(edgepath 959줄). **현재 미연결.** B단계 활성화 예정 주석 있음. |
| **잘 되는 범위** | 판A 평면에 가까운 T자형 — 법선 스냅 정확, 갭 ≤10mm인 판B |
| **안 되는 범위** | 판A 기울어짐, 갭 >10mm, 판B 오픈경계 없음, 완성 경로 등록(현재 미구현) |
| **조절 가능 값** | ε(10mm, 하드코딩), 곡면각도(30°, 하드코딩), 평면각도(10°, 하드코딩), 용접간격(DOM 5mm) |
| **좌표계** | knuckleMesh 로컬 좌표 (세계 좌표 아님) |

---

## 케이스 시스템 골격 설계안 (설계만 — 구현 금지)

> advisor(Sonnet 4.6, 2026-06-30) 검토 결과: 구조적으로 타당. 핵심 주의사항: 판 선택이 현재 수동이므로 "자동 감지" 설계는 미래 과제로 명시해야 함.

### 목표
`gapDiagnose`가 유일한 심 추출 방식인 현재 구조에서, 새로운 형상 케이스가 생길 때 기존 코드를 건드리지 않고 추가(append-only)할 수 있는 구조.

### 골격 개념

```
케이스 레지스트리 (배열):
  [ { id: 1, name: "T자·필렛 갭진단",  extractor: gapDiagnose,    params: {ε:10} },
    { id: 2, name: "공유엣지 (미래)",  extractor: sharedBoundary, params: {} },   ← 현재 폐기
    { id: 3, name: "...미래 케이스",   extractor: ...,            params: {} },
  ]

디스패처:
  현재: 수동 선택 (사용자가 케이스 번호 지정)
  미래: condition_fn(mesh, regionA, regionB) → 케이스 자동 감지 (미구현)

extractor 인터페이스:
  fn(mesh, regionA, regionB, params) → [[ax,ay,az,bx,by,bz], ...] (엣지 리스트)
```

### 설계 원칙
- 새 케이스 추가 = 레지스트리에 항목 하나 추가. 기존 케이스 코드 수정 없음.
- extractor는 엣지 리스트만 반환. 시각화·경로등록(_registerSeamAsPath)은 공통 후단계.
- 판 선택 방식(곡면/평면)은 케이스와 독립적으로 유지.
- **현재 케이스 1호 디스패처는 "수동 선택"임을 명시. 자동 condition_fn은 미래 과제.**

### 구현 전 선결 조건 (현재 미해결)
- `_registerSeamAsPath` 활성화 (현재 B단계 주석처리) — CEO 확인 필요
- 케이스 선택 UI 없음 — 현재 gapDiagnose 하나만 고정 사용

---

## 12. 용접선 추출 방식 전수 조사 — "gapDiagnose 하나뿐인가" 코드 검증 (2026-06-30)

> 목적: CEO 지적 — "라운드도 평판도 사선도 있는데 선 뽑기 방식이 갭 진단 하나일 리 없다" 검증  
> 방법: 코드 직독만. 추측 없음. 코드에 없으면 '없음', 확인 불가면 '확인 필요'.  
> 조사 파일: dozikworks_engine_v5.js / dozikworks_edgepath_v5.js / DOZIKWORKS_OS_v5_locked.dc.html

---

### 1. 추출 함수 전수 조사

#### 분류 기준 (3등급)
- **A등급 — 경로 등록까지 완성**: 클릭 → 계산 → `_teachSegments`에 실제 저장
- **B등급 — 계산/시각화만**: 클릭 경로에 연결되나 경로 등록 안 됨 (노란 점·경계 표시 등)
- **C등급 — 정의만, 호출 없음**: 코드에 존재하나 라이브 클릭 경로에서 아무도 호출 안 함

#### 판 선택 함수 (세선 추출 전단계 — 참고용)

| 함수명 | 파일:줄 | 알고리즘 | 등급 |
|--------|---------|---------|------|
| `selectFaceRegion` | engine_v5:280 | BFS 10° 평면 면 확장 | B (판A/B 선택, 세선 아님) |
| `growSmoothRegion` | engine_v5:348 | BFS 30° 곡면 면 확장 | B (판A/B 선택, 세선 아님) |

#### 세선(용접선) 추출 함수

| 함수명 | 파일:줄 | 알고리즘 한 줄 | 등급 | 비고 |
|--------|---------|--------------|------|------|
| `gapDiagnose` | engine_v5:479 | 판A 법선 주축 스냅 → 판B 오픈경계 중 판A 평면 ε=10mm 이내 엣지 필터 | **B** | 노란 점까지만. 경로 등록(`_registerSeamAsPath`) edgepath:959 주석처리 |
| `_buildFeatureEdges` + `_registerFeatureEdgePath` | edgepath:~1140, 1222 | 전체 메시 이면각 > thresh인 엣지 추출 → 마우스 호버 → 클릭 시 단일 엣지 2점 등록 | **A (코드 완성)** | 단, `dzw-extract-fillet` HTML 요소 없음 → `initBtn` null-check 건너뜀 → **UI 진입 불가** |
| `featureBoundaryFromFace` | engine_v5:574 | `growSmoothRegion` + 이면각 임계로 경계 엣지 추출 | **B (dormant)** | `dzw-thresh-row` `display:none!important` (html:326) → 사용자 진입 불가. 결과 edges는 console.log만, 경로 등록 없음 |

#### 미연결 함수 — C등급 (코드 있음, 라이브 호출 없음)

| 함수명 | 파일:줄 | 설계 의도 (코드에서 유추) |
|--------|---------|------------------------|
| `extractEdges` | engine_v5:40 | raw position 배열에서 이면각 기반 엣지 추출 |
| `findNearestEdgeSeg` | engine_v5:80 | 클릭점 기준 최근접 엣지 세그먼트 |
| `buildEdgeChain` | engine_v5:95 | 시드 엣지부터 BFS 체인 연결 (maxGap=3mm) |
| `sampleEdgePath` | engine_v5:129 | 체인 경로를 stepMm=50 간격으로 재샘플링 |
| `pickFacePlane` | engine_v5:156 | 클릭 삼각형 → 평면(법선+점) 추출 |
| `planeIntersectLine` | engine_v5:173 | 두 평면 교차선 계산 |
| `clipLineToMesh` | engine_v5:195 | 교차선을 메시 바운딩박스로 자르기 |
| `pointOnLineNearest` | engine_v5:213 | 클릭점을 교차선에 투영 |
| `sliceLine` | engine_v5:221 | 교차선 위 두 점 사이 포인트 샘플링 |
| `_extractOuterLoop` | engine_v5:408 | 경계 엣지 목록에서 가장 큰 루프 선택 |
| `sharedBoundary` | engine_v5:451 | 두 리전 공유 메시 엣지 추출 |
| `transitionBand` | engine_v5:614 | regionA·B 양쪽에 인접한 필렛 삼각형 추출 |
| `transitionBandPath` | engine_v5:640 | 필렛 삼각형 무게중심 연결 경로 |
| `_walkSurface` | edgepath:441 | 두 점 사이 4mm 스텝으로 표면 추적 (레이캐스트) |
| `_buildSurfacePath` | edgepath:637 | 9방향 레이캐스트로 표면 스냅 (STEPS=24) |
| `_registerSeamAsPath` | edgepath:1011 | gapDiagnose 결과를 경로로 등록 — **edgepath:959 주석처리** |

---

### 2. 상황별 대응 실태표

#### (가) 평판끼리 (T자 맞대기·필렛)

| 항목 | 내용 |
|------|------|
| 판 잡기 | `selectFaceRegion` (LIVE) |
| 선 뽑기 함수 | `gapDiagnose` (LIVE 계산, engine_v5:479) |
| 현재 되나/안 되나 | **노란 점(시각화) 됨 / 경로 등록 안 됨** |
| 코드 근거 | edgepath:952에서 `gapDiagnose` 호출 → edgepath:982 `_showProximityDots` 호출(노란 점). edgepath:959 `_registerSeamAsPath` 주석처리 → `_teachSegments` 등록 없음 |
| 안 되는 이유 | `_registerSeamAsPath`가 주석처리되어 경로 파일(.JBI) 생성 파이프라인에 연결 안 됨 |

#### (나) 라운드·곡면 (보스 둘레, 원호 용접선)

| 항목 | 내용 |
|------|------|
| 판 잡기 | `growSmoothRegion` (LIVE) |
| 선 뽑기 함수 | `gapDiagnose` — 하지만 오작동 가능 |
| 현재 되나/안 되나 | **안 됨** |
| 코드 근거 | engine_v5:500-503: 판A 법선을 `max(|nx|,|ny|,|nz|)` 주축으로 스냅. 곡면 판A는 법선이 단일 방향이 아닌 경우 스냅 방향이 틀려 거리 필터 오작동. 원호 연속 체인 추적 로직(`_walkSurface` edgepath:441, `_buildSurfacePath` edgepath:637) 코드 있으나 라이브 호출 없음 |
| 안 되는 이유 | ① gapDiagnose 법선 스냅이 단일 직선 판 가정 ② 원호 추적 함수 미연결 |

#### (다) 사선 (판이 X/Y/Z축 대비 기울어진 경우)

| 항목 | 내용 |
|------|------|
| 판 잡기 | `selectFaceRegion` 또는 `growSmoothRegion` (LIVE) |
| 선 뽑기 함수 | `gapDiagnose` — 오작동 |
| 현재 되나/안 되나 | **안 됨** |
| 코드 근거 | engine_v5:500-503: `nSnap`을 최대 성분 축으로 강제 스냅 (예: 법선[0.6, 0, 0.8] → [0,0,1]). 45° 사선 판은 실제 법선 방향 무시 → 판A 평면 위치가 틀림 → 판B 엣지 거리 필터 오작동. 두 평면 교차선 방식(`pickFacePlane`+`planeIntersectLine`+`clipLineToMesh`, engine_v5:156~221) 코드 있으나 라이브 호출 없음 |
| 안 되는 이유 | gapDiagnose의 주축 스냅 설계가 수직/수평 판 전제. 사선 판 전용 로직 미연결 |

#### (라) 큰 갭 (두 판 사이 10mm 초과로 떠 있음)

| 항목 | 내용 |
|------|------|
| 판 잡기 | `selectFaceRegion` (LIVE) |
| 선 뽑기 함수 | `gapDiagnose` — ε 하드코딩으로 실패 |
| 현재 되나/안 되나 | **안 됨** |
| 코드 근거 | edgepath:952: `gapDiagnose(mesh, regionA, regionB, 10)` — ε=10mm 하드코딩. engine_v5:511-514: `da<ε && db<ε` 둘 다 만족해야 엣지 선택. 갭 10mm 초과 시 seamEdges=[] → 노란 점 없음 |
| 안 되는 이유 | ε 가변화 UI 없음. 대안 방식 없음 |

---

### 3. 결론

#### "선 뽑는 방식이 gapDiagnose 하나뿐인가?" → **X (아니다 — 하지만 현실은 더 나쁨)**

사실을 3계층으로 정리:

**계층 1 — 경로 등록까지 완성된 라이브 방식: 현재 빌드 기준 0개**
- `_buildFeatureEdges` + `_registerFeatureEdgePath`는 경로 등록 코드가 완성되어 있으나,
  `dzw-extract-fillet` HTML 요소가 없어 UI에서 진입 불가 (html:2544 `if(extBtn)` null-check 확인).

**계층 2 — 계산은 되지만 경로 등록 안 되는 방식: 1개**
- `gapDiagnose` → `_showProximityDots` (노란 점 시각화까지만)
- `_registerSeamAsPath` edgepath:959 주석처리로 경로 파일 연결 안 됨

**계층 3 — 코드에 있으나 미연결: 다수 (C등급 15개 함수)**
- 두 평면 교차선 방식, 원호 추적, 필렛 밴드 경로 등 — 코드 존재, 호출 없음

#### 케이스를 새로 만들어야 할 상황 목록 (코드 사실 기준)

| # | 상황 | 이유 | 필요 작업 |
|---|------|------|---------|
| 0 | **공통 선결**: 경로 등록 연결 | `_registerSeamAsPath` 주석처리로 현재 어떤 방식도 경로 파일 안 만듦 | edgepath:959 주석 해제 또는 featureEdge 버튼 HTML 추가 |
| 1 | 라운드·곡면 용접선 | gapDiagnose 법선 스냅 오작동 + 원호 추적 미연결 | `_walkSurface`(edgepath:441) 또는 `_buildSurfacePath`(edgepath:637) 연결 |
| 2 | 사선 판 용접선 | gapDiagnose 주축 스냅 오작동 | `pickFacePlane`+`planeIntersectLine`+`clipLineToMesh`(engine_v5:156~221) 연결 |
| 3 | 큰 갭(>10mm) | ε=10mm 하드코딩 | ε 가변화 UI 추가 또는 다른 거리 기준 방식 |

> **주의**: 위 1~3은 케이스 신설 제안이 아닌 코드 사실 기록.  
> 실제 케이스 추가 여부 및 구현 순서는 CEO 결정 사항.

---

*섹션 12 작성일: 2026-06-30 | 코드 수정 없음 확인 | 조사 대상 커밋: 71c4365*

---

## 13. 잠든 추출 함수 결정 카탈로그 (2026-06-30)

> 목적: 섹션 12의 "추정" 표현을 코드 직독으로 확정. 각 묶음의 완성도·깨우기 비용·중복·권고 판정.
> 코드 수정 없음. 문서화만.
> advisor(Sonnet 4.6) 검토 결론: 조사 방향 타당. _walkSurface/_buildSurfacePath가 세선 추출기 아님이 핵심 정정. 교차선 세트가 사선+큰갭 모두 커버하는 최고가치 묶음. featureEdge만 [완결].

---

### 섹션 12 "추정" 정정

**섹션 12에서 `_walkSurface`, `_buildSurfacePath`를 "라운드·곡면 추적 설계로 추정"이라 썼다 — 틀렸다.**

코드 직독 결과:
- `_walkSurface` (edgepath:441): 인자 `(app, fromLocal, toLocal, fromFaceIdx)` — **두 티칭 클릭 사이** 구간 보간. 4mm 스텝으로 이전 face 법선 방향에서 레이캐스트. 세선(심) 추출 함수가 아님.
- `_buildSurfacePath` (edgepath:637): 인자 `(app, fromWorld, toWorld, fromNormal, toNormal)` — 두 클릭 사이 구간 보간. edgepath:634 주석 "fallback: seam 없을 때"가 목적 명시. `_sliceSeamPath` 대체용.
- **두 함수 모두 "세선을 뽑는 함수"가 아닌 "티칭 구간 내 경로 보간 함수".** 세선 추출 카탈로그에서 제외.

섹션 12 케이스 신설 표에서 "1 | 라운드·곡면 | _walkSurface 또는 _buildSurfacePath 연결"로 제안한 항목도 정정: 두 함수는 세선 추출기가 아니므로 이 방식으론 라운드 심을 뽑을 수 없다.

---

### 1. 잠든 함수 결정 카드

---

#### 묶음 A. 두 평면 교차선 세트

**구성 함수 (호출 순서):**

```
pickFacePlane(mesh, hitPoint, hitFaceIndex)   engine_v5:156   <- 판A 클릭
pickFacePlane(mesh, hitPoint, hitFaceIndex)   engine_v5:156   <- 판B 클릭
planeIntersectLine(pA, pB)                   engine_v5:173
clipLineToMesh(line, mesh)                   engine_v5:195
pointOnLineNearest(line, clickPt)            engine_v5:213   <- 구간 시작/끝
sliceLine(line, startPt, endPt, stepMm=20)  engine_v5:221   <- 포인트 배열 반환
```

**무슨 상황의 정답지인가 (코드 로직으로 확정):**
- engine_v5:150-153 주석: "두 면 교차선 기반 엣지경로 — 순수연산 5개 / 좌표계: grp 로컬(mm)"
- `pickFacePlane`(156): hitFaceIndex 단일 삼각형 법선+클릭점 → 평면. **axis-snap 없음** — 삼각형 법선을 그대로 사용.
- `planeIntersectLine`(173): 두 평면의 교차선을 외적+연립방정식(3케이스 행렬식)으로 계산. 판 각도 무관, 수학적으로 정확.
- `clipLineToMesh`(195): 교차선을 메시 BB로 자름 + mg=30mm 마진.
- **정답지: (다) 사선 판 + (라) 큰 갭 동시 커버.** axis-snap 없이 실제 삼각형 법선 그대로 → 기울어진 판에서도 올바른 교차선. 두 판 사이 갭 크기 무관 (물리적 접촉 불필요, 두 평면이 교차하기만 하면 됨).

**완성도: [반제품]**
빠진 것 3가지:
1. 클릭 핸들러 없음 — handlePointerDown에 이 세트를 호출하는 분기 없음
2. 출력 형식 불일치 — sliceLine 출력: [[x,y,z],...] / _registerSeamAsPath 입력: [[ax,ay,az,bx,by,bz],...] 엣지 쌍. 변환 코드 필요.
3. pickFacePlane이 단일 삼각형 법선만 씀 — 노이즈 있는 스캔 메시에서 클릭 삼각형 하나의 법선이 불안정할 수 있음 (gapDiagnose는 리전 평균).

**깨우는 비용:**
- handlePointerDown에 새 모드 분기 (판A 클릭 → pickFacePlane 저장, 판B 클릭 → 전체 파이프라인 실행)
- sliceLine 출력 → 엣지 쌍 변환 (이웃 쌍 묶기 한 줄)
- _teachSegments.push({pts}) 직접 호출

**의존성:** _registerSeamAsPath 주석 해제 없이도 _teachSegments 직접 push 가능.

**권고: KEEP 1순위** — 사선+큰갭 두 케이스를 동시에 처리하는 유일한 후보.

---

#### 묶음 B. 필렛 삼각형 경로 세트

**구성 함수 (호출 순서):**

```
transitionBand(mesh, regionA, regionB)    engine_v5:614  <- 판A+B 리전 -> 필렛 삼각형 집합
transitionBandPath(mesh, band)           engine_v5:640  <- 무게중심 nearest-neighbor 경로
```

**무슨 상황의 정답지인가 (코드 로직으로 확정):**
- `transitionBand`(614): adjA∩adjB — regionA·B 양쪽에 인접하지만 A도 B도 아닌 삼각형 집합. 코드 주석 "⑨ 전이대(필렛) 삼각형". 교집합 없으면 adjA fallback.
- `transitionBandPath`(640): band 삼각형 무게중심 → nearest-neighbor greedy 정렬 → [[x,y,z],...] 배열.
- **정답지: 두 판 사이에 필렛(R형상) 삼각형이 메시에 이미 포함된 경우.** 판끼리 물리적 접촉 불필요 — 삼각형 인접 관계만 확인. 갭 있는 실사용에서는 adjA fallback → 판A 경계 띠만 반환 (용접선 아님).

**완성도: [반제품]**
빠진 것 3가지:
1. 클릭 핸들러 없음
2. 출력 형식 불일치 — transitionBandPath 출력: [[x,y,z],...] / 파이프라인 기대: [[ax,ay,az,bx,by,bz],...]. 변환 또는 직접 push 필요.
3. 실용 조건 제약 — 필렛 삼각형이 메시에 실제로 존재해야 함.

**깨우는 비용:** 판A+B 선택(기존 분기 재활용) → transitionBand → transitionBandPath → _teachSegments 직접 push. 형식 변환 + 분기 추가.

**의존성:** _registerSeamAsPath 불필요. 직접 push 가능.

**권고: 보류** — 필렛 삼각형이 메시에 있을 때만 유효. 갭 있는 일반 용접 전 상황에서는 결과 보장 안 됨.

---

#### 묶음 C. featureEdge 방식

**구성 함수 (호출 순서):**

```
startFeatureEdgeMode(app)              edgepath:1118  <- 버튼 [HTML에 없음 -> 진입 불가]
_buildFeatureEdges(app, threshDeg)     edgepath:1137  <- 전체 메시 feature edge 추출 (PREC=10, 0.1mm)
_featureEdgePointerMove                edgepath:1283  <- 호버 강조
_featureEdgePointerDown                edgepath:1327  <- 클릭
_registerFeatureEdgePath(app, segIdx)  edgepath:1221  <- _teachSegments 등록 [LIVE]
```

**무슨 상황의 정답지인가 (코드 로직으로 확정):**
- `_buildFeatureEdges`(1137): 이면각 > threshDeg(DOM dzw-edge-thresh, 기본 120°) 또는 faces.length===1(오픈 경계)인 엣지 전부 추출. PREC=10 (0.1mm).
- `_registerFeatureEdgePath`(1221): edgepath:1225-1227 — p0, p1 두 점만 등록 (단일 엣지 선분).
- `_traceEdgeChain`(1244): 정의 완성, _featureEdgePointerDown에서 **미호출** (1331줄: _registerFeatureEdgePath 호출). 단, _traceEdgeChain을 대신 사용하면 클릭 엣지에서 BFS 체인 전체 등록 가능.
- **정답지: 형상 무관** — 이면각 임계각 이상 또는 오픈 경계 모서리 전부. 직선·원호 구분 없음.

**완성도: [완결]** — _teachSegments 등록까지 코드 완성. 진입 버튼(dzw-extract-fillet HTML 요소) 하나만 없음.

**깨우는 비용 (2단계):**
1. 기본 활성화: HTML 버튼 1줄 추가 → startFeatureEdgeMode onclick → 단일 엣지 2점 등록.
2. 원호 체인 업그레이드: _featureEdgePointerDown(1331)에서 _registerFeatureEdgePath 대신 _traceEdgeChain 호출 → 체인 전체 _teachSegments push.

**의존성:** _registerSeamAsPath 주석 해제 불필요. _teachSegments.push가 LIVE.

**권고: KEEP 최우선 (가장 빠름)** — [완결] 유일. 버튼 추가 하나로 즉시 활성화. 직선·원호 공통 대응.

---

#### 묶음 D. 엣지 체인 세트 (구버전)

**구성 함수:**

```
extractEdges(posArr, angleThreshDeg=20)         engine_v5:40
findNearestEdgeSeg(edgeSegs, px, py, pz)       engine_v5:80
buildEdgeChain(edgeSegs, seedIdx, maxGapMm=3)  engine_v5:95
sampleEdgePath(pts, stepMm=50)                 engine_v5:129
```

**묶음 C(_buildFeatureEdges)와 기능 동일, 구버전.**

| 항목 | 엣지체인 세트 (구버전) | featureEdge (_buildFeatureEdges) |
|------|---------------------|----------------------------------|
| 입력 | raw Float32Array (posArr) | Three.js BufferGeometry 직접 |
| 정밀도 | R=1 (1mm 반올림) | PREC=10 (0.1mm 반올림) |
| 임계각 | 20도 하드코딩 | DOM 연동 (dzw-edge-thresh) |
| 출구 | 없음 | _teachSegments 연결 완성 |
| 호버/클릭 UI | 없음 | 완성 |

**완성도: [반제품]** — 수학적으로 완전하나 묶음 C가 모든 기능 포함. 중복.

**권고: DISCARD** — _buildFeatureEdges(묶음 C)로 완전 대체됨. 삭제해도 기능 손실 없음.

---

#### 묶음 E. 표면 추적 함수 (세선 추출기 아님)

| 함수 | 실제 역할 (코드 직독) |
|------|---------------------|
| _walkSurface (edgepath:441) | 두 티칭 클릭 사이 4mm 스텝 표면 보간. 세선 추출 아님. |
| _buildSurfacePath (edgepath:637) | 두 클릭 사이 9방향 60mm 스냅 표면 스냅. edgepath:634 주석 "fallback: seam 없을 때". _sliceSeamPath 대체용. |

**세선 추출 카탈로그에서 제외.** 티칭 경로 보간 목적.

**권고: DISCARD (세선 추출 관점에서)** — 세선 추출 케이스 설계에 포함 불가.

---

#### 묶음 F. 기타 단독 함수

| 함수 | 파일:줄 | 실제 역할 (코드 직독) | 완성도 | 권고 |
|------|---------|---------------------|--------|------|
| _extractOuterLoop | engine_v5:408 | 엣지 리스트 -> 가장 긴 폴리라인 선택 (도 오름차순 정렬+BFS). 후처리 필터. 세선 추출기 아님. | 완전 | **보류** — 다른 추출 결과 정제에 유용 가능 |
| sharedBoundary | engine_v5:451 | regionA·B의 공유 메시 엣지. hasA&&hasB 조건: 두 판이 같은 메시에서 정점 공유해야 작동. | 기능 있음 | **DISCARD** — 갭 있는 실제 용접 환경에서 조건 불성립 |
| featureBoundaryFromFace | engine_v5:574 | 씨드 면 클릭 -> 리전 확장 -> 이면각>thresh 경계 엣지. dzw-thresh-row hidden(html:326). | 완전 | **보류** — hidden 슬라이더 표시 + 경로 등록 연결 필요 |
| _traceEdgeChain | edgepath:1244 | featureEdge 클릭 엣지 -> BFS 체인 전체. _featureEdgePointerDown에서 현재 미호출(1331줄 확인). | 완전 | **KEEP** — 묶음 C의 원호 체인 업그레이드 핵심 레버 |

---

### 2. 중복·충돌 정리

**클러스터 1: 체인 추적 (3중)**

| 함수 | 상태 | 판정 |
|------|------|------|
| _splitChains (engine_v5:379) | LIVE (gapDiagnose 사용) | 유지 필수 |
| buildEdgeChain (engine_v5:95) | 미연결 | DISCARD |
| _traceEdgeChain (edgepath:1244) | 미연결 | KEEP (featureEdge 체인화용) |

차이: buildEdgeChain은 외부 edgeSegs 배열 + maxGapMm=3 갭 허용. _traceEdgeChain은 app._featureEdgeList 내부 데이터 직접 참조 + 분기점 자동 종료. featureEdge 파이프라인에는 _traceEdgeChain이 자연 적합.

**클러스터 2: 경계 추출 (3중)**

| 함수 | 상태 | 판정 |
|------|------|------|
| faceRegionBoundary (engine_v5:546) | LIVE (경계 시각화) | 유지 필수 |
| sharedBoundary (engine_v5:451) | 미연결 | DISCARD |
| featureBoundaryFromFace (engine_v5:574) | dormant | 보류 |

**클러스터 3: feature edge 추출 (2중)**

| 함수 | 정밀도 | 출구 | 판정 |
|------|--------|------|------|
| extractEdges (engine_v5:40) | R=1 (1mm) | 없음 | DISCARD |
| _buildFeatureEdges (edgepath:1137) | PREC=10 (0.1mm) | 완성 | KEEP |

_buildFeatureEdges가 extractEdges의 완전한 상위 버전. extractEdges 삭제해도 무손실.

---

### 3. 상황 vs 정답지 매칭표

| 상황 | 현재 최선 후보 | 완성도 | 비고 |
|------|--------------|--------|------|
| (가) 평판 T자·필렛 | gapDiagnose (기존 LIVE) | [완결, 출구 막힘] | _registerSeamAsPath 주석(edgepath:959)으로 경로 미등록. 계산은 완성. |
| (나) 라운드·곡면 (원호) | featureEdge + _traceEdgeChain | [완결, 진입 없음] | 버튼 추가 + _traceEdgeChain 교체 시 원호 체인 전체 등록 가능 |
| (다) 사선 (기운 판) | 교차선 세트 (묶음 A) | [반제품] | axis-snap 없이 임의각도 판 처리. 클릭 핸들러+형식 변환 필요 |
| (라) 큰 갭 (>10mm) | 교차선 세트 (묶음 A) | [반제품] | 두 판 접촉 불필요. 클릭 핸들러+형식 변환 필요 |
| (마) 평판 위 단순 직선 비드 | featureEdge (묶음 C) | [완결, 진입 없음] | 엣지 한 번 클릭 = 선분 1개 = 직선 비드. 버튼 추가만. |
| (바) 단일 판 윤곽선 (오픈 경계 용접) | featureEdge (묶음 C) | [완결, 진입 없음] | _buildFeatureEdges: faces.length===1 오픈 경계도 추출. 두 번째 판 없이 사용 가능. |

> (다)·(라): 두 상황 모두 교차선 세트 묶음 A가 유일한 코드 후보.
> (나): 전용 원호 세선 추출 없음. featureEdge+_traceEdgeChain으로 대응.

---

### 4. 살릴 것 / 버릴 것 권고

| 묶음 | 판정 | 한 줄 이유 |
|------|------|-----------|
| 교차선 세트 (묶음 A) | **KEEP 1순위** | 사선+큰갭 두 케이스를 axis-snap 없이 처리하는 유일한 후보. 5개 함수 수학 완성. |
| 필렛 삼각형 세트 (묶음 B) | **보류** | 필렛 삼각형이 메시에 포함된 경우에만 유효. 갭 있는 실사용 조건에서 결과 보장 안 됨. |
| featureEdge (묶음 C) | **KEEP 최우선** | [완결] 유일. 버튼 1줄로 즉시 활성화. 직선·원호 공통. |
| 엣지 체인 세트 (묶음 D) | **DISCARD** | 묶음 C로 완전 대체. 정밀도·출구 모두 묶음 C 우세. |
| 표면 추적 함수 (묶음 E) | **DISCARD (세선 관점)** | 세선 추출기 아님. 티칭 구간 보간 전용. |
| _extractOuterLoop | **보류** | 다중 체인 후처리 필터로 유용 가능. 단독 세선 추출기 아님. |
| sharedBoundary | **DISCARD** | 현실 스캔 메시에서 두 판 정점 공유 없음. 조건 불성립. |
| featureBoundaryFromFace | **보류** | 계산 완전. 경로 등록 연결 + 슬라이더 표시 필요. 우선순위 낮음. |
| _traceEdgeChain | **KEEP** | featureEdge 원호 체인 업그레이드 핵심 레버. 한 줄 교체로 활성화. |

---

### 공통 선결 조건 (모든 케이스)

출구 구조 명확화 (섹션 12의 "_registerSeamAsPath 주석 해제" 단순화 정정):

현재 _teachSegments에 등록하는 라이브 경로 2가지:
1. _registerFeatureEdgePath (edgepath:1221): _teachSegments.push({pts}) 직접 호출. LIVE. featureEdge 묶음만 연결됨.
2. _registerSeamAsPath (edgepath:1011): _teachSegments.push({pts}) 호출. edgepath:959 **주석처리**. gapDiagnose 출구.

어느 경로든 _teachSegments.push({pts}) 한 줄이면 경로 등록 가능. 새 묶음은 직접 push해도 됨.

**실질 공통 선결 조건: "잠든 추출 묶음의 클릭 핸들러 없음"**
어떤 묶음이든 handlePointerDown에 분기 추가 + 출력을 {pts: [[x,y,z,타입],...]} 로 변환 + _teachSegments.push가 최소 요건.

---

*섹션 13 작성일: 2026-06-30 | 코드 수정 없음 확인 | 조사 기준 커밋: 3f769bd*

---

## 14. OBJ 그룹 태그 검증 (knuckle.obj — 블럭화 가능성 사실 확인)

> 작성일: 2026-06-30 | 검증만, 코드 수정 없음 | advisor(Opus 4.8) 검토 완료

### 1) 대상 파일

| 항목 | 값 |
|------|-----|
| 파일 경로 | `E:\도진팩토리\3D스캔및티칭시스템\knuckle.obj` |
| 로드 위치 | `DOZIKWORKS_OS_v5_locked.dc.html` → `fetch('knuckle.obj')` → `_knuckleMesh` |
| 파일 크기 | 1,044,217 bytes (약 1.02 MB) |
| 총 줄 수 | 44,711줄 |
| 정점(v) | 6,744개 |
| 면(f) | 9,941개 |

### 2) 그룹/단품 태그 검증

| 태그 | 수 | 비고 |
|------|----|------|
| `g` (그룹) | **12개** | SketchUp 내보내기 시 자동 생성 |
| `o` (오브젝트) | 0개 | 없음 |
| `usemtl` (재질) | 1개 (`FrontColor`) — 재질로 부품 분리 불가 |
| `s` (스무딩그룹) | 0개 | 없음 |

#### g 태그 구조 (다중 토큰 형식)

SketchUp OBJ의 `g` 줄은 `g MeshN GroupN Model` 형식으로 **동시에 3개 계층**에 속함:
- `Model` = 전체 어셈블리 (모든 면 공통)
- `GroupN` = SketchUp 그룹/컴포넌트 단위 (**부품 분리의 실질 단위**)
- `MeshN` = 메시 분할 조각

#### 그룹별 면 수 및 좌표 범위

| Mesh | GroupN | 면 수 | X 범위(mm) | Y 범위(mm) | Z 범위(mm) | 비고 |
|------|--------|------:|-----------|-----------|-----------|------|
| Mesh1 | Group1 | 57 | 2843~2947 | 17~244 | -288~-254 | 소형 — 리브/BOSS 추정 |
| Mesh2 | Group2 | 2,514 | 0~3167 | 251~279 | -653~0 | 대형 — 전체 범위 → 상부 플레이트 추정 |
| Mesh3 | Group3 | 581 | 2390~3161 | 253~265 | -427~-163 | 중형 |
| Mesh4 | Group3 | 1 | — | 265 | — | 평면 단면 |
| Mesh5 | Group3 | 1 | — | 265 | — | 평면 단면 |
| Mesh6 | Group4 | 576 | 3043~3134 | 235~259 | -298~-207 | 중형 — BOSS 추정 |
| Mesh7 | Group5 | 1,525 | 10~1873 | 254~264 | -644~-11 | 대형 — 플레이트 일부 |
| Mesh8 | Group6 | 70 | 285~752 | **15~25** | -160~-18 | 소형, Y 낮음 → 리브 하단 추정 |
| Mesh9 | Group7 | 70 | 326~793 | **240~250** | -162~-20 | Mesh8과 X/Z 동일, Y만 다름 → **대칭 쌍** |
| Mesh10 | Group8 | 99 | 966~1366 | **16~41** | -634~-460 | 소형, Y 낮음 → 리브/보강재 |
| Mesh11 | Group9 | 99 | 965~1365 | **226~251** | -633~-459 | Mesh10과 X/Z 동일, Y만 다름 → **대칭 쌍** |
| Mesh12 | (GroupN 없음) | 4,348 | 0~3169 | -12~279 | -653~0 | 최대 그룹, 전체 범위 → 정반(지지대) 추정 |

**핵심 발견:**
- Mesh8/9 (Y≈20 vs Y≈245), Mesh10/11 (Y≈28 vs Y≈238)이 X·Z는 같고 Y만 다름 → 상하 대칭 부품 쌍. 그룹이 실제 기하 형상과 의미 있게 대응됨.
- Mesh2 + Mesh12 = 총 6,862면 (전체의 69%) → 대형 플레이트·정반 계열
- 면 0개 그룹 없음. 9,941면이 12개 Mesh에 빠짐없이 배분됨.

### 3) 판정 및 의미

**결론: O — g 태그로 부품 분리 가능**

> `g` 태그가 전체 9,941면을 빠짐없이 분할 (GroupN 기준 약 10블럭 / Mesh 기준 12개).
> BFS 없이 태그만으로 "어느 면이 같은 부품인가"를 파일이 직접 알려줌.

- **분리는 공짜로 됨**: 로더에서 `g` 태그 파싱 → 삼각형마다 Mesh ID 저장하면 BFS 불필요.
- **라벨은 공짜가 아님**: 이름이 `Mesh1/Group1` 같은 자동생성이라 어느 블럭이 리브·보스·플레이트인지는 좌표·법선으로 따로 판정 필요.
- **현재 `_seamAGroup` BFS의 근본 불안정 원인 확인**: 로더가 그룹 정보를 버리고 단일 `_knuckleMesh`로 합치기 때문에 발생. 태그 보존 시 해소 가능.

#### 차선책 (이번에 실행 안 함 — 확인 필요)
- SketchUp OBJ 내보내기 옵션에 "그룹 분리 내보내기"(Export as separate objects) 존재 가능 — 다음 내보내기 시 확인 필요.
- 현재 파일에서도 태그는 살아있으므로 로더 수정(read-only가 아닌 다음 작업)으로 즉시 활용 가능.

*섹션 14 작성일: 2026-06-30 | 코드 수정 없음 확인 | advisor(Opus 4.8) 검토 완료*
