# AI 꾸러미 — 옆모습 윤곽 돌출 + 모서리 깎기 (베벨) — Profile extrusion with bevel (ExtrudeGeometry)
> 모서리마다 반지름을 준 정확한 2D 윤곽을 두께로 돌출하고 테두리를 둥글게 깎아, 하드서피스 부품 모서리에 빛이 맺히게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i468

이 문서를 코드 도우미(Claude Code · Cursor · ChatGPT 등)에 그대로 주면 돼요. 「## 주문서」 가 할 일, 나머지는 참고 자료예요.

## 주문서

### 만들어 줘: 옆모습 윤곽 돌출 + 모서리 깎기 (베벨) — Profile extrusion with bevel (ExtrudeGeometry)

#### 1. 목표
SF 레이저 블래스터 부품을 옆모습 윤곽에서 돌출해 만들어 줘 — 모서리마다 둥근 반지름, 두께 테두리는 베벨로 깎아 빛이 맺히게. 분위기는 반짝이는 금속.

#### 2. 핵심 기술 용어
- **Profile extrusion with bevel (ExtrudeGeometry)** — 윤곽 돌출 + 모서리 깎기
- **THREE.Shape · Shape.holes** — 2D 윤곽 · 구멍
- **bevelSize · bevelThickness · bevelSegments · bevelOffset** — 깎는 너비 · 깊이 · 단계 · 안쪽으로 당기기
- **toCreasedNormals** — 각진 곳만 날카롭게, 나머지는 매끈한 법선

#### 3. 환경
- 플랫폼: three.js r186 (ES 모듈 · TypeScript, `import * as THREE from "three"`), WebGL2, 외부 라이브러리 추가 없이
- 화면: 3D · 브라우저 — PC 와 폰(가로 844×390 · 세로 390×844) 모두, 60fps 목표

#### 4. 조건
- 바깥 크기가 변하지 않게 bevelOffset = −베벨, depth = 두께 − 2 × 베벨
- 윤곽 모서리 반지름은 양쪽 변 길이의 45% 를 넘지 않게
- 앞뒤 평평한 면의 법선은 ±z 로 다시 세운다 (베벨 법선과 섞이면 큰 삼각형에 얼룩)
- 빛이 모서리를 훑도록 움직이는 빛 + 환경 반사(HDRI 또는 RoomEnvironment)
- 베벨 없음 / 있음을 화면 반씩 나눠 비교

#### 5. 완성 기준 (이게 보이면 성공)
- ① 2D 윤곽선이 그려지고 → ② 두께로 쑥 돌출되고 → ③ 베벨 없음 · 있음을 반씩 비교한다
- 베벨 있는 오른쪽은 모든 모서리에 가는 빛 선이 맺히고, 왼쪽은 칼날처럼 어둡게 끊긴다
- 「나눠 보기 위치」 슬라이더로 비교선을 옮길 수 있다
- 화면에 베벨 전후 삼각형 수가 보인다

#### 6. 진행 방식
- 핵심 코드 위주로, 설명은 짧게. 내 프로젝트에 끼워 넣기 쉬운 함수 · 클래스로 나눠 줘.
- 처음 화면에 바로 결과가 보이게, 그리고 켬/끔(또는 전/후) 비교를 할 수 있게 만들어 줘.
- 그림 · 소리 · 모델 파일이 필요하면 코드로 만든 임시 대체물로 먼저 돌아가게 하고, 진짜 파일로 바꿀 자리를 표시해 줘.
- 마지막에 「확인 방법」(무엇을 보면 성공인지)과 「조절할 값」 목록을 짧게 정리해 줘.
- 답변과 코드 주석은 한국어로 해 줘.

## 원리
- 윤곽은 [x, y, 둥글기] 점 목록 — 꺾이는 점마다 두 변을 따라 반지름만큼 물러난 곳을 2차 곡선(quadraticCurveTo)으로 잇는다 (변 길이의 45% 까지).
- 창 · 구멍은 같은 방식의 Path 를 Shape.holes 에 넣는다.
- ExtrudeGeometry 의 depth = 두께 − 2 × 베벨, bevelSize = bevelThickness = 베벨, bevelOffset = −베벨 — 깎아도 바깥 크기가 그대로다.
- toCreasedNormals(각 0.6 rad)로 깎인 곡면은 매끈, 앞뒤 평면은 법선을 정확히 ±z 로 다시 세워 얼룩을 없앤다.
- 베벨 없는 모서리는 칼날처럼 빛이 안 맺히고, 베벨 있는 모서리엔 가는 하이라이트 선이 생긴다 — 진짜 물건은 모서리가 다 조금씩 둥글다.

## 핵심 코드 — 둥근 모서리 윤곽 → 베벨 돌출 (바깥 크기 그대로)
(발췌: demos/lib/blaster.ts roundPath() · roundShape() · extrudeSide() 를 정리)
```ts
import { toCreasedNormals } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
type Pt = [number, number, number?]; // x, y, 모서리 둥글기

function roundPath(p: THREE.Path, pts: Pt[]): void {
  const n = pts.length;
  for (let i = 0; i < n; i++) {
    const a = pts[(i + n - 1) % n]!, b = pts[i]!, c = pts[(i + 1) % n]!;
    const r = b[2] ?? 0;
    if (r <= 0) { if (i === 0) p.moveTo(b[0], b[1]); else p.lineTo(b[0], b[1]); continue; }
    const l1 = Math.hypot(a[0] - b[0], a[1] - b[1]), l2 = Math.hypot(c[0] - b[0], c[1] - b[1]);
    const k1 = Math.min(r, l1 * 0.45) / l1, k2 = Math.min(r, l2 * 0.45) / l2; // 변의 45% 까지만
    const sx = b[0] + (a[0] - b[0]) * k1, sy = b[1] + (a[1] - b[1]) * k1;
    if (i === 0) p.moveTo(sx, sy); else p.lineTo(sx, sy);
    p.quadraticCurveTo(b[0], b[1], b[0] + (c[0] - b[0]) * k2, b[1] + (c[1] - b[1]) * k2);
  }
}
function roundShape(pts: Pt[], holes: Pt[][] = []): THREE.Shape {
  const s = new THREE.Shape();
  roundPath(s, pts); s.closePath();
  for (const h of holes) { const p = new THREE.Path(); roundPath(p, h); p.closePath(); s.holes.push(p); }
  return s;
}
function extrudeSide(shape: THREE.Shape, width: number, bevel: number, seg = 3, curveSeg = 6): THREE.BufferGeometry {
  const depth = Math.max(0.01, width - 2 * bevel);
  const g = new THREE.ExtrudeGeometry(shape, {
    depth, bevelEnabled: bevel > 0,
    bevelThickness: bevel, bevelSize: bevel, bevelOffset: -bevel, // 깎아도 바깥 크기 그대로
    bevelSegments: seg, curveSegments: curveSeg,
  });
  g.translate(0, 0, -depth / 2);
  const out = toCreasedNormals(g, 0.6); // 깎인 곳은 매끈, 꺾인 곳은 날카롭게
  // (견본은 여기서 앞뒤 평면 삼각형의 법선을 정확히 (0, 0, ±1) 로 다시 세운다)
  return out;
}
// 몸체: 윤곽 + 창 구멍, 두께 7.2cm, 베벨 0.35cm
const receiver = extrudeSide(roundShape(
  [[-14, 0.2, 0.4], [22, 0.2, 0.3], [22, 6.2, 0.3], [19.4, 9, 0.7], [-7.5, 9, 0.9], [-11.5, 7.6, 0.6], [-14, 5.6, 0.5]],
  [[[2.5, 3, 0.9], [11.5, 3, 0.9], [11.5, 6.2, 0.9], [2.5, 6.2, 0.9]]],
), 7.2, 0.35);
```

## 흔한 실수 · 확인 목록
- [ ] **bevelOffset 를 안 주면 베벨만큼 부품이 커져 다른 부품과 안 맞는다** — bevelOffset = −베벨, depth = 두께 − 2 × 베벨로 바깥 치수를 지킨다.
- [ ] **computeVertexNormals 만 쓰면 큰 앞면에 얼룩진 음영이 생긴다** — toCreasedNormals 로 날카로운 각을 지키고, 앞뒤 평면 삼각형의 법선을 ±z 로 다시 세운다.
- [ ] **모서리 둥글기를 크게 주면 짧은 변에서 곡선이 겹쳐 뒤집힌다** — 반지름을 양쪽 변 길이의 45% 이하로 자른다.
- [ ] **빛이 가만히 있으면 베벨 차이가 안 보인다** — 빛을 돌려 모서리를 훑게 하고, 환경 반사를 켠다.

## 완성 기준 체크리스트
- [ ] ① 2D 윤곽선이 그려지고 → ② 두께로 쑥 돌출되고 → ③ 베벨 없음 · 있음을 반씩 비교한다
- [ ] 베벨 있는 오른쪽은 모든 모서리에 가는 빛 선이 맺히고, 왼쪽은 칼날처럼 어둡게 끊긴다
- [ ] 「나눠 보기 위치」 슬라이더로 비교선을 옮길 수 있다
- [ ] 화면에 베벨 전후 삼각형 수가 보인다

## 이 기술 정보
- id: `i468` · 분류: 기계 · 구조 › 구조 · 조립 · 3D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 베벨 단계(3)와 곡선 조각(6)만큼 삼각형이 늘어난다 (견본 화면에 「베벨 없음 → 있음」 삼각형 수 표시). 한 번 짓는 모양이라 가볍다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i468
- 쓰면 좋을 때: 총 · 기계 · 로봇 판처럼 옆모습이 정확해야 하는 하드서피스 부품 / 평평한 판 부품을 치수대로 여러 장 만들 때
- 쓰지 말 때: 둥글게 돌린 부품(통 · 렌즈) — 대신 회전체(i450) / 매끈한 유기체 곡면 — 대신 서브디비전(i448)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### timed — `src/demos/demosHard.ts:1634`
```ts
function timed(id: string, f: () => Scene3D): () => Scene3D {
  return () => {
    const t0 = performance.now();
    const sc = f();
    STATS.make[id] = +(performance.now() - t0).toFixed(2);
    const r0 = sc.render!.bind(sc);
    sc.render = (r, w, h) => {
      const a = performance.now();
      r0(r, w, h);
      STATS.frame[id] = +lerp(STATS.frame[id] ?? 0, performance.now() - a, 0.05).toFixed(2);
      STATS.kitMs = +kitBuildMs().toFixed(1);
    };
    return sc;
  };
}
```

### i468 견본 항목 — `src/demos/demosHard.ts:1651`
```ts
  i468: { kind: '3d', caption: '2D 윤곽 → 두께로 돌출 → 모서리 깎기: 왼쪽 베벨 없음 · 오른쪽 베벨 있음 (모서리에 빛)', make: timed('i468', demo468) }
```

## 관련 기술
- 먼저 알면 좋은 기술: [선반 회전체 + 장식 (윤곽선 하나로)](https://ai-techstudio.web.app/ai/t/i450.md) `i450` · [깎고 붙이기 (CSG 불리언)](https://ai-techstudio.web.app/ai/t/i451.md) `i451`
- 다음에 해 볼 기술: [나사 · 볼트 · 패널 이음선](https://ai-techstudio.web.app/ai/t/i470.md) `i470` · [실사 PBR 텍스처 (무료 CC0 스캔 재질)](https://ai-techstudio.web.app/ai/t/i474.md) `i474`
- 참고 문서: [three.js 문서 — ExtrudeGeometry](https://threejs.org/docs/#api/en/geometries/ExtrudeGeometry) · [three.js 예제 — webgl_geometry_shapes](https://threejs.org/examples/#webgl_geometry_shapes)
