diff --git a/incoming/ROUND.md b/incoming/ROUND.md index 03c0c3b02..6525c13c8 100644 --- a/incoming/ROUND.md +++ b/incoming/ROUND.md @@ -151,50 +151,27 @@ await browser.close(); ## 후보 -10월 23일 포크 `dev`(`1b9a04a`)를 둘러보고 적었어요. 바퀴 66 부터 이 표에서 골라요. 창작자 지시 5 대로 1~24위는 Pattern Lab 과 Lab 에서 보드로 가는 길이에요. 처음 온 사람이 레이어를 더하고 노브를 돌려 To hardware 로 보내기까지를 390px 터치와 1440px 마우스, 키보드로 따라가며 쟀고, 미리보기와 내보낸 JS, C++ 헤더가 어긋나는 곳은 작은 스크립트로 돌려 봤어요. 25위 아래와 10, 13, 18~20, 22~24위는 10월 21일 표에서 남은 줄이에요. 그 줄의 파일 줄 번호는 오늘 것으로 고쳤고, 쪽 속도나 대비처럼 다시 재지 않은 값은 그날 잰 값이에요. 그 값을 내는 코드는 그대로예요. 바퀴 9~60 의 `## 바퀴` 줄에 적힌 후보 번호는 지난 표의 번호예요. 고칠 곳은 `web/src/` 아래면 그 앞을 줄였어요. - -| 순위 | 갈래 | 무엇 | 까닭 | 고칠 곳 | -|---|---|---|---|---| -| 1 | UI | 폰에서 Lab 아래 탭 줄을 눌러도 다른 패널이 닫히지 않고, 「⌄ 4」 메뉴가 화면 위에 열린다 | 390×844 터치로 처음 열면 아래 묶음의 탭 줄이 333px 창에 736px 예요. Code, Layers, Pixel, Text 만 보이고, Knobs 는 23px 「Kn」 조각이고, 나머지 넷은 「⌄ 4」 뒤에 있어요. 탭마다 27×27px × 가 보이는데, dockview 가 `(hover: none)` 에서 앞에 없는 탭의 × 를 다 드러내서예요(`node_modules/dockview/dist/styles/dockview.css:3048`~`:3052`). 「Kn」 을 누르면 pointerdown 에 Knobs 가 앞서며 탭 줄이 311px 굴러가고, 손가락을 떼며 오는 click 은 그 자리로 온 × 에 떨어져 Graphic Export 가 닫혀요. 돌아가려고 왼쪽 「t」 조각을 누르면 Text 가 닫혀요. Text 에서 Knobs 로 갔다 오는 데 패널 둘이 사라지고, 마우스로는 하나도 안 닫혀요. 「⌄ 4」(39×34px)를 누르면 메뉴는 DOM 의 236,438 에 136×145px 로 생기지만, 그 자리를 `elementFromPoint` 로 찍으면 뒤 탭의 × 가 나와요. dockview 의 팝업 틀은 `z-index: var(--dv-overlay-z-index)` 이고(`node_modules/dockview-core/dist/esm/dockview/components/popupService.js:41`), 내장 테마는 그 값을 999 로 두는데(`dockview.css:49` 들) Lab 테마(`app/pattern-lab/lab-dock.css:6`~`:47`)엔 없어서 `auto` 가 돼요. 테마에 `--dv-overlay-z-index: 999` 를 두고, `(hover: none)` 에서 `.dockview-theme-patternflow .dv-tab .dv-default-tab .dv-default-tab-action` 을 숨겨 페이지에 넣어 봤어요(두 클래스 셀렉터는 dockview 에 져요). 메뉴가 눌려 Knobs 가 앞섰고, 탭이 45~87px 로 줄어 Knobs 까지 들어왔고, 같은 누름에 닫힌 패널은 0개였어요. 폰에서 패널은 Panels ▾ 로 닫아요. | `app/pattern-lab/lab-dock.css`, `app/pattern-lab/labCss.test.ts` | -| 2 | UI | Lab 에서 「+ Code」 로 넣은 새 레이어가 노브를 읽어서, 넣자마자 노브를 돌리면 그림이 바뀐다 | 1440×900 에서 처음 열고 「+ Code」 를 한 번 누르면 8,192픽셀 가운데 8,079개(98.6%)가 바뀌어 밑의 패턴이 가려져요. 그 뒤 노브 넷을 하나씩 끌면 바뀌는 픽셀은 0개예요. `requestAnimationFrame` 을 감싸 시계를 세우고 합성 캔버스를 비교했어요. 새 레이어 눈을 끄면 밑의 Glitch 가 돌아와요(5,100픽셀). 시작 코드 `STARTER_CODE`(`lib/lab/store/shared.ts:17`~`:27`)는 노브를 읽지 않고 모든 칸에 `setValue` 를 해요. `addCodeLayer`(`lib/lab/store/layers.ts:21`~`:25`)는 그것을 맨 위에 불투명도 1 로 넣고, 기본 램프(`lib/lab/types.ts:39`~`:46`)는 두 끝이 다 alpha 1 이에요. 시작 코드가 `params.knobNormalized` 를 읽게 해요. K2 가 빠르기, K3 가 줄 간격 같은 식이에요. 맨 아래가 아닌 새 코드 레이어는 램프 낮은 끝을 alpha 0 으로 시작해요. Color Ramp 의 「Fade low」(`app/pattern-lab/panels/RampPanel.tsx:405`)가 하는 일이에요. 시험은 `STARTER_CODE` 가 `knobNormalized` [0,0,0,0] 과 [1,1,1,1] 에서 다른 프레임을 그리는지, 레이어 하나 위에 넣은 새 레이어의 낮은 끝이 alpha 0 인지 봐요. | `lib/lab/store/shared.ts`, `lib/lab/store/layers.ts`, 새 `lib/lab/store/starter.test.ts` | -| 3 | UI | Panels ▾ 에서 뒤에 숨은 패널을 누르면 닫지 않고 앞으로 꺼낸다 | Panels 메뉴는 열린 패널에 ● 를 달아요. 폰에선 Preview 밖의 패널 여덟이 한 묶음의 탭이라, ● 가 붙은 Knobs 도 다른 탭 뒤에 숨어 있어요. 그걸 누르면 `togglePanel`(`app/pattern-lab/PatternLabClient.tsx:314`~`:324`)이 있는 패널을 지우기만 해서 Knobs 가 사라지고, 한 번 더 눌러야 맨 끝 탭으로 다시 붙어요. 390×844 에서 Knobs 에 닿기까지 네 번 눌렀어요. 패널이 있는데 앞에 없으면 `setActive()` 하고, 이미 앞에 있을 때만 닫아요. 이 판단을 `app/pattern-lab/panels/registry.tsx` 로 옮기면 시험이 닿아요. | `app/pattern-lab/PatternLabClient.tsx`, `app/pattern-lab/panels/registry.tsx`, `app/pattern-lab/panels/registry.test.ts` | -| 4 | UI | To hardware 가 번역하지 않은 레이어를 「Header ready」 로 넘기지 않는다 | 코드 레이어가 둘이면 To hardware 는 레이어마다 따로 번역해 붙이게 해요. 「+ Code」 를 누르고 열면 「2 layers to translate」, 「0 done」 인데 「Next →」 가 눌리고, 다음 단계는 「Header ready (16.9 KB). Try it on the board first」 라고 해요. 두 칸은 빈 함수 그대로라 보드에선 검은 화면이에요(`lib/lab/hExport.ts:16`~`:17` 의 「builds (black screen)」). 「Next →」(`app/pattern-lab/HardwareModal.tsx:288`~`:303`)는 `headerLooksReal`(`:106`)만 보는데, 뼈대가 늘 `#pragma once` 로 시작해서 늘 참이에요. 「Header ready」 글(`:307`~`:312`)은 `translated`(`:102`~`:104`)를 보지 않아요. 덜 번역됐으면 그 단계에서 검게 나올 레이어를 L0, L1 처럼 이름으로 알리고, 하나도 번역하지 않았으면 「Next →」 를 막아요. 시험은 코드 레이어 둘로 Next 를 누르고 경고 글을 찾아요. | `app/pattern-lab/HardwareModal.tsx`, `app/pattern-lab/HardwareModal.test.tsx` | -| 5 | UI | To hardware 에 AI 답을 통째로 붙여도 헤더만 남고, 빈 칸에서 「Copy .h」 가 복사했다고 하지 않는다 | 레이어가 하나일 때 「Here is the converted header:」 와 ```` ```cpp ```` 울타리, 「Let me know…」 까지 든 대화 답을 그대로 붙이면 「Looks like a header ✓」 가 뜨고 「Next →」 가 열려요. 그 글이 말과 울타리째 「Copy .h」 와 「Download .h」, 보드로 보내기에 가요. 붙인 글은 `single.trim()`(`app/pattern-lab/HardwareModal.tsx:94`)으로만 다듬고, 검사(`:106`)는 `/^\s*#pragma\s+once\b/m` 라 어느 줄에 있어도 지나요. 레이어를 쌓은 쪽은 `cleanPastedUnit`(`lib/lab/hExport.ts:720`)이 울타리를 떼요. 아무것도 붙이지 않았을 때도 「Copy .h」(`HardwareModal.tsx:285`~`:287`)는 눌려서, 0바이트를 복사하고 「Copied ✓」 를 띄워요. 「Download .h」 와 「Next →」 는 그때 막혀 있어요. 레이어 하나 쪽도 울타리와 앞뒤 말을 떼고 `#pragma once` 를 맨 앞에서 찾아요. 「Copy .h」 도 `disabled={!headerLooksReal}` 로 둬요. | `app/pattern-lab/HardwareModal.tsx`, `lib/lab/hExport.ts`, `app/pattern-lab/HardwareModal.test.tsx` | -| 6 | 근간 | To hardware 가 숨긴 레이어 대신 보이는 레이어를 번역하게 한다 | 코드 레이어 「Shown」(보임)과 「Hidden」(숨김, 고른 레이어)을 두고 「Copy the conversion prompt」 를 누르면, 18,616바이트 프롬프트에 Hidden 의 코드만 들고 Shown 은 없어요. 고른 레이어가 보이되 0% 여도 같아요. `stacked`(`app/pattern-lab/HardwareModal.tsx:65`)는 숨김과 0% 를 빼고 세서 「레이어 하나」 길로 들어오는데, 번역할 `focus`(`:72`~`:74`)는 고른 코드 레이어를, 없으면 숨긴 것까지 든 첫 코드 레이어(`:62`)를 써요. 커뮤니티에 올리는 쪽은 보이는 레이어를 골라서(`app/pattern-lab/PatternLabClient.tsx:108`~`:110`), 보드엔 커뮤니티와 다른 패턴이 가요. 같은 조건으로 고르고, 이름(`HardwareModal.tsx:141`)도 같이 맞춰요. 시험은 숨긴 레이어를 고른 채 프롬프트를 복사해 보이는 레이어의 코드가 들었는지 봐요. | `app/pattern-lab/HardwareModal.tsx`, `app/pattern-lab/HardwareModal.test.tsx` | -| 7 | 근간 | Lab 이 만든 C++ 헤더가 노브 값과 이름 때문에 컴파일에 막히지 않는다 | `fmtFloat`(`lib/lab/hExport.ts:46`~`:49`)는 정수인지 먼저 보고 나서 소수 다섯째 자리로 반올림해요. 그래서 반올림해서야 정수가 되는 값은 `.0` 없이 나와요. 범위 Hue=0..360 에서 Director 키프레임(버스 값 1000 가운데 175)으로 옮긴 노브가 `62.99999999999999` 가 되면 뼈대에 `static float knobVal[4] = {63f, …}` 를 쓰고, g++ 는 `unable to find numeric literal operator 'operator""f'` 로 멈춰요. 0..100 범위에선 버스 값 70 이 이렇게 돼요. 반올림해 0 이 되는 값은 `0f` 예요. 이름은 `"` 만 `'` 로 바꾸고 `\` 는 두어서(`:236`~`:237`), 레이어를 쌓은 패턴 이름 `AC\` 는 `NAME = "AC\";` 가 되어 「missing terminating " character」 로 멈춰요. 노브 이름 `Speed\` 도 `KNOB_LABELS` 를 같은 식으로 깨요. 레이어 하나 쪽 `enforcePatternName`(`:711`)은 `\` 를 떼요. 반올림한 뒤에 정수면 `.0f` 를 붙이고, 이름과 노브 이름에서 `"` 와 `\` 를 떼요. 컴파일 시험은 `lib/lab/textExport.test.ts:558`~`:690` 의 g++ 틀을 빌려요. | `lib/lab/hExport.ts`, 새 `lib/lab/hExport.test.ts` | -| 8 | 근간 | 레이어 하나를 반투명이나 곱하기로 두어도 올린 패턴이 미리보기와 같은 밝기다 | 흰 코드 레이어 하나를 불투명도 0.5 로 두면 미리보기는 128 인데 올리는 패턴은 255 예요. 곱하기(multiply)면 미리보기 0 에 올린 쪽 255, 0.3 의 스크린이면 77 과 255 예요. 레이어를 쌓았을 때 내보내는 코드는 미리보기와 같은 값을 내요. `needsFlatten`(`lib/lab/flatten.ts:176`~`:182`)이 보이는 코드 레이어가 하나면 불투명도와 섞기를 보지 않고 false 를 돌려줘서, `buildExportCode`(`app/pattern-lab/PatternLabClient.tsx:108`~`:131`)가 그 코드를 그대로 내보내요. C++ 쪽 레이어 하나 프롬프트(`buildCppPrompt`)에도 불투명도와 섞기가 없어요. 하나 남은 레이어가 불투명도 1 밑이거나 곱하기면 `needsFlatten` 이 true 를 돌려주게 해요. To hardware 의 `stacked`(`app/pattern-lab/HardwareModal.tsx:65`)도 이 함수를 써서 보드 쪽도 같이 맞아요. | `lib/lab/flatten.ts`, `lib/lab/compositor.test.ts` | -| 9 | 근간 | `const draw = …` 로 쓴 코드 레이어를 다른 레이어와 쌓아 올려도 그 패턴이 돈다 | 미리보기는 `const draw = (display) => …` 를 오류 없이 돌려요. `lib/pattern/harness.ts:597`~`:605` 가 코드를 함수 안에 그대로 두고 `typeof` 로 꺼내서예요. 쌓아 내보내는 코드는 레이어마다 `var setup, update, draw;` 를 먼저 쓰고 그 밑에 레이어 코드를 붙여서(`lib/lab/flatten.ts:300`~`:305`), 같은 함수 안에서 「Identifier 'draw' has already been declared」 로 불러오기부터 실패해요. `const update`, `const setup`, `let`, `class` 도 같아요. 이런 레이어가 하나라도 있으면 쌓아 올린 패턴은 커뮤니티에서 돌지 않아요. `var` 그림자는 바깥 함수에 두고, 레이어 코드는 안쪽 함수에서 `typeof` 로 `{ setup, update, draw }` 를 돌려주게 감싸요. 시험은 화살표 함수 레이어를 픽셀 레이어 밑에 두고 `loadCode` 가 되는지, 픽셀이 미리보기와 같은지 봐요. | `lib/lab/flatten.ts`, `lib/lab/compositor.test.ts` | -| 10 | 근간 | Lab 에서 레이어를 하나 더 얹어도 코드 레이어가 내보낸 JS 에서 미리보기와 같은 픽셀을 찍는다 | 코드 레이어 하나면 코드를 그대로 내보내고(`lib/lab/flatten.ts:176` 의 `needsFlatten`), 둘 이상이면 각 레이어를 `flatten.ts:237` 의 `__mkDisplay` 위에서 돌려요. 이 판은 좌표를 `x \| 0` 으로 자르고(`:241`, `:249`) 색을 `Uint8ClampedArray` 에 그대로 넣어요(`:245`). 미리보기와 Live Editor 의 판은 `Math.floor` 로 자르고 `clampByte` 로 반올림해요(`lib/pattern/harness.ts:645`, `:665`, `:55`). 그래서 −1 과 0 사이 좌표의 점은 내보낸 쪽에서만 0번 줄에 찍히고, 반 값은 내보낸 쪽이 짝수로 반올림해요. 빈 픽셀 레이어 위에 코드 레이어를 얹어 128×64 를 비교했어요. (20,32) 둘레 반지름 40 원은 (42,0), (43,0) 두 픽셀이 내보낸 쪽에만 찍혔어요. `setPixel(x, y, x*2+0.5, …)` 기울기는 8,192픽셀이 다 1씩 달랐어요((0,0) 이 미리보기 1, 내보낸 쪽 0). `__mkDisplay` 를 harness 의 두 규칙대로 고치고, `lib/lab/compositor.test.ts` 의 「preview matches the flattened export」에 코드 레이어 판을 더해요. 그 시험이 쌓는 코드 레이어 `blue()`(`:200`~`:216`, #159)는 정수 좌표에 0 과 255 만 그려서 이 어긋남에 닿지 않아요. | `lib/lab/flatten.ts`, `lib/lab/compositor.test.ts` | -| 11 | 근간 | 코드의 `// @matrix` 줄을 고쳐 치는 동안 픽셀과 텍스트 레이어가 잘려 나가지 않는다 | 128×64 판에 다 칠한 픽셀 레이어(8,192칸)와 텍스트 레이어를 두고, 코드의 `// @matrix 128x64` 를 96 으로 한 자씩 고쳐 쳤어요. 편집기가 칠 때마다 `updateLayerCode` 를 불러서 `12x64` 에서 폭 12 로 잘려 768칸이 됐어요. `1x64` 는 읽지 못해 128×64 로 돌아갔고, `9x64` 에서 다시 잘려 576칸이 됐어요. `96x64` 에 닿았을 때 8,192칸 가운데 576칸만 남았어요(93% 잃음). 텍스트의 `spec.x` 는 바로 96 으로 바꿨을 때의 48 이 아니라 47 이에요. 다른 `@matrix` 가 든 코드를 붙여 넣어도 말없이 잘려요. `lib/lab/store/layers.ts:171`~`:182` 가 줄이 바뀔 때마다 크기를 바꾸고, 읽지 못한 줄은 `?? DEFAULT_MATRIX` 로 받아요. 크기를 바꾸기 전에 픽셀 되돌리기 기록도 남기지 않아요. 읽히는 줄만 받고, 바꾸기 전에 래스터 레이어를 `pixelHistory` 에 남겨요. 칠 때마다가 아니라 편집기를 떠날 때 바꿀지도 이 PR 에서 정해요. | `lib/lab/store/layers.ts`, 새 `lib/lab/store/layers.test.ts` | -| 12 | UI | To hardware 에 붙인 헤더가 창을 닫거나 쪽을 다시 불러와도 남는다 | 헤더를 붙인 뒤 Esc 나 바깥 판으로 창을 닫고 다시 열면 칸이 `""` 로 비어 있어서, AI 에게 다시 물어야 해요. 폰에선 코드 레이어 둘 가운데 L0 를 붙여 「1 done」 이 된 뒤 쪽을 다시 불러오니 두 칸이 다 비고 「0 done」 이었어요. 프로젝트는 남았어요. 창을 연 채 안드로이드 뒤로 가기를 누르면 `/` 로 가고, 앞으로 돌아오면 창이 닫혀 있어요. 붙인 글과 단계는 이 창의 `useState`(`app/pattern-lab/HardwareModal.tsx:51`~`:54`)에만 있고, 창은 닫히면 사라져요(`:146`~`:157`). 붙인 글은 레이어 코드에 묶어 sessionStorage 에 두고 창을 열 때 되살려요. 코드가 바뀌면 지워요. 시험은 `HardwareModal.test.tsx` 의 `Lab` 틀로 붙이고 Esc 하고 다시 그려서 봐요. | `app/pattern-lab/HardwareModal.tsx`, `app/pattern-lab/HardwareModal.test.tsx` | -| 13 | UI | Lab 의 창 안에서 글을 끌어 고르다 창 밖에서 놓아도 창이 닫히지 않는다 | 크로미움은 누른 곳과 뗀 곳이 다르면 click 을 두 곳의 공통 조상에 보내요. To hardware 의 `closeOnBackdrop`(`app/pattern-lab/HardwareModal.tsx:146`)은 `event.target === event.currentTarget` 만 봐요. 헤더 칸에서 끌어 바깥 판에서 떼면 공통 조상이 바깥 판이라 이 검사를 지나 창이 닫히고, 붙인 헤더는 이 창의 `useState`(`:51`, `:52`)에만 있어 같이 사라져요. 이 빌드엔 To hardware 가 없어서 같은 구조를 낱장 HTML 로 만들어 크로미움에서 끌어 봤어요. 오늘 규칙으론 1번 닫혔고, 누름도 바깥 판에서 시작했을 때만 닫는 규칙으론 0번이었어요. 바깥 판을 한 번 누르면 두 규칙 다 닫아요. 실제 Lab 에서도 Code guide 의 코드 글(`panels/CodeGuideModal.tsx:16`)과 Import image 제목(`panels/ImportImageModal.tsx:219`)을 끌어 바깥에서 떼니 창이 닫혔어요. 바깥 판의 `onPointerDown` 에서 누름이 판 자신에서 시작했는지 적어 두고, click 은 그때만 닫아요. | `app/pattern-lab/HardwareModal.tsx`, `app/pattern-lab/panels/CodeGuideModal.tsx`, `app/pattern-lab/panels/ImportImageModal.tsx`, `app/pattern-lab/HardwareModal.test.tsx` | -| 14 | UI | 텍스트 효과의 빠르기를 Text 패널에서 바로 돌린다 | 흐르기, 반짝임, 색 램프 효과는 「Speed knob」(`app/pattern-lab/panels/TextPanel.tsx:391`)으로 고른 노브 하나가 빠르기를 정해요. 그런데 Text 패널엔 그 노브를 돌리는 칸이 없고, 노브가 0 이면 「turn it up in Knobs」 라고만 해요(`:402`~`:409`). 폰에선 Knobs 가 같은 탭 묶음이라 빠르기를 바꿀 때마다 탭을 두 번 옮기고, 그동안 안내 글은 안 보여요. 오늘은 그 두 번에 패널 둘이 닫혀요(1위 줄). 「Speed knob」 고르개 옆에 `setKnob(spec.effectKnob)` 에 묶인 범위 칸을 둬요. Knobs 패널의 노브와 같은 값이라 둘이 같이 움직여요. | `app/pattern-lab/panels/TextPanel.tsx`, `app/pattern-lab/panels/TextPanel.test.tsx` | -| 15 | UI | Code 패널의 붙여넣기 상자와 안내가 영어로, 고른 레이어의 종류대로 말한다 | 클립보드 읽기를 거절하게 두고 Code 패널의 Paste 를 누르면 `window.prompt` 가 「붙여넣을 패턴 코드를 아래 상자에 입력하거나 붙여넣어 주세요:」 로 떠요(`app/pattern-lab/panels/CodePanel.tsx:102`). 쪽은 `lang="en"` 이고, Make 가이드(`components/guide/copy/lab.ts:61`)가 사람들을 이 상자로 보내요. 텍스트 레이어를 고르면 Code 패널은 「The selected layer is pixel art」(`CodePanel.tsx:47`), Pixel 패널은 「The selected layer is a code pattern」(`app/pattern-lab/panels/PixelPanel.tsx:130`)이라고 해요. 두 안내는 텍스트 레이어가 생기기(#184) 전에 쓴 글이에요. 상자 글을 영어로 쓰고, 두 안내는 `layer.type` 으로 지어요. 시험은 `readText` 를 거절시키고 `window.prompt` 를 엿보는 것과, 텍스트 레이어를 고른 두 패널의 글을 봐요. | `app/pattern-lab/panels/CodePanel.tsx`, `app/pattern-lab/panels/PixelPanel.tsx`, `app/pattern-lab/panels/CodePanel.test.tsx`, `app/pattern-lab/panels/PixelPanel.test.tsx` | -| 16 | UI | Knobs 패널에서 화살표 키 한 번이 기기의 노브 한 칸만큼 움직인다 | 노브 칸은 `step="0.001"` 인 range 예요(`app/pattern-lab/panels/KnobsPanel.tsx:286`). 1440×900 에서 화살표 오른쪽을 10번 누르니 Freq 가 10 에서 10.01 로 가서 13픽셀이 바뀌었고, Quantize 는 2 에서 2.01 로 가서 0픽셀이었어요. Freq 의 10..120 을 끝까지 가려면 110,000번이에요. PageUp 은 범위의 10% 예요. 기기는 한 바퀴가 24칸이고 범위 끝에서 끝이 두 바퀴예요. 화살표에 `knobDetentStep(ranges[i])`(`lib/pattern/controls.ts:33`)만큼 움직이는 `onKeyDown` 을 달면 끝에서 끝이 48번이에요. 마우스로 끌 때를 위해 `step` 은 그대로 둬요. 시험은 ArrowRight 가 store 의 노브를 한 칸 올리는지 봐요. | `app/pattern-lab/panels/KnobsPanel.tsx`, 새 `app/pattern-lab/panels/KnobsPanel.test.tsx` | -| 17 | UI | 폰에서 Layers 와 Knobs 의 단추가 손가락 크기다 | 390×844 터치에서 Layers 막대의 「+ Code」 가 60×28px, 「+ Pixel」 56×28, 「+ Text」 54×28, 「Dup」 41×28, 「Del」 37×28px 예요. 레이어 줄의 눈은 20×17px, 마스크 칸은 13×13px 이고, Knobs 의 「Push」 는 40×22px 예요. 막대 단추는 `app/pattern-lab/LabPanels.module.css:173`~`:187` 의 28px, 눈은 `:352`~`:362` 의 폭 20px, 노브 단추는 `app/pattern-lab/panels/KnobsPanel.module.css:173`~`:181` 의 22px 에서 와요. Text 패널은 이미 `@media (hover: none)` 에서 칸을 44px 로 키워요(`app/pattern-lab/panels/TextPanel.module.css:136`~`:147`). 같은 꼴을 두 파일에 넣어요. 노브 끌기 자체는 손가락으로 돼요(40px 끌기에 범위의 18%). | `app/pattern-lab/LabPanels.module.css`, `app/pattern-lab/panels/KnobsPanel.module.css`, `app/pattern-lab/labCss.test.ts` | -| 18 | UI | 폰에서 Pixel 패널이 그림을 1× 대신 2× 로 맞춰 연다 | 390×844 에서 Pixel 패널의 그림 칸은 372×151px 예요. Preview 가 높이의 42% 를 갖고(`app/pattern-lab/panels/registry.tsx:116`), 도구 줄 둘이 103px 와 75px, 아래 상태 줄이 51px 를 써요. 상태 줄이 51px 인 건 「alt-click picks · right-drag erases · middle-drag pans」(`panels/PixelPanel.tsx:220`)가 두 줄로 접혀서인데, 이 손짓은 터치 화면엔 없어요. 그래서 맞춤이 1× 를 골라(`panels/pixel/usePixelViewport.ts:18` 주석도 그렇게 적어요) LED 한 칸이 CSS 1px 이에요. 이 글만 `@media (hover: none)` 에서 숨기니 상태 줄이 25px, 그림 칸이 372×177px 가 되고 Fit 이 2× 를 골랐어요. 2× 는 그림 칸 높이가 168px(64×2+40) 이상이면 되니 9px 남아요. 본은 같은 Lab 의 `app/pattern-lab/panels/TextPanel.module.css:136` 에 있는 `@media (hover: none)` 이에요. | `app/pattern-lab/panels/PixelPanel.tsx`, `app/pattern-lab/LabPanels.module.css`, `app/pattern-lab/panels/pixel/usePixelViewport.ts`(주석) | -| 19 | UI | Pattern Lab Pixel 의 − + 단추가 보이는 가운데를 붙잡고 확대한다 | 휠 확대는 커서 아래 자리를 지켜요(`app/pattern-lab/panels/pixel/usePixelViewport.ts:120`, `:157`~`:170`). − + 가 부르는 `zoomBy`(`:68`)는 배율만 바꾸고 굴림 위치를 그대로 둬서, 그림이 뷰포트 왼쪽 위를 기준으로 커져요. 1440×900 에서 10× 로 키우고 가운데로 굴렸어요(가운데 칸 64, 32). + 를 한 번 누르니 가운데가 58.2, 29.1 로 6칸 밀렸고, − 두 번엔 71.2, 32 로 갔어요. 같은 자리에서 휠 한 칸은 64.1 그대로였어요. Fit 은 그림이 다 들어가 가운데에 오니 괜찮아요. Director 의 ± 가 보이는 가운데를 지키는 꼴(`panels/director/useTimelineZoom.ts:62`~`:73`)을 따라요. `zoomBy` 가 뷰포트 가운데를 `wheelAnchor` 에 넣고, 같은 레이아웃 효과가 굴림을 되돌려요. | `app/pattern-lab/panels/pixel/usePixelViewport.ts`, `app/pattern-lab/panels/PixelPanel.test.tsx` | -| 20 | UI | Lab 의 Share 창이 Esc 로 닫힌다 | Lab 머리줄의 Share 가 여는 `components/community/PublishModal.tsx`(`app/pattern-lab/PatternLabClient.tsx:547`)는 바깥 판 클릭으로만 닫히고(`:254`) 키 리스너가 없어요. 이 빌드엔 `NEXT_PUBLIC_COMMUNITY_URL` 이 없어 Share 가 보이지 않아서(`lib/community/apiBase.ts:36`) jsdom 으로 쟀어요. 창을 그리고 window 와 초점 칸에 Escape 를 보내니 `onClose` 는 0번 불렸어요. 초점은 여는 순간 제목 칸으로 가요(`:323`). Lab 의 다른 창이 쓰는 `lib/hooks/useDialog.ts:18` 꼴을 따라요. To hardware 창의 주석(`app/pattern-lab/HardwareModal.tsx:150`~`:159`)은 Publish 가 키를 듣지 않는다고 보고 첫 Esc 를 대신 받아요. Publish 가 듣게 되어도 두 리스너가 다 `setPublishCode(null)` 만 하니 To hardware 는 남아요. 그 주석과 `HardwareModal.test.tsx:216` 를 함께 고쳐요. | `components/community/PublishModal.tsx`, `components/community/PublishModal.test.tsx`, `app/pattern-lab/HardwareModal.tsx` | -| 21 | UI | 커뮤니티의 「Add firmware header」 창이 Lab 의 지금 단추 이름을 말한다 | 이 창은 헤더를 만드는 길로 「Generate the port with Copy C++ prompt in Pattern Lab」(`components/community/AddHeaderModal.tsx:154`)과 「Generate one with Copy C++ prompt in」(`:176`)을 말해요. Lab 엔 그 단추가 없어요. 지금은 머리줄 「To hardware」 창의 「Copy the conversion prompt」(`app/pattern-lab/HardwareModal.tsx:263`)예요. `web/ARCHITECTURE.md:120` 은 이미 그렇게 적어요. 아래 「창작자가 열어야 하는 것」 의 프리셋 문서 줄과 같은 옛 이름인데, 이 파일은 `web/` 안이라 바퀴가 고쳐요. | `components/community/AddHeaderModal.tsx` | -| 22 | 근간 | 범위가 0.001 보다 좁은 노브도 커뮤니티 카드에서 Lab 과 같은 값으로 돈다 | 노브 값을 0..1 로 펴는 계산이 여덟 곳에 따로 있고, 바닥값이 둘로 갈렸어요. Lab 엔진(`lib/lab/engine.ts:236`), 캡처 워커(`lib/lab/capture/capture.worker.ts:167`), C++ 내보내기(`lib/lab/hExport.ts:206`), 텍스트 효과 빠르기(`app/pattern-lab/panels/TextPanel.tsx:80`, #187)는 `Math.max(0.0001, span)` 이고, 커뮤니티 샌드박스(`sandbox/sandbox.ts:90`)와 카드(`lib/community/knobs.ts:46`, `components/community/PatternCard.tsx:378`, `:394`)는 `Math.max(0.001, span)` 이에요. #130 이 `0.0001..0.0004` 같은 범위가 올린 뒤에도 남게 고쳤는데, 그 범위 가운데인 0.00025 를 Lab 은 `knobNormalized` 0.5 로, 샌드박스는 0.15 로 패턴에 줘요. 카드의 첫 값도 어긋나요. `knobs.ts:23` 의 `midpoint` 가 소수 셋째 자리에서 잘라서 `0.0001..0.0004` 는 범위 밖인 0(샌드박스 값 −0.1)으로, `0.001..0.002` 는 맨 끝인 0.002 로 시작해요(tsx 로 `knobSetupFromCode` 를 불러 쟀어요). 이 값은 카드와 패턴 쪽, 서버 썸네일(`lib/community/thumbs.ts:93`)이 같이 써요. 바닥값 하나와 반올림 없는 가운데값을 `lib/pattern/controls.ts` 에 두고 여덟 곳이 불러요. 샌드박스를 고치면 `npm run build:sandbox` 로 `public/pattern-sandbox.html` 을 다시 만들어요(`check:sandbox-sync`). PR 이 크면 커뮤니티 쪽부터 해요. | `lib/pattern/controls.ts`, `lib/pattern/controls.test.ts`, `lib/community/knobs.ts`, `lib/lab/engine.ts`, `lib/lab/capture/capture.worker.ts`, `lib/lab/hExport.ts`, `app/pattern-lab/panels/TextPanel.tsx`, `components/community/PatternCard.tsx`, `sandbox/sandbox.ts`, `web/public/pattern-sandbox.html` | -| 23 | 준비 | Lab 에서 만든 헤더를 CC BY 4.0 으로 올려도 내려받은 `.h` 에 라이선스 줄이 하나만 있다 | 로드맵 `biz-market`(`app/roadmap/roadmap-data.ts:1099`)은 「the provenance and licence records the community already keeps」를 상업 라이선스의 토대로 삼아요. 그런데 Lab 의 C++ 뼈대는 첫 줄에 `// SPDX-License-Identifier: CC-BY-SA-4.0` 를 박아 넣어요(`lib/lab/hExport.ts:211`). To hardware 창은 이 헤더를 그대로 공유 창에 넘기고(`app/pattern-lab/HardwareModal.tsx:368`), 원작은 CC BY 4.0 을 고를 수 있어요(`lib/pattern/share.ts:25`). 내려받을 때 쓰는 `buildSharedHeaderFile`(`share.ts:196`, `lib/community/download.ts:69`)은 `#pragma once` 밑에 제 줄을 넣기만 하고 위에 있던 줄을 떼지 않아요. `buildHExport` 로 만든 헤더를 CC BY 4.0 으로 감싸 보니 1번째 줄은 `CC-BY-SA-4.0`, 18번째 줄은 `CC-BY-4.0` 이었어요. 감쌀 때 원래 있던 SPDX 줄을 떼어요. 시험은 `scripts/license-smoke.ts:120` 꼴로 하나 더해요. | `lib/pattern/share.ts`, `web/scripts/license-smoke.ts` | -| 24 | 근간 | Pattern Lab CSS 에서 아무 컴포넌트도 쓰지 않는 클래스 13개와 그 규칙 169줄이 빠진다 | `app/pattern-lab/**/*.module.css` 10개에 정의된 클래스를, 그 파일을 불러오는 `.tsx` 의 `styles.x`, `styles["x"]` 와 `composes` 에 맞춰 봤어요. 동적 접근은 없었고 13개가 어디서도 쓰이지 않아요. `app/pattern-lab/PatternLab.module.css` 가 9개(`header`, `headerMeta`, `actionRow`, `previewHeader`, `editorHeader`, `guideButton`, `pasteButton`, `clearButton`, `viewToggle`)이고, 셀렉터가 다 죽은 규칙만 137줄이에요. 주로 `:91`~`:132`, `:251`~`:359`, `@media (max-width: 980px)` 안 `:506`~`:544` 에 있고, 그 사이 산 규칙(`.stats`, `.darkButton` `:274`)은 남겨요. #183 이 `keyButton`, `genThinking`, `.emptyState` 를 이미 걷었어요. `.headerMeta, .stats, .actionRow`(`:100`)처럼 산 클래스와 묶인 셀렉터(`:109`, `:274` 도)에선 죽은 이름만 빼요. `panels/CapturePanel.module.css` 는 `stageBox`, `stageCanvas`, `stageCanvasTransparent` 가 27줄(`:110`~`:141`)이고, `panels/DirectorPanel.module.css` 는 `hint` 가 5줄(`:233`~`:237`)이에요. 나머지 7개 파일은 0개예요. | `app/pattern-lab/PatternLab.module.css`, `app/pattern-lab/panels/CapturePanel.module.css`, `app/pattern-lab/panels/DirectorPanel.module.css` | -| 25 | UI | Pattern 탭의 고급 안내와 To hardware 칸의 두 문단, Build 탭의 다른 길 안내가 제 글자 크기로 보인다 | `app/globals.css:847` 의 `.panel-body p`(20px, 900px 아래 17px `:868`)는 명시도 (0,1,1)이라 CSS 모듈 클래스 하나를 이겨요. 1440 에서 `.advancedNote`(`components/sections/PatternPanel.module.css:283`)와 `.hardwareNote` 둘(`:344`)은 14px 대신 20px, `.otherPaths`(`components/sections/BuildPanel.module.css:173`)는 15px 대신 20px 예요. 옆 카드 글은 15px 이고 칸 제목 `hardwareTitle` 은 18px 이라 안내가 제목보다 커요. `margin: 0` 도 져서 아래 여백 28px 가 붙고, To hardware 칸은 572px 로 363px 보다 209px 길어요. `InsidePanel.module.css:105` 의 `.body .mapNote`, `InsideGlobe/BuildCard.module.css:10` 의 `.card .empty` 꼴을 따라요. `BuildPanel.tsx:237` 은 `styles.buildPanel` 이 있고, `PatternPanel.tsx:159` 의 `panel-body` 엔 모듈 클래스를 하나 더해요. | `components/sections/PatternPanel.module.css`, `components/sections/PatternPanel.tsx`, `components/sections/BuildPanel.module.css` | -| 26 | UI | Make 가이드의 덱 순서 경고가 지금 펌웨어 판을 말한다 | 「Your deck」 단계는 「Up to firmware v3.10.4, Send over Wi-Fi drops this order; the next firmware keeps it」라고 해요(`components/guide/copy/community.ts:71`, 한국어 `:137`, 주석 `:23`~`:25`). 그 다음 펌웨어 3.10.5 가 10월 2일에 나와 덱 순서를 지키고(`CHANGELOG.md:113`), 지금은 3.11.0 이에요(`firmware/patternflow/net_config.h:114`). 「3.10.5 부터」처럼 판 번호로 적어요. | `components/guide/copy/community.ts` | -| 27 | UI | 가로로 든 폰에서 Build, Pattern, Inside 탭의 글이 화면의 절반 넘게 보인다 | 900px 아래에선 3D 보기(`--mb-viewer-h: 44vh`, `app/globals.css:185`, `:273`)와 탭 막대 52px(`:2794`, `:2805`)가 위에 붙어요. 844×390 에서 재 보니 둘이 224px 를 쥐어서 글은 166px(43%)만 보이고, 740×360 에선 150px 예요. Pattern 탭의 편집기는 하한 120px(`components/sections/PatternPanel.module.css:414`)라 166px 가운데 120px 를 덮어서, 그 주석(`:404`~`:411`)이 막으려던 일이 가로 화면에서 다시 생겨요. `@media (max-width: 900px) and (max-height: 500px)` 에서 `--mb-viewer-h` 를 30vh 로 주니 글이 221px(57%) 보였고, 편집기는 131px 로 따라왔어요. 세로 390×844 는 그대로예요. 변수가 vh 그대로라 `components/sections/InsideGlobe/BuildCard.tsx:38`~`:41` 의 계산도 맞아요. | `app/globals.css` | -| 28 | UI | 폰에서 로드맵 마디를 누르면 뜨는 창이 닫기 단추까지 화면 안에 든다 | 창 폭은 360px 로 박혀 있고(`app/roadmap/RoadmapMap.tsx:97`, `:704`), 오른쪽에 자리가 없으면 마디 왼쪽으로 넘기기만 해서(`:373`~`:374`) 0 아래로 내려가도 잡지 않아요. 390px 에서 맨 왼쪽 마디를 누르니 창이 x −384~−24 에 떠서 닫기 단추(−78~−45)까지 하나도 안 보였고, 가운데 마디는 −153 에서 시작해 날짜와 제목이 132px 잘렸어요. 폭을 `min(360, 캔버스 − 28)` 로, `left` 를 `margin`~`캔버스 − 폭 − margin` 사이로 잡아요. `top` 은 이미 `:375` 에서 그렇게 잡아요. 계산을 함수로 빼서 390 폭을 시험해요. `roadmap-data.ts` 는 건드리지 않아요. | `app/roadmap/RoadmapMap.tsx`, `app/roadmap/RoadmapMap.test.tsx` | -| 29 | UI | 폰에서 덱 쪽과 프로필 쪽이 긴 덱 카드 때문에 옆으로 밀리지 않는다 | 폰 규칙의 `.deckGrid { grid-template-columns: 1fr }`(`components/community/Community.module.css:5765`~`:5767`)는 `minmax(auto, 1fr)` 이라 카드가 제목 줄의 최소 폭보다 좁아지지 않아요. 제목 줄엔 제목과 「2 slots」, 비공개면 「private」 칩, 작성자가 한 줄로 서고(`components/community/DeckCard.tsx:47`~`:57`) 제목만 `nowrap` 말줄임이에요(`Community.module.css:2986`). 로그인해서 20자 제목의 비공개 덱이 있는 `/community/decks` 를 390px 로 여니 카드가 386px 라 `scrollWidth` 가 402px 였고, 360px 에서도 카드는 386px 그대로였어요. 바로 아래 `.profileGrid`(`:5769`)처럼 `minmax(0, 1fr)` 로 주입하니 카드 358px, 제목은 말줄임, `scrollWidth` 390px 이었어요. 같은 격자를 `app/community/u/[username]/page.tsx:134` 도 써요. | `components/community/Community.module.css`, `components/community/communityCss.test.ts` | -| 30 | UI | 기기 주소칸 앞 글자를 눌러도 칸에 초점이 가고, 보내기 창에서도 칸 테두리가 보이며, 폰에서 칸이 누르기 쉬운 높이가 된다 | 「Device address:」 글자와 칸이 `