# AI 꾸러미 — 굴착기 (무한궤도 링크 · 유압 실린더 · 4절 링크)
> 트랙 링크 수십 개가 바퀴 둘레 경로를 따라 돌고, 붐 · 암 · 버킷 실린더가 두 핀 사이에서 늘고 줄며 흙을 퍼 옮김  
> 견본: https://ai-techstudio.web.app/#t/i544

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

## 주문서

### 만들어 줘: 굴착기 (무한궤도 링크 · 유압 실린더 · 4절 링크)

#### 1. 목표
건설 놀이에 「굴착기 (무한궤도 링크 · 유압 실린더 · 4절 링크)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 트랙 링크 수십 개가 바퀴 둘레 경로를 따라 돌고, 붐 · 암 · 버킷 실린더가 두 핀 사이에서 늘고 줄며 흙을 퍼 옮김

#### 2. 핵심 기술 용어
- 굴착기 (무한궤도 링크 · 유압 실린더 · 4절 링크) — 트랙 링크 수십 개가 바퀴 둘레 경로를 따라 돌고, 붐 · 암 · 버킷 실린더가 두 핀 사이에서 늘고 줄며 흙을 퍼 옮김

#### 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: `i544` · 분류: 기계 · 구조 › 기계 장치 · 3D · 난이도 어려움 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i544
- 쓰면 좋을 때: 쓰면 좋을 곳 — 건설 놀이 · 기계 연결 원리

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeDigger — `src/demos/demosMechDigger.ts:187`
```ts
function makeDigger(): Scene3D {
  const stage = makeStage({ size: 6 });
  const { scene, cam } = stage;
  const M = mechMats();
  // 노란 도장 — 아래쪽일수록 흙때 (색 어둡게 · 거칠게)
  const yellow = M.yellow;
  yellow.onBeforeCompile = (sh) => {
    sh.vertexShader = 'varying float vWY;\n' + sh.vertexShader.replace('#include <begin_vertex>', '#include <begin_vertex>\nvWY = (modelMatrix * vec4(transformed, 1.0)).y;');
    sh.fragmentShader =
      'varying float vWY;\n' +
      sh.fragmentShader
        .replace(
          '#include <color_fragment>',
          `#include <color_fragment>
          float dirt = (1.0 - smoothstep(0.12, 1.0, vWY)) * (0.75 + 0.25 * sin(vWY * 37.0 + gl_FragCoord.x * 0.02));
          diffuseColor.rgb = mix(diffuseColor.rgb, diffuseColor.rgb * vec3(0.42, 0.34, 0.26), clamp(dirt, 0.0, 1.0) * 0.8);`,
        )
        .replace('#include <roughnessmap_fragment>', '#include <roughnessmap_fragment>\nroughnessFactor = mix(roughnessFactor, 0.85, clamp(dirt, 0.0, 1.0) * 0.8);');
  };
  yellow.customProgramCacheKey = () => 'digger-dirty-yellow';
  const cylYellow = new THREE.MeshPhysicalMaterial({ color: 0xf2b822, metalness: 0.1, roughness: 0.38, clearcoat: 0.7, clearcoatRoughness: 0.2 });
  const rodChrome = new THREE.MeshPhysicalMaterial({ color: 0xffffff, metalness: 1, roughness: 0.05 });
  const glass = new THREE.MeshPhysicalMaterial({ color: 0x26384f, metalness: 0, roughness: 0.04, transparent: true, opacity: 0.38, envMapIntensity: 1.6, clearcoat: 1, side: THREE.DoubleSide, depthWrite: false });
  const soil = new THREE.MeshStandardMaterial({ color: 0x7a5636, roughness: 0.95 });
  const soilDark = new THREE.MeshStandardMaterial({ color: 0x5e4029, roughness: 1, flatShading: true });
  const bodyMats: THREE.Material[] = [yellow, M.iron, M.dark, M.rubber, M.steel, M.chrome, glass, M.white];
  const extraMats: THREE.Material[] = [cylYellow, rodChrome, glass, soil, soilDark];

  const root = new THREE.Group(); // 기계 전체 (주행)
  scene.add(root);

  /* ── 하부: 무한궤도 · 바퀴 · 프레임 ── */
  const linkGeo = (() => {
    const plate = new THREE.BoxGeometry(0.095, 0.045, TR_W);
    const grouser = new THREE.BoxGeometry(0.026, 0.036, TR_W).translate(0.018, 0.04, 0);
    const lugA = new THREE.BoxGeometry(0.085, 0.045, 0.06).translate(0, -0.04, 0.11);
    const lugB = new THREE.BoxGeometry(0.085, 0.045, 0.06).translate(0, -0.04, -0.11);
    const pin = new THREE.CylinderGeometry(0.014, 0.014, TR_W + 0.02, 8).rotateX(Math.PI / 2).translate(0.048, 0, 0);
    const g = mergeGeometries([plate, grouser, lugA, lugB, pin])!;
    for (const x of [plate, grouser, lugA, lugB, pin]) x.dispose();
    return g;
  })();
  const trackMat = M.iron;
  const tracks: THREE.InstancedMesh[] = [];
  for (const sz of [-1, 1]) {
    const im = new THREE.InstancedMesh(linkGeo, trackMat, N_LINK);
    im.position.z = sz * TR_Z;
    im.castShadow = im.receiveShadow = true;
    im.frustumCulled = false;
    root.add(im);
    tracks.push(im);
  }
  const spinners: { o: THREE.Object3D; r: number }[] = [];
  const sprG = bevelExtrude(sprocketShape(11, TR_R - 0.07, TR_R - 0.015, 0.06), 0.13, 0.012, 6, 2);
  const idlerG = lathe(
    [
      [0.05, -0.12],
      [TR_R - 0.04, -0.12],
      [TR_R - 0.025, -0.1],
      [TR_R - 0.025, -0.03],
      [TR_R - 0.06, -0.02],
      [TR_R - 0.06, 0.02],
      [TR_R - 0.025, 0.03],
      [TR_R - 0.025, 0.1],
      [TR_R - 0.04, 0.12],
      [0.05, 0.12],
    ],
    40,
  ).rotateX(Math.PI / 2);
  const rollG = lathe(
    [
      [0.03, -0.2],
      [0.075, -0.2],
      [0.075, -0.07],
      [0.06, -0.06],
      [0.06, 0.06],
      [0.075, 0.07],
      [0.075, 0.2],
      [0.03, 0.2],
    ],
    24,
  ).rotateX(Math.PI / 2);
  const hubG = new THREE.CylinderGeometry(0.09, 0.11, 0.08, 20).rotateX(Math.PI / 2);
  const bottomInner = TR_CY - TR_R + 0.0225;
  const topInner = TR_CY + TR_R - 0.0225;
  for (const sz of [-1, 1]) {
    const z = sz * TR_Z;
    const spr = new THREE.Mesh(sprG, M.dark);
    spr.position.set(-TR_CX, TR_CY, z);
    root.add(spr);
    spinners.push({ o: spr, r: TR_R });
    const hub = new THREE.Mesh(hubG, M.iron);
    hub.position.set(-TR_CX, TR_CY, z + sz * 0.1);
    root.add(hub);
    const idl = new THREE.Mesh(idlerG, M.dark);
    idl.position.set(TR_CX, TR_CY, z);
    root.add(idl);
    spinners.push({ o: idl, r: TR_R });
    for (const x of [-0.62, -0.21, 0.21, 0.62]) {
      const r = new THREE.Mesh(rollG, M.dark);
      r.position.set(x, bottomInner + 0.075, z);
      root.add(r);
      spinners.push({ o: r, r: 0.075 });
    }
    for (const x of [-0.45, 0.45]) {
      const r = new THREE.Mesh(rollG, M.dark);
      r.scale.set(0.7, 0.7, 0.6);
      r.position.set(x, topInner - 0.053, z);
      root.add(r);
      spinners.push({ o: r, r: 0.053 });
    }
    // 궤도 프레임 (옆에서 본 윤곽: 가운데가 높고 양끝이 낮아지는 판)
    const fs = new THREE.Shape();
    fs.moveTo(-0.95, 0.27);
    fs.lineTo(0.95, 0.27);
    fs.lineTo(1.0, 0.42);
    fs.lineTo(0.85, 0.56);
    fs.lineTo(-0.85, 0.56);
    fs.lineTo(-1.0, 0.42);
    fs.closePath();
    const frame = new THREE.Mesh(bevelExtrude(fs, 0.3, 0.025, 4, 2), yellow);
    frame.position.z = z;
    root.add(frame);
  }
  // 가운데 몸 · 선회 링
  const car = new THREE.Mesh(bevelExtrude(roundRect(1.25, 1.3, 0.12), 0.32, 0.04, 8, 2).rotateX(-Math.PI / 2), yellow);
  car.position.y = 0.5;
  root.add(car);
  const ring = new THREE.Mesh(new THREE.CylinderGeometry(0.62, 0.66, 0.1, 48), M.dark);
  ring.position.y = 0.72;
  root.add(ring);
  {
    const boltG = new THREE.CylinderGeometry(0.018, 0.018, 0.03, 6);
    const bolts = new THREE.InstancedMesh(boltG, M.steel, 24);
    const m4 = new THREE.Matrix4();
    for (let i = 0; i < 24; i++) {
      const a = (i / 24) * TAU;
      m4.makeTranslation(Math.cos(a) * 0.6, 0.78, Math.sin(a) * 0.6);
      bolts.setMatrixAt(i, m4);
    }
    root.add(bolts);
  }

  /* ── 상부 (선회) ── */
  const upper = new THREE.Group();
  upper.position.y = UP_Y;
  root.add(upper);
  const flat = (s: THREE.Shape, h: number, bevel: number): THREE.BufferGeometry => bevelExtrude(s, h, bevel, 10, 3).rotateX(-Math.PI / 2);
  // 갑판
  const deck = new THREE.Mesh(flat(roundRect(2.35, 1.9, 0.18, -0.17, 0), 0.2, 0.04), yellow);
  deck.position.y = 0.1;
  upper.add(deck);
  // 평형추 (뒤쪽, 둥근 덩어리)
  {
    const s = new THREE.Shape();
    s.moveTo(-0.95, -0.95);
    s.lineTo(-1.15, -0.95);
    s.quadraticCurveTo(-1.48, -0.9, -1.5, 0);
    s.quadraticCurveTo(-1.48, 0.9, -1.15, 0.95);
    s.lineTo(-0.95, 0.95);
    s.closePath();
    const cw = new THREE.Mesh(flat(s, 0.55, 0.07), yellow);
    cw.position.y = 0.47;
    upper.add(cw);
    const stripe = new THREE.Mesh(flat(s, 0.06, 0.01), M.dark);
    stripe.scale.set(1.01, 1, 1.01);
    stripe.position.y = 0.36;
    upper.add(stripe);
  }
  // 엔진 덮개 · 그릴 · 배기관
  {
    const hood = new THREE.Mesh(flat(roundRect(0.95, 1.15, 0.1, -0.5, 0.35), 0.48, 0.06), yellow);
    hood.position.y = 0.44;
    upper.add(hood);
    const slat = new THREE.BoxGeometry(0.6, 0.025, 0.02);
    for (let i = 0; i < 6; i++) {
      const s = new THREE.Mesh(slat, M.dark);
      s.position.set(-0.5, 0.3 + i * 0.045, -0.93);
      upper.add(s);
    }
    const grillBack = new THREE.Mesh(new THREE.BoxGeometry(0.64, 0.3, 0.01), M.rubber);
    grillBack.position.set(-0.5, 0.41, -0.928);
    upper.add(grillBack);
    const pipe = new THREE.Mesh(
      lathe(
        [
          [0.0, 0],
          [0.05, 0],
          [0.05, 0.42],
          [0.058, 0.44],
          [0.058, 0.5],
          [0.045, 0.5],
        ],
        20,
      ),
      M.chrome,
    );
    pipe.position.set(-0.32, 0.66, -0.55);
    upper.add(pipe);
    const cap = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 0.012, 20), M.dark);
    cap.position.set(-0.3, 1.17, -0.55);
    cap.rotation.z = 0.5;
    upper.add(cap);
    // 앞 오른쪽 공구함 · 난간
    const box = new THREE.Mesh(flat(roundRect(0.75, 0.48, 0.06, 0.55, 0.68), 0.32, 0.04), yellow);
    box.position.y = 0.36;
    upper.add(box);
  }
  // 운전석: 노란 아래판 · 검은 기둥 · 유리 · 지붕 · 의자 · 조종 손잡이
  {
    const cab = new THREE.Group();
    cab.position.set(0, 0.2, 0);
    upper.add(cab);
    const x0 = 0.12;
    const x1 = 0.95;
    const z0 = 0.2;
    const z1 = 0.92;
    const hTop = 1.05;
    const lower = new THREE.Mesh(flat(roundRect(x1 - x0, z1 - z0, 0.05, (x0 + x1) / 2, -(z0 + z1) / 2), 0.28, 0.03), yellow);
    lower.position.y = 0.14;
    cab.add(lower);
    // 옆 윤곽 (앞이 기운 다섯모)
    const side = new THREE.Shape();
    side.moveTo(x0, 0.28);
    side.lineTo(x1, 0.28);
    side.lineTo(x1, 0.62);
    side.lineTo(x1 - 0.14, hTop);
    side.lineTo(x0, hTop);
    side.closePath();
    const shell = new THREE.Mesh(new THREE.ExtrudeGeometry(side, { depth: z1 - z0 - 0.04, bevelEnabled: false }).translate(0, 0, -(z1 - z0 - 0.04) / 2), glass);
    shell.position.z = (z0 + z1) / 2;
    shell.renderOrder = 2;
    cab.add(shell);
    const pil = new THREE.CylinderGeometry(0.025, 0.025, 1, 8);
    const pillar = (a: THREE.Vector3, b: THREE.Vector3): void => {
      const m = new THREE.Mesh(pil, M.dark);
      const d = b.clone().sub(a);
      m.position.copy(a).addScaledVector(d, 0.5);
      m.quaternion.setFromUnitVectors(V3(0, 1, 0), d.clone().normalize());
      m.scale.y = d.length();
      cab.add(m);
    };
    for (const z of [z0 + 0.02, z1 - 0.02]) {
      pillar(V3(x0, 0.28, z), V3(x0, hTop, z));
      pillar(V3(x1, 0.28, z), V3(x1, 0.62, z));
      pillar(V3(x1, 0.62, z), V3(x1 - 0.14, hTop, z));
      pillar(V3((x0 + x1) / 2 - 0.05, 0.28, z), V3((x0 + x1) / 2 - 0.05, hTop, z));
      pillar(V3(x0, 0.62, z), V3(x1, 0.62, z));
    }
    pillar(V3(x1, 0.62, z0 + 0.02), V3(x1, 0.62, z1 - 0.02));
    pillar(V3(x1, 0.28, z0 + 0.02), V3(x1, 0.28, z1 - 0.02));
    const roof = new THREE.Mesh(flat(roundRect(x1 - x0 + 0.06, z1 - z0 + 0.06, 0.05, (x0 + x1) / 2 - 0.05, -(z0 + z1) / 2), 0.06, 0.02), yellow);
    roof.position.y = hTop + 0.03;
    cab.add(roof);
    const seatB = new THREE.Mesh(flat(roundRect(0.3, 0.32, 0.06, 0.38, -0.56), 0.1, 0.03), M.rubber);
    seatB.position.y = 0.42;
    const back = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.36, 0.3), M.rubber);
    back.position.set(0.22, 0.62, 0.56);
    back.rotation.z = 0.15;
    cab.add(seatB, back);
    for (const z of [0.4, 0.72]) {
      const st = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 0.16, 8), M.steel);
      st.position.set(0.55, 0.5, z);
      st.rotation.z = -0.25;
      const knob = new THREE.Mesh(new THREE.SphereGeometry(0.028, 12, 8), M.red);
      knob.position.set(0.57, 0.58, z);
      cab.add(st, knob);
    }
    const lamp = new THREE.Mesh(new THREE.CylinderGeometry(0.045, 0.05, 0.05, 16).rotateZ(Math.PI / 2), M.chrome);
    lamp.position.set(x1 - 0.1, hTop + 0.09, (z0 + z1) / 2);
    const lens = new THREE.Mesh(new THREE.CircleGeometry(0.04, 16).rotateY(Math.PI / 2), new THREE.MeshBasicMaterial({ color: 0xfff6d8 }));
    lens.position.set(x1 - 0.074, hTop + 0.09, (z0 + z1) / 2);
    cab.add(lamp, lens);
    extraMats.push(lens.material as THREE.Material);
  }
  // 붐 받침 (갑판 앞 두 귀)
  {
    const ear = new THREE.Shape();
    ear.moveTo(0.55, 0.2);
    ear.lineTo(1.35, 0.2);
    ear.lineTo(1.3, 0.3);
    ear.absarc(BOOM_PIV[0], BOOM_PIV[1], 0.13, 0, Math.PI, false);
    ear.lineTo(0.6, 0.3);
    ear.closePath();
    for (const dz of [-0.2, 0.2]) {
      const e = new THREE.Mesh(bevelExtrude(ear, 0.06, 0.012, 16, 2), yellow);
      e.position.z = BOOM_Z + dz;
      upper.add(e);
    }
  }

  /* ── 핀 (회전축) ── */
  const pinG = new THREE.CylinderGeometry(0.045, 0.045, 1, 16).rotateX(Math.PI / 2);
  const pinCapG = new THREE.CylinderGeometry(0.06, 0.06, 0.02, 16).rotateX(Math.PI / 2);
  const pin = (parent: THREE.Object3D, p: V2, w: number, z = 0): void => {
    const m = new THREE.Mesh(pinG, M.steel);
    m.position.set(p[0], p[1], z);
    m.scale.z = w;
    parent.add(m);
    for (const s of [-1, 1]) {
      const c = new THREE.Mesh(pinCapG, M.iron);
      c.position.set(p[0], p[1], z + (s * w) / 2);
      parent.add(c);
    }
  };

  /* ── 붐 ── */
  const boom = new THREE.Group();
  boom.position.set(BOOM_PIV[0], BOOM_PIV[1], BOOM_Z);
  upper.add(boom);
  const boomPts: V2[] = [
    [0, 0],
    [0.55, 0.3],
    [1.0, 0.42],
    [1.55, 0.3],
    [2.1, 0],
  ];
  boom.add(new THREE.Mesh(bevelExtrude(taperShape(boomPts, 0.34, 0.24), 0.3, 0.035, 40, 3), yellow));
  pin(upper, BOOM_PIV, 0.56, BOOM_Z);
  pin(boom, BOOM_TIP, 0.42);
  pin(boom, BC_ROD, 0.66);
  // 팔 실린더 받침 (붐 위 귀)
  {
    const s = new THREE.Shape();
    s.moveTo(AC_BASE[0] - 0.16, AC_BASE[1] - 0.12);
    s.lineTo(AC_BASE[0] + 0.16, AC_BASE[1] - 0.12);
    s.absarc(AC_BASE[0], AC_BASE[1], 0.08, 0, Math.PI, false);
    s.closePath();
    for (const dz of [-0.1, 0.1]) {
      const e = new THREE.Mesh(bevelExtrude(s, 0.04, 0.01, 12, 2), yellow);
      e.position.z = dz;
      boom.add(e);
    }
    pin(boom, AC_BASE, 0.26);
  }
  // 유압 호스 (붐 위를 따라)
  {
    const hoseCurve = (dz: number): THREE.CatmullRomCurve3 =>
      new THREE.CatmullRomCurve3([V3(0.15, 0.24, dz), V3(0.55, 0.47, dz), V3(1.0, 0.6, dz), V3(1.55, 0.47, dz), V3(1.95, 0.22, dz), V3(2.15, 0.16, dz)]);
    for (const dz of [-0.08, 0.08]) {
      const h = new THREE.Mesh(new THREE.TubeGeometry(hoseCurve(dz), 48, 0.022, 8), M.rubber);
      boom.add(h);
    }
    for (const u of [0.3, 0.55, 0.8]) {
      const clampM = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.03, 0.26), M.dark);
      const p = hoseCurve(0).getPointAt(u);
      clampM.position.set(p.x, p.y - 0.01, 0);
      boom.add(clampM);
    }
  }

  /* ── 팔 ── */
  const arm = new THREE.Group();
  arm.position.set(BOOM_TIP[0], BOOM_TIP[1], 0);
  boom.add(arm);
  const armPts: V2[] = [
    [-0.42, 0.12],
    [0, 0.02],
    [0.8, 0.0],
    [1.6, 0],
  ];
  arm.add(new THREE.Mesh(bevelExtrude(taperShape(armPts, 0.3, 0.17), 0.24, 0.03, 40, 3), yellow));
  pin(arm, AC_ROD, 0.3);
  pin(arm, ARM_TIP, 0.5);
  pin(arm, ROCK_C, 0.34);
  // 버킷 실린더 받침
  {
    const s = new THREE.Shape();
    s.moveTo(KC_BASE[0] - 0.14, KC_BASE[1] - 0.12);
    s.lineTo(KC_BASE[0] + 0.14, KC_BASE[1] - 0.12);
    s.absarc(KC_BASE[0], KC_BASE[1], 0.07, 0, Math.PI, false);
    s.closePath();
    for (const dz of [-0.09, 0.09]) {
      const e = new THREE.Mesh(bevelExtrude(s, 0.035, 0.01, 12, 2), yellow);
      e.position.z = dz;
      arm.add(e);
    }
    pin(arm, KC_BASE, 0.24);
  }
  {
    const hc = new THREE.CatmullRomCurve3([V3(-0.2, 0.2, 0.07), V3(0.4, 0.15, 0.13), V3(1.0, 0.12, 0.13), V3(1.25, 0.16, 0.1)]);
    arm.add(new THREE.Mesh(new THREE.TubeGeometry(hc, 32, 0.018, 8), M.rubber));
  }

  /* ── 버킷 ── */
  const bucket = new THREE.Group();
  bucket.position.set(ARM_TIP[0], ARM_TIP[1], 0);
  arm.add(bucket);
  const BW = 0.62;
  const backCurve: V2[] = [
    [-0.1, 0.12],
    [0.12, 0.13],
    [0.32, 0.02],
    [0.44, -0.2],
    [0.42, -0.44],
    [0.28, -0.6],
    [0.1, -0.66],
  ];
  const lip: V2 = [0.1, -0.66];
  const tipMark = new THREE.Object3D();
// … (420줄 더 — 전체는 라이브 견본 페이지의 「코드」 탭)
```

### i544 견본 항목 — `src/demos/demosMechDigger.ts:1019`
```ts
  i544: { kind: '3d', caption: '무한궤도 링크 60개 × 2가 바퀴 둘레 경로를 따라 돌고, 유압 실린더 셋은 두 핀 사이 거리만큼 봉이 통에서 나왔다 들어간다 — 버킷은 4절 링크', make: () => makeDigger() }
```
