# AI 꾸러미 — 산업용 로봇 팔 (회전체 관절 · 유압 실린더 · IK 집기)
> 회전체와 모서리 깎은 돌출로 두툼한 관절 몸, 두 핀 사이 실린더가 늘고 줄며, 목표점까지 각을 식으로 풀어 상자를 집어 옮김  
> 견본: https://ai-techstudio.web.app/#t/i542

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

## 주문서

### 만들어 줘: 산업용 로봇 팔 (회전체 관절 · 유압 실린더 · IK 집기)

#### 1. 목표
공장에 「산업용 로봇 팔 (회전체 관절 · 유압 실린더 · IK 집기)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 회전체와 모서리 깎은 돌출로 두툼한 관절 몸, 두 핀 사이 실린더가 늘고 줄며, 목표점까지 각을 식으로 풀어 상자를 집어 옮김

#### 2. 핵심 기술 용어
- 산업용 로봇 팔 (회전체 관절 · 유압 실린더 · IK 집기) — 회전체와 모서리 깎은 돌출로 두툼한 관절 몸, 두 핀 사이 실린더가 늘고 줄며, 목표점까지 각을 식으로 풀어 상자를 집어 옮김

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

#### 4. 조건
- 한 프레임 16ms 안 (폰 포함) — 무거운 계산은 처음에 한 번 또는 여러 프레임에 나눠서
- 새 재질 · 셰이더는 첫 화면 전에 미리 컴파일 (renderer.compileAsync)
- 화면 가장자리 흐림 · 비네트로 얼버무리지 않기
- 외부 라이브러리 추가 없이 three.js 와 three/examples 만

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「회전체와 모서리 깎은 돌출로 두툼한 관절 몸, 두 핀 사이 실린더가 늘고 줄며, 목표점까지 각을 식으로 풀어 상자를 집어 옮김」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 회전체와 모서리 깎은 돌출로 두툼한 관절 몸, 두 핀 사이 실린더가 늘고 줄며, 목표점까지 각을 식으로 풀어 상자를 집어 옮김


## 흔한 실수 · 확인 목록
- (아직 정리된 것이 없어요)

## 완성 기준 체크리스트
- [ ] 견본처럼 「회전체와 모서리 깎은 돌출로 두툼한 관절 몸, 두 핀 사이 실린더가 늘고 줄며, 목표점까지 각을 식으로 풀어 상자를 집어 옮김」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i542` · 분류: 기계 · 구조 › 기계 장치 · 3D · 난이도 어려움 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i542
- 쓰면 좋을 때: 쓰면 좋을 곳 — 공장 · 자동화 퍼즐 · 좌표와 각도 체험

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeArm — `src/demos/demosMechArm.ts:162`
```ts
function makeArm(): Scene3D {
  const st = makeStage({ size: 3.2 });
  const { scene, cam } = st;
  const M = mechMats();
  const texs: THREE.Texture[] = [];
  const extraMats: THREE.Material[] = [];
  const T = <X extends THREE.Texture>(t: X): X => {
    texs.push(t);
    return t;
  };
  const E = <X extends THREE.Material>(m: X): X => {
    extraMats.push(m);
    return m;
  };
  const root = new THREE.Group();
  scene.add(root);
  const mesh = (g: THREE.BufferGeometry, m: THREE.Material, parent: THREE.Object3D, x = 0, y = 0, z = 0): THREE.Mesh => {
    const o = new THREE.Mesh(g, m);
    o.position.set(x, y, z);
    parent.add(o);
    return o;
  };
  const zCyl = (r: number, len: number, seg = 40): THREE.BufferGeometry => new THREE.CylinderGeometry(r, r, len, seg).rotateX(Math.PI / 2);
  const xCyl = (r: number, len: number, seg = 32): THREE.BufferGeometry => new THREE.CylinderGeometry(r, r, len, seg).rotateZ(-Math.PI / 2);
  /** 둥근 모서리 북 (Z 축) — 가장자리를 깎아 반사가 테를 따라 흐르게 */
  const drum = (r: number, w: number, bevel = 0.015): THREE.BufferGeometry =>
    lathe(
      [
        [0, -w / 2],
        [r - bevel, -w / 2],
        [r, -w / 2 + bevel],
        [r, w / 2 - bevel],
        [r - bevel, w / 2],
        [0, w / 2],
      ],
      48,
    ).rotateX(Math.PI / 2);
  const screw = zCyl(0.014, 0.012, 12);
  const screws = (parent: THREE.Object3D, pts: [number, number][], z: number): void => {
    for (const [x, y] of pts) mesh(screw, M.chrome, parent, x, y, z);
  };
  const decal = (tex: THREE.Texture, w: number, h: number): THREE.Mesh =>
    new THREE.Mesh(new THREE.PlaneGeometry(w, h), E(new THREE.MeshPhysicalMaterial({ map: tex, roughness: 0.45, clearcoat: 0.5, polygonOffset: true, polygonOffsetFactor: -2 })));
  const stripes = T(stripeTex());
  const warn = T(warnTex());

  /* ── 받침 (볼트 박은 원판) ── */
  const base = new THREE.Group();
  root.add(base);
  mesh(
    lathe(
      [
        [0, 0],
        [0.55, 0],
        [0.56, 0.015],
        [0.56, 0.07],
        [0.54, 0.09],
        [0.44, 0.1],
        [0.41, 0.14],
        [0, 0.14],
      ],
      64,
    ),
    M.iron,
    base,
  );
  {
    const hex = new THREE.CylinderGeometry(0.03, 0.03, 0.03, 6);
    const washer = new THREE.CylinderGeometry(0.042, 0.042, 0.008, 20);
    for (let i = 0; i < 8; i++) {
      const a = (i / 8) * TAU + 0.2;
      mesh(washer, M.steel, base, Math.cos(a) * 0.49, 0.094, Math.sin(a) * 0.49);
      mesh(hex, M.steel, base, Math.cos(a) * 0.49, 0.11, Math.sin(a) * 0.49).rotation.y = a;
    }
  }
  // 바닥 안전선 (노랑 · 검정 빗금 고리)
  {
    const ringTex = T(stripeTex());
    ringTex.wrapS = THREE.RepeatWrapping;
    ringTex.repeat.set(10, 1);
    const ring = new THREE.Mesh(new THREE.RingGeometry(0.66, 0.74, 96, 1), E(new THREE.MeshStandardMaterial({ map: ringTex, roughness: 0.7 })));
    // 고리 uv 를 둘레 방향으로
    const uv = ring.geometry.attributes.uv as THREE.BufferAttribute;
    const p = ring.geometry.attributes.position as THREE.BufferAttribute;
    for (let i = 0; i < p.count; i++) {
      const a = Math.atan2(p.getY(i), p.getX(i));
      uv.setXY(i, (a / TAU + 0.5) * 1, Math.hypot(p.getX(i), p.getY(i)) > 0.7 ? 1 : 0);
    }
    ring.rotation.x = -Math.PI / 2;
    ring.position.y = 0.003;
    ring.receiveShadow = true;
    root.add(ring);
  }

  /* ── J1 허리 ── */
  const j1 = new THREE.Group();
  root.add(j1);
  mesh(
    lathe(
      [
        [0, 0.14],
        [0.37, 0.14],
        [0.38, 0.155],
        [0.38, 0.19],
        [0.36, 0.2],
        [0.35, 0.44],
        [0.33, 0.47],
        [0.22, 0.5],
        [0, 0.5],
      ],
      64,
    ),
    M.orange,
    j1,
  );
  mesh(lathe([[0.385, 0.2], [0.385, 0.225], [0.36, 0.225], [0.36, 0.2]], 64), M.dark, j1);
  // 어깨 기둥 — 옆모습을 깎아 돌출
  {
    const s = new THREE.Shape();
    s.moveTo(-0.3, 0.4);
    s.lineTo(0.36, 0.4);
    s.quadraticCurveTo(0.42, 0.52, 0.39, 0.66);
    s.absarc(A0, H0, 0.25, -0.24, Math.PI + 0.55, false);
    s.lineTo(-0.3, 0.56);
    s.lineTo(-0.3, 0.4);
    mesh(bevelExtrude(s, 0.4, 0.04, 32, 4), M.orange, j1);
    // 옆 덮개판 (짙은 회색) + 나사
    const plate = roundRect(0.34, 0.2, 0.04, 0.02, 0.52);
    for (const z of [0.2, -0.2]) {
      const p = mesh(bevelExtrude(plate, 0.014, 0.004), M.dark, j1, 0, 0, z + Math.sign(z) * 0.002);
      void p;
      screws(j1, [[-0.13, 0.44], [0.17, 0.44], [-0.13, 0.6], [0.17, 0.6]], z + Math.sign(z) * 0.011);
    }
    // 앞 빗금 띠 · 경고 세모
    const d = decal(stripes, 0.42, 0.06);
    d.position.set(0.358, 0.3, 0);
    d.rotation.y = Math.PI / 2;
    j1.add(d);
    const w = decal(warn, 0.11, 0.11);
    w.position.set(0.02, 0.52, 0.214);
    j1.add(w);
  }
  // 어깨 관절 북 · 모터
  for (const z of [0.215, -0.215]) {
    mesh(drum(0.25, 0.07), M.orange, j1, A0, H0, z);
    mesh(drum(0.13, 0.03, 0.008), M.steel, j1, A0, H0, z + Math.sign(z) * 0.045);
  }
  {
    // 어깨 모터 (짙은 회색, 냉각 지느러미)
    const pts: [number, number][] = [[0, 0]];
    for (let i = 0; i < 6; i++) {
      const y = 0.02 + i * 0.03;
      pts.push([0.13, y], [0.13, y + 0.012], [0.118, y + 0.016], [0.118, y + 0.03]);
    }
    pts.push([0.11, 0.21], [0.09, 0.225], [0, 0.225]);
    const motor = mesh(lathe(pts, 40), M.dark, j1, A0, H0, -0.245);
    motor.rotation.x = -Math.PI / 2;
    mesh(drum(0.06, 0.02, 0.006), M.brushed, j1, A0, H0, -0.48);
  }
  // 가스 스프링 받침 (허리 뒤)
  const springA = new THREE.Vector3(-0.17, 0.5, 0.3);
  mesh(bevelExtrude(roundRect(0.12, 0.1, 0.02, springA.x, springA.y), 0.12, 0.012), M.dark, j1, 0, 0, 0.255);

  /* ── J2 어깨 → 위팔 ── */
  const j2 = new THREE.Group();
  j2.position.set(A0, H0, 0);
  j1.add(j2);
  mesh(bevelExtrude(dogbone(L1, 0.2, 0.165, 0.125), 0.3, 0.035, 40, 4), M.orange, j2);
  {
    const plate = roundRect(L1 * 0.5, 0.15, 0.05, L1 * 0.52, 0);
    for (const z of [0.15, -0.15]) {
      mesh(bevelExtrude(plate, 0.014, 0.004), M.dark, j2, 0, 0, z + Math.sign(z) * 0.003);
      screws(
        j2,
        [
          [L1 * 0.3, 0.05],
          [L1 * 0.74, 0.05],
          [L1 * 0.3, -0.05],
          [L1 * 0.74, -0.05],
        ],
        z + Math.sign(z) * 0.012,
      );
    }
    const w = decal(warn, 0.09, 0.09);
    w.position.set(L1 * 0.52, 0, 0.169);
    j2.add(w);
    // 위쪽 빗금 띠
    const d = decal(stripes, 0.36, 0.05);
    d.position.set(L1 * 0.52, 0.126, 0);
    d.rotation.x = -Math.PI / 2;
    j2.add(d);
  }
  // 가스 스프링 핀 (위팔 옆)
  const springB = new THREE.Object3D();
  springB.position.set(0.42, 0.02, 0.3);
  j2.add(springB);
  mesh(zCyl(0.028, 0.16, 20), M.steel, j2, 0.42, 0.02, 0.22);

  /* ── J3 팔꿈치 → 아래팔 ── */
  const j3 = new THREE.Group();
  j3.position.set(L1, 0, 0);
  j2.add(j3);
  for (const z of [0.19, -0.19]) {
    mesh(drum(0.18, 0.06), M.orange, j3, 0, 0, z);
    mesh(drum(0.1, 0.025, 0.006), M.steel, j3, 0, 0, z + Math.sign(z) * 0.038);
  }
  {
    // 팔꿈치 모터 (옆에 붙음)
    const motor = mesh(lathe([[0, 0], [0.1, 0], [0.105, 0.01], [0.105, 0.15], [0.09, 0.17], [0, 0.17]], 36), M.dark, j3, 0, 0, 0.22);
    motor.rotation.x = Math.PI / 2;
    mesh(drum(0.045, 0.02, 0.005), M.brushed, j3, 0, 0, 0.4);
  }
  mesh(bevelExtrude(dogbone(J4X + 0.04, 0.15, 0.11, 0.1), 0.24, 0.03, 36, 3), M.orange, j3);
  {
    const d = decal(stripes, 0.2, 0.04);
    d.position.set(0.2, 0.102, 0);
    d.rotation.x = -Math.PI / 2;
    j3.add(d);
  }
  // 손목 비틀기 관절 둘레 테
  mesh(lathe([[0.1, 0], [0.112, 0.01], [0.112, 0.04], [0.1, 0.05], [0.08, 0.05], [0.08, 0]], 40).rotateZ(-Math.PI / 2), M.dark, j3, J4X, 0, 0);

  /* ── J4 손목 비틀기 ── */
  const j4 = new THREE.Group();
  j4.position.set(J4X + 0.05, 0, 0);
  j3.add(j4);
  const tubeLen = L2 - J4X - 0.05 - 0.1;
  mesh(
    lathe(
      [
        [0.092, 0],
        [0.092, tubeLen * 0.7],
        [0.082, tubeLen * 0.78],
        [0.078, tubeLen],
        [0, tubeLen],
      ],
      40,
    ).rotateZ(-Math.PI / 2),
    M.white,
    j4,
  );
  mesh(lathe([[0.096, 0.02], [0.096, 0.045], [0.093, 0.045], [0.093, 0.02]], 40).rotateZ(-Math.PI / 2), M.dark, j4, tubeLen * 0.3, 0, 0);
  // 손목 볼 (양옆 뺨)
  {
    const cheek = new THREE.Shape();
    cheek.moveTo(-0.12, -0.07);
    cheek.lineTo(0, -0.085);
    cheek.absarc(0, 0, 0.085, -Math.PI / 2, Math.PI / 2, false);
    cheek.lineTo(-0.12, 0.07);
    cheek.lineTo(-0.12, -0.07);
    for (const z of [0.085, -0.085]) {
      const c = mesh(bevelExtrude(cheek, 0.04, 0.01, 24, 3), M.orange, j4, L2 - J4X - 0.05, 0, z);
      void c;
    }
  }

  /* ── J5 손목 꺾기 ── */
  const j5 = new THREE.Group();
  j5.position.set(L2 - J4X - 0.05, 0, 0);
  j4.add(j5);
  mesh(drum(0.07, 0.13, 0.01), M.dark, j5);
  for (const z of [0.108, -0.108]) mesh(drum(0.045, 0.012, 0.004), M.brushed, j5, 0, 0, z);
  mesh(lathe([[0.06, 0], [0.072, 0.012], [0.072, 0.05], [0.06, 0.06], [0, 0.06]], 40).rotateZ(-Math.PI / 2), M.orange, j5, 0.03, 0, 0);

  /* ── J6 손끝 돌리기 + 집게 ── */
  const j6 = new THREE.Group();
  j6.position.set(0.1, 0, 0);
  j5.add(j6);
  mesh(lathe([[0.075, 0], [0.075, 0.018], [0.065, 0.024], [0, 0.024]], 40).rotateZ(-Math.PI / 2), M.chrome, j6);
  {
    for (let i = 0; i < 6; i++) {
      const a = (i / 6) * TAU;
      mesh(xCyl(0.008, 0.008, 8), M.dark, j6, 0.026, Math.cos(a) * 0.055, Math.sin(a) * 0.055);
    }
  }
  // 집게 몸 (Z 로 긴 상자) · 미끄럼 레일
  const gripBody = new THREE.Mesh(bevelExtrude(roundRect(0.075, 0.12, 0.02), 0.36, 0.015), M.dark);
  gripBody.position.set(0.062, 0, 0);
  j6.add(gripBody);
  mesh(new THREE.CylinderGeometry(0.008, 0.008, 0.48, 12).rotateX(Math.PI / 2), M.chrome, j6, 0.104, 0.035, 0);
  mesh(new THREE.CylinderGeometry(0.008, 0.008, 0.48, 12).rotateX(Math.PI / 2), M.chrome, j6, 0.104, -0.035, 0);
  {
    const d = decal(stripes, 0.3, 0.035);
    d.position.set(0.062, 0.061, 0);
    d.rotation.set(-Math.PI / 2, 0, Math.PI / 2);
    d.scale.set(1, 1, 1);
    j6.add(d);
  }
  // 손가락 — 집게 가운데(손목 중심에서 LT)는 j6 의 x = LT - 0.1
  const GX = LT - 0.1;
  const fingers: THREE.Group[] = [];
  {
    const fShape = new THREE.Shape();
    fShape.moveTo(0.1, -0.05);
    fShape.lineTo(GX + 0.06, -0.035);
    fShape.quadraticCurveTo(GX + 0.09, -0.03, GX + 0.09, 0);
    fShape.quadraticCurveTo(GX + 0.09, 0.03, GX + 0.06, 0.035);
    fShape.lineTo(0.1, 0.05);
    fShape.lineTo(0.1, -0.05);
    const fg = bevelExtrude(fShape, 0.026, 0.006, 16, 2);
    const carriage = bevelExtrude(roundRect(0.05, 0.11, 0.012, 0.11, 0), 0.05, 0.008);
    const pad = new THREE.BoxGeometry(0.12, 0.06, 0.008);
    for (const s of [1, -1]) {
      const f = new THREE.Group();
      mesh(fg, M.steel, f, 0, 0, s * 0.013);
      mesh(carriage, M.orange, f, 0, 0, s * 0.02);
      mesh(pad, M.rubber, f, GX + 0.02, 0, -s * 0.004);
      f.userData['s'] = s;
      j6.add(f);
      fingers.push(f);
    }
  }
  const gripPt = new THREE.Object3D();
  gripPt.position.set(GX, 0, 0);
  j6.add(gripPt);

  /* ── 가스 스프링 (허리 받침 A ↔ 위팔 핀 B) ── */
  const barrelG = new THREE.CylinderGeometry(0.038, 0.038, 1, 24);
  const rodG = new THREE.CylinderGeometry(0.016, 0.016, 1, 16);
  const barrel = mesh(barrelG, M.dark, j1);
  const barrelCap = mesh(new THREE.CylinderGeometry(0.042, 0.042, 1, 24), M.orange, j1);
  const rod = mesh(rodG, M.chrome, j1);
  const eyeG = new THREE.TorusGeometry(0.028, 0.012, 10, 20);
  const eyeA = mesh(eyeG, M.steel, j1, springA.x, springA.y, springA.z);
  const eyeB = mesh(eyeG, M.steel, j1);
  void eyeA;

  /* ── 케이블 (팔을 따라 휘는 고무 관) ── */
  const anchors: [THREE.Object3D, THREE.Vector3][] = [
    [j1, new THREE.Vector3(-0.32, 0.3, -0.12)],
    [j1, new THREE.Vector3(-0.2, 0.62, -0.3)],
    [j2, new THREE.Vector3(0.05, 0.22, -0.24)],
    [j2, new THREE.Vector3(L1 * 0.5, 0.15, -0.2)],
    [j2, new THREE.Vector3(L1 - 0.05, 0.2, -0.25)],
    [j3, new THREE.Vector3(0.22, 0.14, -0.15)],
    [j4, new THREE.Vector3(0.1, 0.1, -0.06)],
    [j4, new THREE.Vector3(tubeLen * 0.85, 0.1, 0)],
  ];
  const cablePts = anchors.map(() => new THREE.Vector3());
  const curve = new THREE.CatmullRomCurve3(cablePts, false, 'centripetal');
  const CSEG = 64;
  const cables: { m: THREE.Mesh; r: number; off: number }[] = [];
  for (const [r, off] of [
    [0.03, 0],
    [0.017, 0.055],
  ] as const) {
    const g = new THREE.TubeGeometry(new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(1, 0, 0)]), CSEG, r, 10, false);
    const m = new THREE.Mesh(g, r > 0.02 ? M.rubber : M.dark);
    m.frustumCulled = false;
    root.add(m);
    cables.push({ m, r, off });
  }
  const updCables = (): void => {
    anchors.forEach(([o, p], i) => cablePts[i]!.copy(p).applyMatrix4(o.matrixWorld));
    for (const c of cables) {
      const pts = cablePts.map((p, i) => (c.off ? p.clone().add(new THREE.Vector3(0, c.off * (i > 1 ? 0.6 : 0.2), -c.off * 0.5)) : p));
      const tg = new THREE.TubeGeometry(c.off ? new THREE.CatmullRomCurve3(pts, false, 'centripetal') : curve, CSEG, c.r, 10, false);
      (c.m.geometry.attributes.position as THREE.BufferAttribute).copyArray(tg.attributes.position!.array as Float32Array).needsUpdate = true;
      (c.m.geometry.attributes.normal as THREE.BufferAttribute).copyArray(tg.attributes.normal!.array as Float32Array).needsUpdate = true;
      tg.dispose();
    }
  };

  /* ── 컨베이어 ── */
  const conv = new THREE.Group();
  root.add(conv);
  const X0 = -0.15;
  const X1 = 2.6;
  const CL = X1 - X0;
  const CW = 0.46;
  const bt = T(beltTex());
  bt.repeat.set(CL / 0.22, 1);
  const beltM = E(new THREE.MeshStandardMaterial({ map: bt, roughness: 0.8 }));
  {
    const top = new THREE.Mesh(new THREE.PlaneGeometry(CL, CW).rotateX(-Math.PI / 2), beltM);
    top.position.set((X0 + X1) / 2, BELT_TOP, BELT_Z);
    conv.add(top);
    // 롤러 끝 (반원통 벨트) · 옆 레일 · 다리
    for (const x of [X0, X1]) {
      mesh(zCyl(0.05, CW + 0.02, 32), M.rubber, conv, x, BELT_TOP - 0.05, BELT_Z);
      mesh(zCyl(0.025, CW + 0.12, 16), M.chrome, conv, x, BELT_TOP - 0.05, BELT_Z);
    }
    const rail = bevelExtrude(roundRect(CL + 0.14, 0.12, 0.025, (X0 + X1) / 2, BELT_TOP - 0.03), 0.04, 0.01);
    for (const s of [1, -1]) mesh(rail, M.navy, conv, 0, 0, BELT_Z + s * (CW / 2 + 0.03));
    const leg = new THREE.BoxGeometry(0.05, BELT_TOP - 0.09, 0.05).translate(0, (BELT_TOP - 0.09) / 2, 0);
    for (const x of [X0 + 0.15, (X0 + X1) / 2, X1 - 0.15])
      for (const s of [1, -1]) mesh(leg, M.iron, conv, x, 0, BELT_Z + s * (CW / 2 + 0.03));
    const cross = new THREE.BoxGeometry(0.04, 0.04, CW + 0.06);
    for (const x of [X0 + 0.15, (X0 + X1) / 2, X1 - 0.15]) mesh(cross, M.iron, conv, x, 0.15, BELT_Z);
    // 멈춤 막대 (노랑)
    mesh(bevelExtrude(roundRect(0.04, 0.07, 0.012, X_STOP - BOX.x / 2 - 0.025, BELT_TOP + 0.035), CW + 0.1, 0.008), M.yellow, conv, 0, 0, BELT_Z);
    // 밑 롤러 줄 (벨트 아래 가지런히)
    const roll = zCyl(0.022, CW - 0.04, 16);
    for (let x = X0 + 0.25; x < X1 - 0.1; x += 0.25) mesh(roll, M.steel, conv, x, BELT_TOP - 0.06, BELT_Z);
  }

  /* ── 팔레트 ── */
  {
    const wood = E(new THREE.MeshStandardMaterial({ color: 0xc9a06a, roughness: 0.82 }));
    const woodD = E(new THREE.MeshStandardMaterial({ color: 0xa97f4c, roughness: 0.85 }));
// … (311줄 더 — 전체는 라이브 견본 페이지의 「코드」 탭)
```

### i542 견본 항목 — `src/demos/demosMechArm.ts:875`
```ts
  i542: {
    kind: '3d',
    caption: '6축 로봇 팔 — 컨베이어로 온 상자를 해석적 IK(허리 각 + 2링크 평면 삼각형)로 집어 팔레트에 쌓는다. 몸은 회전체 + 모서리 깎은 돌출, 가스 스프링 · 케이블은 관절을 따라 매 장면 다시 계산',
    make: () => makeArm(),
  }
```
