# AI 꾸러미 — 보이는 선 · 숨은 선 · 굵은 선 — Hidden line rendering (EdgesGeometry)
> 모서리를 뽑아 보이는 선은 실선, 가려진 선은 점선, 강조할 선은 굵은 선으로 그려 교과서 입체 그림처럼 보이게 한다.  
> 견본: https://ai-techstudio.web.app/#t/u31

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

## 주문서

### 만들어 줘: 보이는 선 · 숨은 선 · 굵은 선 — Hidden line rendering (EdgesGeometry)

#### 1. 목표
직육면체 · 사각뿔 · 삼각기둥의 모서리를 보이는 선은 실선, 숨은 선은 점선, 강조는 굵은 선으로 그려 줘. 분위기는 교과서 입체 그림.

#### 2. 핵심 기술 용어
- **Hidden line rendering (EdgesGeometry)** — 보이는 선 · 숨은 선 그리기
- **LineDashedMaterial + depthFunc GreaterDepth** — 가려진 곳에서만 그려지는 점선
- **LineSegments2 + LineMaterial (fat lines)** — 픽셀 굵기를 정할 수 있는 굵은 선
- **polygonOffset** — 면을 살짝 뒤로 밀어 선이 면에 묻히지 않게

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

#### 4. 조건
- 모서리는 EdgesGeometry 로 뽑고 실선 · 점선 · 굵은 선이 같은 모서리를 함께 쓴다
- 점선은 depthFunc: THREE.GreaterDepth + computeLineDistances() — 둘 중 하나라도 빠지면 점선이 안 보인다
- 면 재질에 polygonOffset: true, polygonOffsetFactor 1, polygonOffsetUnits 1
- LineMaterial.resolution 을 매 그리기마다 화면 크기로 맞춘다 (안 맞추면 굵기가 틀어진다)
- renderOrder 로 점선(2) · 굵은 선(3)을 면보다 뒤에 그린다

#### 5. 완성 기준 (이게 보이면 성공)
- 도형이 돌 때 앞 모서리는 진한 실선, 뒤로 숨은 모서리는 옅은 점선으로 바뀌어 보인다
- 「면만 → 보이는 선 → + 숨은 선 → + 굵은 선」 4단계가 저절로 넘어가거나 슬라이더로 고를 수 있다
- 「굵은 선 굵기」 1 ~ 10px 슬라이더로 화면 두께가 바로 바뀌고, 멀어져도 픽셀 두께는 같다
- 선이 면 위에서 깜빡이지 않는다

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

## 원리
- EdgesGeometry 는 면 사이 각이 큰 모서리만 뽑아 선분 목록을 만든다 (삼각형 대각선은 빠진다).
- 같은 선분을 두 번 그린다: 실선은 보통 깊이 검사, 점선은 depthFunc = GreaterDepth 로 「면 뒤에 있을 때만」.
- 점선은 computeLineDistances() 로 선 길이를 재야 dashSize · gapSize 가 먹는다.
- 보통 선은 1픽셀로 고정이라, 굵은 선은 LineSegments2 + LineMaterial(linewidth 픽셀) 로 판을 만들어 그린다.
- 면 재질에 polygonOffset 을 주어 면을 살짝 뒤로 밀면 선이 면에 묻혀 깜빡이지 않는다.

## 핵심 코드 — 한 모서리로 실선 · 숨은 점선 · 굵은 선 셋
(발췌: demos/demosStructure.ts u31 make() 를 정리)
```ts
import * as THREE from 'three';
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js';
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js';
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';

const faceMat = new THREE.MeshStandardMaterial({
  color: 0xcfe6ff, roughness: 0.7,
  polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1, // 면을 살짝 뒤로
});
const thin = new THREE.LineBasicMaterial({ color: 0x1d2a55 });
const dash = new THREE.LineDashedMaterial({
  color: 0x5a6aa0, dashSize: 0.07, gapSize: 0.06,
  depthFunc: THREE.GreaterDepth, // 면 뒤에 있을 때만 = 숨은 선
  transparent: true, opacity: 0.9,
});
const fat = new LineMaterial({ color: 0x1d2a55, linewidth: 3.5, worldUnits: false }); // 픽셀 굵기

const geo = new THREE.BoxGeometry(1.5, 1.1, 1.1);
const edges = new THREE.EdgesGeometry(geo);
const g = new THREE.Group();
const thinL = new THREE.LineSegments(edges, thin);
const dashL = new THREE.LineSegments(edges, dash);
dashL.computeLineDistances(); // 이게 없으면 점선이 실선으로
dashL.renderOrder = 2;
const fatL = new LineSegments2(new LineSegmentsGeometry().fromEdgesGeometry(edges), fat);
fatL.renderOrder = 3;
g.add(new THREE.Mesh(geo, faceMat), thinL, dashL, fatL);
scene.add(g);

// 그릴 때마다: fat.resolution.set(width, height); renderer.render(scene, camera);
```

## 흔한 실수 · 확인 목록
- [ ] **computeLineDistances 를 빼면 점선이 실선으로 그려진다** — 점선 재질은 선 길이 속성(lineDistance)이 있어야 한다. LineSegments 를 만든 뒤 꼭 부른다.
- [ ] **LineBasicMaterial 의 linewidth 를 키워도 굵어지지 않는다** — WebGL 은 선 굵기를 1로 고정한다. 굵은 선은 LineSegments2 + LineMaterial 로.
- [ ] **LineMaterial.resolution 을 안 맞추면 굵기가 이상하다** — 창 크기가 바뀔 때마다(견본은 매 그리기마다) resolution.set(w, h).
- [ ] **면과 선이 같은 깊이라 깜빡인다** — 면 재질에 polygonOffset 을 줘 면을 살짝 뒤로 민다.

## 완성 기준 체크리스트
- [ ] 도형이 돌 때 앞 모서리는 진한 실선, 뒤로 숨은 모서리는 옅은 점선으로 바뀌어 보인다
- [ ] 「면만 → 보이는 선 → + 숨은 선 → + 굵은 선」 4단계가 저절로 넘어가거나 슬라이더로 고를 수 있다
- [ ] 「굵은 선 굵기」 1 ~ 10px 슬라이더로 화면 두께가 바로 바뀌고, 멀어져도 픽셀 두께는 같다
- [ ] 선이 면 위에서 깜빡이지 않는다

## 이 기술 정보
- id: `u31` · 분류: 3D 모델 · 캐릭터 › 3D 모델링 · 절차 · 3D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 선분 수십 ~ 수백 개라 가볍다. 굵은 선(LineSegments2)은 선분마다 판 하나라 수천 개를 넘기면 늘어난다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u31
- 쓰면 좋을 때: 입체도형 · 쌓기나무처럼 「안 보이는 모서리」를 알려 줘야 하는 수학 화면 / 설계도 · 도면처럼 선이 주인공인 그림
- 쓰지 말 때: 곡면이 많은 모양(구 · 캐릭터) — 모서리가 거의 안 나오거나 지저분하다. 대신 외곽선(u02)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### u31 견본 항목 — `src/demos/demosStructure.ts:1283`
```ts
  u31: {
    kind: '3d',
    caption: '면만 → 보이는 선 → 숨은 선은 점선 → 굵은 선(LineSegments2) — 교과서 입체 그림처럼',
    make() {
      const scene = baseScene('#e8f4ff', '#ffffff');
      const root = new THREE.Group();
      scene.add(root);
      const shapes: THREE.BufferGeometry[] = [new THREE.BoxGeometry(1.5, 1.1, 1.1), new THREE.ConeGeometry(0.85, 1.4, 4, 1), new THREE.CylinderGeometry(0.6, 0.6, 1.3, 3)];
      const xs = [-2.1, 0, 2.1];
      const faceMat = new THREE.MeshStandardMaterial({ color: 0xcfe6ff, roughness: 0.7, polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1 });
      const thin = new THREE.LineBasicMaterial({ color: 0x1d2a55 });
      const dash = new THREE.LineDashedMaterial({ color: 0x5a6aa0, dashSize: 0.07, gapSize: 0.06, depthFunc: THREE.GreaterDepth, transparent: true, opacity: 0.9 });
      const fat = new LineMaterial({ color: 0x1d2a55, linewidth: 3.5, worldUnits: false });
      const groups: { thinL: THREE.LineSegments; dashL: THREE.LineSegments; fatL: LineSegments2 }[] = [];
      shapes.forEach((geo, i) => {
        const g = new THREE.Group();
        g.position.x = xs[i]!;
        const m = new THREE.Mesh(geo, faceMat);
        const edges = new THREE.EdgesGeometry(geo);
        const thinL = new THREE.LineSegments(edges, thin);
        const dashL = new THREE.LineSegments(edges, dash);
        dashL.computeLineDistances();
        dashL.renderOrder = 2;
        const fg = new LineSegmentsGeometry().fromEdgesGeometry(edges);
        const fatL = new LineSegments2(fg, fat);
        fatL.renderOrder = 3;
        g.add(m, thinL, dashL, fatL);
        root.add(g);
        groups.push({ thinL, dashL, fatL });
      });
      const cam = new THREE.PerspectiveCamera(30, 1, 0.1, 50);
      const hud = new Hud();
      let step = 0;
      let auto = true;
      const names = ['면만', '보이는 선', '+ 숨은 선 (점선)', '+ 굵은 선'];
      return {
        scene,
        camera: cam,
        update(t) {
          if (auto) step = Math.floor(t / 2.2) % 4;
          root.children.forEach((g, i) => {
            g.rotation.y = t * 0.45 + i;
            g.rotation.x = 0.35;
          });
          for (const G of groups) {
            G.thinL.visible = step === 1 || step === 2;
            G.dashL.visible = step >= 2;
            G.fatL.visible = step === 3;
          }
        },
        resize(w, h) {
          const tn = Math.tan((cam.fov * Math.PI) / 360);
          const d = Math.max(1.3 / tn, 3.2 / (tn * (w / h)));
          cam.position.set(0, 0.6, d);
          cam.lookAt(0, -0.05, 0);
        },
        render(r, w, h) {
          fat.resolution.set(w, h);
          r.render(scene, cam);
          hud.draw(r, w, h, `${w}x${h}${step}`, (g) => {
            const s = fsz(w, h);
            if (w < 560) {
              pill(g, w / 2, h - s * 2.3, `${step + 1}/4 ${names[step]}`, s * 0.9, 'rgba(255,110,60,0.95)', '#fff', 'center');
              return;
            }
            names.forEach((n, i) => {
              const x = s * 0.6 + i * (w - s * 1.2) / 4;
              g.globalAlpha = i === step ? 1 : 0.35;
              pill(g, x, h - s * 2.3, n, s * 0.8, i === step ? 'rgba(255,110,60,0.95)' : 'rgba(30,40,80,0.6)');
            });
            g.globalAlpha = 1;
          });
        },
        controls: [
          { type: 'toggle', label: '저절로 단계 넘기기', value: true, on: (v) => (auto = v) },
          { type: 'range', label: '단계 (0 면 ~ 3 굵은 선)', min: 0, max: 3, step: 1, value: 3, on: (v) => ((auto = false), (step = v)) },
          { type: 'range', label: '굵은 선 굵기(px)', min: 1, max: 10, step: 0.5, value: 3.5, on: (v) => (fat.linewidth = v) },
        ],
        dispose() {
          disposeScene(scene);
          fat.dispose();
          hud.dispose();
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [3점 조명 (반구 + 해 + 테두리)](https://ai-techstudio.web.app/ai/t/u14.md) `u14`
- 다음에 해 볼 기술: [단면 자르기 (스텐실 단면 · 원뿔 곡선)](https://ai-techstudio.web.app/ai/t/i35.md) `i35` · [외곽선 (뒤집은 껍데기)](https://ai-techstudio.web.app/ai/t/u02.md) `u02`
- 참고 문서: [three.js 예제 — webgl_lines_fat](https://threejs.org/examples/#webgl_lines_fat) · [three.js 문서 — EdgesGeometry](https://threejs.org/docs/#api/en/geometries/EdgesGeometry)
