Handshake Lab v2 — A·B·C단계 구현 기록
초록
구현 계획의 준비 단계 A·B·C를 실제로 수행한 과정 기록. A는 solveGrip()이 clearance>=MARGIN을 정지 조건으로 쓰는 이분 탐색이라 검사가 같은 상수를 쓰면 결과가 필연이 되는 순환을 끊기 위해 DESIGN_CLEARANCE와 VERIFY_THRESHOLD를 분리하고 inspect()에 필드를 추가만 했다. B는 박스에 선택적 회전을 주고 선분을 로컬 좌표로 옮겨 기존 AABB 루틴을 재사용하는 정확한 방식을 쓰고, 박스-박스는 SAT로 구현하되 분리량이 참거리의 하한이라는 한계를 명시했다. C는 at()가 이미 손바닥 프레임의 local→world 변환이었음을 드러내 Frame API로 올리되 연산 순서를 보존해 원본과 13자세 2,496개 스칼라가 비트 단위로 일치함을 확인했다. 검사 15종의 인벤토리와, 세 단계에서 각각 발생한 자기 정정(실패할 수 없는 대조군, 교란된 대조군, 확인하지 않은 사실 주장)을 방법론적 교훈으로 함께 기록한다.
Handshake Lab v2 — A·B·C단계 구현 기록
문서 메타데이터
- 시뮬레이션 원저작: Codexee
- 구현·검증·작성: Ari
- 작성일: 2026-09-14
- 성격: 구현 계획의 준비 단계 A·B·C를 실제로 수행한 과정 기록. 계획서가 "무엇을 왜 그 순서로" 라면, 이 문서는 "어떻게 고쳤고 무엇으로 확인했는가" 를 남긴다.
- 선행: v1 독립 재현 기록
범위: A·B·C는 모두 형상을 바꾸지 않는 준비 작업이다. 이 문서는 손·손목 재설계(D단계)를 다루지 않으며, 제작 가능성이나 자연스러움에 대해 아무것도 주장하지 않는다.
1. 작업 격리
원본을 건드리지 않는 것이 전제였다. 별도 사본에서만 작업하고, 매 단계마다 원본이 무손상인지 기계적으로 확인했다.
확인 항목은 셋이다 — 원본에 신규 심볼이 0건일 것, 변경 대상이었던 상수·함수가 원형 그대로일 것, 파일 수정시각이 불변일 것. 세 단계 모두에서 통과했다.
원본은 동시에 정답지 역할도 한다. C단계 검증이 원본 모듈을 직접 불러 결과를 대조하므로, 원본이 오염되면 검증 자체가 무의미해진다. 격리는 예의가 아니라 검증의 전제 조건이었다.
2. A단계 — 검증 기반 분리
2-1. 문제
v1 의 파지 자세는 고정값이 아니라 solver 의 출력이다.
const valid = value => { /* 해당 마디 이후를 움직여 보고 */
return changed.every(a => ps.every(b => ... || clearance(a,b) >= MARGIN)); };
if (!valid(candidate)) { for (let k=0;k<18;k++) { /* 이분법 */ } }
export const grip = solveGrip();
각 관절을 0.028 rad 씩 전진시키다 clearance >= MARGIN 을 깨는 순간 18회 이분법으로 경계 직전까지 되돌린다. 손가락이 MARGIN 에서 멈추도록 강제되므로, 검사가 같은 MARGIN 을 쓰면 "관통 0건, 최소 0.4 mm" 는 필연이다. 검증이 아니라 solver 정지 조건의 되읽기다.
2-2. 변경
상수를 둘로 쪼갰다.
export const SCALE=1.65, DESIGN_CLEARANCE=0.0004, VERIFY_THRESHOLD=0, DURATION=13;
export const MARGIN=DESIGN_CLEARANCE; // 기존 소비자 호환 별칭
solveGrip() 은 DESIGN_CLEARANCE 만, 검사는 VERIFY_THRESHOLD 만 쓴다. 후자를 0 으로 둔 것은 검증 질문을 "설계 목표를 지켰는가"가 아니라 "관통이 있는가" 로 바꾸기 위해서다.
inspect() 에는 필드를 추가만 했다.
const penetrating = min < VERIFY_THRESHOLD, depth = Math.max(0, -min);
return {min, pair, contacts, nearFingers, checked, penetrating, depth};
기존 필드를 건드리지 않은 이유가 있다. min 은 화면의 간극 표시와 상태 판정에, nearFingers 는 접촉 카운터에, 그리고 브라우저 검사가 그 카운터의 텍스트를 단언한다. 이름이나 의미를 바꾸면 가동 중인 데모와 E2E 검사가 함께 깨진다.
2-3. 짚어둘 점 — contacts 는 포위 판정이 아니다
contacts 와 nearFingers 의 조건은 d < .0012, 즉 간극 1.2 mm 이내 근접이다. "접촉점 8/8" 이 맞잡았다는 뜻으로 읽히기 쉬우나 그렇지 않다. 이 구분은 D단계에서 포위 판정을 도입할 때 결정적이다.
3. B단계 — 회전 가능한 충돌 검사
3-1. 문제
박스 거리 함수가 lo = c − h, hi = c + h 의 축별 비교만 수행한다. 방향 입력이 없다. 손바닥에 회전을 주는 순간 이 함수는 오류를 내지 않고 조용히 틀린 간극을 보고한다. 무증상이라 더 위험하다.
3-2. 변경
박스에 선택적 방향 R(행 = 로컬 축)을 허용하고, 회전이 있으면 선분을 박스 로컬 좌표로 옮겨 기존 AABB 루틴을 그대로 재사용한다.
if (box.R) {
const la = M.toLocal(box.R, box.c, a), lb = M.toLocal(box.R, box.c, b);
return segmentBoxDistance(la, lb, {type:'box', c:[0,0,0], h:box.h});
}
거리는 회전 불변이므로 이는 근사가 아니라 정확하다. 거리 계산 자체를 다시 쓰지 않아 기존 경로의 수치가 보존된다는 이점도 있다.
박스–박스는 회피할 수 없었다. 두 손바닥 쌍이 실제로 검사 대상이고, 최소 간극 쌍 분포에도 등장한다. SAT(15축)로 구현했다.
3-3. SAT 의 한계를 명시했다
SAT 가 주는 분리량은 참거리의 하한이다. 면 정렬일 때만 참거리와 같고, 모서리–모서리 근접에서는 실제보다 작게 보고할 수 있다. 겹침(음수) 판정은 정확하다.
따라서 이 값은 "이만큼은 떨어져 있다" 는 보수적 보장으로만 쓴다. 코드 주석과 이 기록 양쪽에 같은 단서를 남겼다. 보수적 방향의 오차이므로 안전 판정에는 쓸 수 있으나, 정밀 간극 보고에는 쓸 수 없다.
4. C단계 — 좌표 변환을 드러내기
4-1. 발견
v1 에는 변환 계층이 없다고 알려져 있었으나, 정확히는 암묵적으로 존재했다.
const at = p => V.add(origin, [s*p[0]*SCALE, p[1]*SCALE, s*p[2]*SCALE]);
이것은 손바닥 프레임의 local→world 변환 그 자체다 — 선형부 diag(s·SCALE, SCALE, s·SCALE) 에 평행이동 origin. 없던 것을 만든 것이 아니라 숨어 있던 것을 드러내는 작업이었다.
4-2. 변경과 수치 보존
const palmFrame = Frame.scaled(s*SCALE, SCALE, s*SCALE, origin);
const at = p => Frame.apply(palmFrame, p);
부동소수점 결과가 달라지지 않도록 연산 순서를 보존했다. s 가 ±1 이라 s*p*SCALE 과 (s*SCALE)*p 가 정확히 같고, 대각 행렬이라 0*x 항과 +0 은 오차를 만들지 않는다.
4-3. 팔은 일부러 두었다
어깨가 월드 상수이고 팔꿈치를 손목에서 역산한다. 순방향 체인으로 바꾸면 데이터 흐름이 뒤집히고 거동이 달라진다. C 의 목적은 구조를 드러내는 것이지 거동을 바꾸는 것이 아니므로 그대로 두고 코드에 표시했다. 순방향 전환은 D단계 몫이다.
5. 검사 인벤토리
15개 검사가 무엇을 지키는지 정리한다.
| # | 검사 | 지키는 것 |
|---|---|---|
| 1–2 | 캡슐/박스 거리 기본 성질 | 거리 함수의 수학적 정확성 |
| 3 | 13초 전 구간 1,561자세 | 관통 없음, 자세당 466쌍 |
| 4 | 손끝 접근·복귀 | 근접 카운터와 자세 왕복 |
| 5–6 | (기존) 손가락/손목 겹침, 과두께 손바닥 | 검사기가 실제 겹침을 잡는가 |
| 7 | 상수 분리 | 두 임계가 다름을 강제 |
| 8 | 좌표계 오류 | 뒤집힌 손을 관통으로 검출 |
| 9 | 표본 사이 관통 | 성긴 샘플링의 맹점 |
| 10 | 항등 회전 등가 | OBB 경로가 기존과 일치 (차이 2.22e-16) |
| 11 | 회전 불변성 | 함께 회전해도 거리 동일 |
| 12 | SAT 분리/겹침 | 축정렬 분리량 정확히 일치 |
| 13 | 회전 단독 관통 | 회전 처리가 실제로 동작 |
| 14 | 원본과 비트 대조 | 13자세 2,496개 스칼라 완전 일치 |
| 15 | 미러 프레임 성질 | 반사가 아니라 회전임을 확인 |
v1 기준 수치는 전 과정에서 불변이었다 — checked 466, 727,426 쌍, 최소 0.4000042815903663 mm.
6. 세 번의 자기 정정
세 단계에서 세 번, 내가 만든 검사나 주장을 되돌렸다. 결과보다 이 과정이 더 기록할 가치가 있다고 본다.
A단계 — 실패할 수 없는 대조군. "표본 사이 고속 관통" 검사를 재샘플링으로 만들었더니 성긴 값과 조밀한 값이 소수점 16자리까지 같았다. grip 이 모듈 로드 시 한 번만 풀려 폐합 구간 자세가 시간 불변이고, 최소 간극이 그 평탄 영역에 놓이기 때문이다. 재샘플링으로는 원리적으로 아무것도 잡히지 않는다. 표본 사이를 실제로 가로지르는 운동을 주입하는 방식으로 교체했다 — 인접 표본이 각각 +18.69 mm 를 보고하는 동안 사이 지점이 −6.6 mm 로 관통한다.
B단계 — 교란된 대조군. 회전 대조군에서 손바닥을 회전시키며 동시에 확대했다. 확대만 했을 때와 확대+회전이 똑같이 −0.170300 이었다. 회전이 결과에 전혀 기여하지 않았으므로 OBB 처리의 옳고 그름을 구별할 수 없는 검사였다. 크기를 고정하고 회전만 가하도록 바꾸고, 회전 전 자세가 관통이 아님도 함께 단언해 양방향으로 실패 가능하게 만들었다.
C단계 — 확인하지 않은 사실 주장. s = −1 미러를 반사라 단정하고 손방향이 뒤집힌다고 여러 번 서술했다. 행렬식을 계산하니 양수(4.492125 = 1.65³)였다. 두 축이 함께 뒤집히므로 반사가 아니라 y축 180° 회전이고 방향성은 보존된다. v1 은 계획서와 인계 문서가 공통으로 경고한 "단순 반사" 오류를 실제로는 범하지 않았다.
정리하면 두 가지를 습관으로 삼았다. 검사를 추가할 때 "통과했는가" 이전에 "이 검사가 실패할 수 있는가" 를 확인할 것. 성질을 주장하기 전에 그 값을 실제로 계산해 볼 것. 세 번 모두, 통과한 결과를 그대로 받았다면 잘못된 확신이 남았을 것이다.
7. 의도적으로 바꾸지 않은 것
- 팔의 역산 IK — 거동이 바뀌므로 D단계로
inspect()기존 필드 — 데모와 E2E 가 소비 중- 손·손목 형상 — A·B·C 의 범위 밖
- v1 원본 — 검증의 정답지
8. 한계
- 검사 이식성. 원본 대조 검사가 원본 모듈을 절대 경로로 불러온다. 이 환경 밖에서는 실행되지 않는다. 다른 담당자에게 넘길 경우 원본 스냅샷을 저장소 안에 고정해 해소해야 한다.
- SAT 정밀도. §3-3 대로 모서리 근접에서 참거리보다 작게 보고할 수 있다.
- 여전히 기하학이다. 관통 없음은 제작 가능성도, 접촉력 성립도, 자연스러움도 뜻하지 않는다. 이 셋은 각각 별개 단계의 주제다.
코드 인용과 수치는 2026-09-14 실행 결과다. 원본은 수정하지 않았으며, 모든 변경은 별도 사본에 적용했다.
