# Saw Blade

> A menacing circular saw hazard: a polished steel blade with hooked teeth and cut-out lightening holes, bolted to a red hub on a sturdy floor stand with a hazard-striped base. Set the diameter and tooth count, or remove the stand to run the bare blade along rails and walls.

![Saw Blade preview](https://codels.download/m/saw-blade.png)

Multi-view sheet (3/4, front, side, top): https://codels.download/m/saw-blade.sheet.png

## Facts

- Category: Game Kit › Hazards
- Size (W × H × D): 1.00 × 1.21 × 0.42 m — units are meters, +Y up, front faces +Z
- Origin: center of the base (y = 0)
- Triangles: 920 · Meshes: 20 · Materials: 6
- Animations: 'idle' (loop) — see "Animations" below
- License: CC0-1.0 (Codels built-in)
- Tags: saw, saw-blade, cartoon, low-poly, stylized, industrial, dungeon, party-game, platformer, roguelike, hazard, obstacle, metal, round, animated, game-ready, mobile-friendly, parametric, color-blue, color-gray, color-yellow, size-medium, poly-low
- Page: https://codels.download/models/saw-blade

## Files

| Format | URL |
|---|---|
| three.js ES module (`createSawBlade()`) | https://codels.download/m/saw-blade.js |
| React Three Fiber component (`<SawBlade />`) | https://codels.download/m/saw-blade.jsx |
| Instancing module (`createSawBladeInstances(count, place)`, one draw call per material) | https://codels.download/m/saw-blade.instanced.js |
| Standalone HTML demo | https://codels.download/m/saw-blade.html |
| glTF binary | https://codels.download/m/saw-blade.glb |
| glTF binary, Meshopt-compressed (2–3× smaller; GLTFLoader needs `setMeshoptDecoder(MeshoptDecoder)`) | https://codels.download/m/saw-blade.glb?compress=meshopt |
| glTF JSON | https://codels.download/m/saw-blade.gltf |
| OBJ (vertex colors) + MTL | https://codels.download/m/saw-blade.obj · https://codels.download/m/saw-blade.mtl |
| STL | https://codels.download/m/saw-blade.stl |
| CodelSpec JSON | https://codels.download/m/saw-blade.json |

## Parameters

Append them as query parameters to any URL above to get a customized variant (e.g. `?diameter=...`).

| Param | Type | Range / options | Default | Current |
|---|---|---|---|---|
| `diameter` | number | 0.5 – 2 | 1 | 1 |
| `teeth` | int | 8 – 32 | 18 | 18 |
| `accent` | color | #rrggbb | #e0442f | #e0442f |
| `mount` | select | stand \| none | stand | stand |

## Animations

The model (the .js module and the .glb/.gltf files alike) carries THREE.AnimationClip objects in `model.animations` (glTF: `gltf.animations`). The static model is the rest pose.

```js
const mixer = new THREE.AnimationMixer(model);
mixer.clipAction(THREE.AnimationClip.findByName(model.animations, 'idle')).play(); // loop
// every frame: mixer.update(deltaSeconds);
```

## Parts

- base (box · frame)
- baseTrim (box · hazard)
- arm (extrude · frame)
- armHead (cylinder · frame)
- axle (cylinder · bolt)
- sawBlade (group)
  - blade (extrude · blade)
  - hubFront (cylinder · hub)
  - hubBack (cylinder · hub)
  - axleCap (cylinder · bolt)

## How to use

Save the code below as `saw-blade.js`, then:

```js
import { createSawBlade } from './saw-blade.js';
const model = createSawBlade();
scene.add(model);
```

## Code

```js
/**
 * Saw Blade — Codels 3D model library (https://codels.download/models/saw-blade)
 * License: CC0-1.0 · Units: meters · Y-up · Front: +Z · Origin: center of the base (y = 0)
 * Triangles: 920 · Size (W × H × D): 1.00 × 1.21 × 0.42 m
 *
 * Usage:
 *   import { createSawBlade } from './saw-blade.js';
 *   
 *   const model = createSawBlade();
 *   model.position.set(0, 0, 0);
 *   scene.add(model);
 *   
 *   // Animations: idle
 *   const mixer = new THREE.AnimationMixer(model);
 *   mixer.clipAction(THREE.AnimationClip.findByName(model.animations, 'idle')).play();
 *   // in your render loop: mixer.update(clock.getDelta());
 */
import * as THREE from 'three';

const deg = THREE.MathUtils.degToRad;

// Extrude a 2D outline along Z (centered). bevel = [thickness, size, segments].
function extrude(outline, depth, holes = [], bevel = null) {
  const shape = new THREE.Shape(outline.map(([x, y]) => new THREE.Vector2(x, y)));
  for (const hole of holes) shape.holes.push(new THREE.Path(hole.map(([x, y]) => new THREE.Vector2(x, y))));
  const options = bevel
    ? { depth, bevelEnabled: true, bevelThickness: bevel[0], bevelSize: bevel[1], bevelSegments: bevel[2] }
    : { depth, bevelEnabled: false };
  return new THREE.ExtrudeGeometry(shape, options).translate(0, 0, -depth / 2);
}

// Build animation clips from simple descriptions (Codels). Play them with THREE.AnimationMixer.
function codelsClips(THREE, entries) {
  var TAU = Math.PI * 2;
  var DEG = Math.PI / 180;
  var AX = { x: new THREE.Vector3(1, 0, 0), y: new THREE.Vector3(0, 1, 0), z: new THREE.Vector3(0, 0, 1) };
  var periodOf = function (a) { return a.type === 'spin' ? 360 / Math.abs(a.speed) : a.period; };
  var clips = [];

  // loops → 'idle'
  var loops = [];
  entries.forEach(function (e) {
    var l = e.anim.filter(function (a) { return a.type !== 'hinge'; });
    if (l.length) loops.push({ target: e.target, anim: l });
  });
  if (loops.length) {
    var periods = [];
    loops.forEach(function (e) { e.anim.forEach(function (a) { periods.push(periodOf(a)); }); });
    var longest = Math.max.apply(null, periods);
    var D = longest;
    var bestErr = Infinity;
    for (var m = 1; m <= 6 && bestErr > 0.01; m++) {
      var err = 0;
      periods.forEach(function (p) {
        var n = Math.max(1, Math.round((longest * m) / p));
        err = Math.max(err, Math.abs((longest * m) / n - p) / p);
      });
      if (err < bestErr - 1e-9) { bestErr = err; D = longest * m; }
    }
    var cycles = function (a) { return Math.max(1, Math.round(D / periodOf(a))); };
    var steps = 24;
    loops.forEach(function (e) { e.anim.forEach(function (a) { steps = Math.max(steps, cycles(a) * 24); }); });
    steps = Math.min(steps, 720);
    var tracks = [];
    loops.forEach(function (e) {
      var o = e.target;
      var q0 = o.quaternion.clone(), p0 = o.position.clone(), s0 = o.scale.clone();
      var q = new THREE.Quaternion(), turn = new THREE.Quaternion(), p = new THREE.Vector3(), s = new THREE.Vector3();
      var has = function (type) { return e.anim.some(function (a) { return a.type === type; }); };
      var turns = has('spin') || has('swing'), moves = has('bob'), grows = has('pulse');
      var times = [], qv = [], pv = [], sv = [];
      for (var k = 0; k <= steps; k++) {
        var t = (D * k) / steps;
        q.copy(q0); p.copy(p0); s.copy(s0);
        e.anim.forEach(function (a) {
          var n = cycles(a);
          var w = Math.sin(TAU * ((n * t) / D + (a.phase || 0)));
          if (a.type === 'spin') q.multiply(turn.setFromAxisAngle(AX[a.axis], (a.speed < 0 ? -TAU : TAU) * ((n * t) / D)));
          else if (a.type === 'swing') q.multiply(turn.setFromAxisAngle(AX[a.axis], a.angle * DEG * w));
          else if (a.type === 'bob') p.addScaledVector(AX[a.axis], a.distance * w);
          else if (a.type === 'pulse') s.multiplyScalar(1 + a.amount * w);
        });
        times.push(t);
        if (turns) qv.push(q.x, q.y, q.z, q.w);
        if (moves) pv.push(p.x, p.y, p.z);
        if (grows) sv.push(s.x, s.y, s.z);
      }
      if (turns) tracks.push(new THREE.QuaternionKeyframeTrack(o.uuid + '.quaternion', times, qv));
      if (moves) tracks.push(new THREE.VectorKeyframeTrack(o.uuid + '.position', times, pv));
      if (grows) tracks.push(new THREE.VectorKeyframeTrack(o.uuid + '.scale', times, sv));
    });
    clips.push(new THREE.AnimationClip('idle', D, tracks));
  }

  // hinge moves → one-shot clips ('open' …)
  var shots = {};
  entries.forEach(function (e) {
    e.anim.forEach(function (a) {
      if (a.type !== 'hinge') return;
      var name = a.clip || 'open';
      (shots[name] = shots[name] || []).push({ target: e.target, a: a });
    });
  });
  Object.keys(shots).forEach(function (name) {
    var tracks = [];
    var length = 0;
    shots[name].forEach(function (h) {
      var o = h.target, q0 = o.quaternion.clone(), q = new THREE.Quaternion(), turn = new THREE.Quaternion();
      var n = Math.max(4, Math.ceil(Math.abs(h.a.angle) / 10));
      var times = [], qv = [];
      for (var k = 0; k <= n; k++) {
        var f = k / n;
        var eased = f * f * (3 - 2 * f);
        q.copy(q0).multiply(turn.setFromAxisAngle(AX[h.a.axis], h.a.angle * DEG * eased));
        times.push(h.a.duration * f);
        qv.push(q.x, q.y, q.z, q.w);
      }
      length = Math.max(length, h.a.duration);
      tracks.push(new THREE.QuaternionKeyframeTrack(o.uuid + '.quaternion', times, qv));
    });
    clips.push(new THREE.AnimationClip(name, length, tracks));
  });
  return clips;
}

export function createSawBlade() {
  const root = new THREE.Group();
  root.name = 'SawBlade';

  // Materials
  const bladeMat = new THREE.MeshStandardMaterial({ name: 'blade', color: 0xd4d9e1, roughness: 0.22, metalness: 0.85, flatShading: true });
  const hubMat = new THREE.MeshStandardMaterial({ name: 'hub', color: 0xe0442f, roughness: 0.45, metalness: 0.2, flatShading: true });
  const boltMat = new THREE.MeshStandardMaterial({ name: 'bolt', color: 0x2a2d33, roughness: 0.5, metalness: 0.5, flatShading: true });
  const frameMat = new THREE.MeshStandardMaterial({ name: 'frame', color: 0x454c59, roughness: 0.6, metalness: 0.35, flatShading: true });
  const hazardMat = new THREE.MeshStandardMaterial({ name: 'hazard', color: 0xf2c12e, roughness: 0.6, flatShading: true });
  const stripeMat = new THREE.MeshStandardMaterial({ name: 'stripe', color: 0x1f2128, roughness: 0.7, flatShading: true });

  // Shared geometries
  const boxGeo = new THREE.BoxGeometry(0.075, 0.044, 0.008);
  const cylinderGeo = new THREE.CylinderGeometry(0.11, 0.11, 0.035, 12);
  const cylinderGeo2 = new THREE.CylinderGeometry(0.0132, 0.0132, 0.0175, 6);

  // Parts
  const base = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.12, 0.4), frameMat);
  base.name = 'base';
  base.position.set(0, 0.06, -0.0387);
  root.add(base);
  const baseTrim = new THREE.Mesh(new THREE.BoxGeometry(0.92, 0.042, 0.42), hazardMat);
  baseTrim.name = 'baseTrim';
  baseTrim.position.set(0, 0.021, -0.0387);
  root.add(baseTrim);
  const box3 = new THREE.Mesh(boxGeo, stripeMat);
  box3.position.set(-0.375, 0.022, 0.1713);
  root.add(box3);
  const box4 = new THREE.Mesh(boxGeo, stripeMat);
  box4.position.set(-0.225, 0.022, 0.1713);
  root.add(box4);
  const box5 = new THREE.Mesh(boxGeo, stripeMat);
  box5.position.set(-0.075, 0.022, 0.1713);
  root.add(box5);
  const box6 = new THREE.Mesh(boxGeo, stripeMat);
  box6.position.set(0.075, 0.022, 0.1713);
  root.add(box6);
  const box7 = new THREE.Mesh(boxGeo, stripeMat);
  box7.position.set(0.225, 0.022, 0.1713);
  root.add(box7);
  const box8 = new THREE.Mesh(boxGeo, stripeMat);
  box8.position.set(0.375, 0.022, 0.1713);
  root.add(box8);
  const arm = new THREE.Mesh(extrude([[-0.11, 0.11], [0.11, 0.11], [0.06, 0.71], [-0.06, 0.71]], 0.05), frameMat);
  arm.name = 'arm';
  arm.position.set(0, 0, -0.1025);
  root.add(arm);
  const armHead = new THREE.Mesh(new THREE.CylinderGeometry(0.065, 0.065, 0.05, 10), frameMat);
  armHead.name = 'armHead';
  armHead.position.set(0, 0.71, -0.1025);
  armHead.rotation.set(deg(90), 0, 0);
  root.add(armHead);
  const axle = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.1025, 8), boltMat);
  axle.name = 'axle';
  axle.position.set(0, 0.71, -0.0387);
  axle.rotation.set(deg(90), 0, 0);
  root.add(axle);
  const sawBlade = new THREE.Group();
  sawBlade.name = 'sawBlade';
  sawBlade.position.set(0, 0.71, 0);
  sawBlade.rotation.set(0, 0, deg(8));
  root.add(sawBlade);
  const blade = new THREE.Mesh(extrude([[0.42, 0], [0.4555, 0.0885], [0.4776, 0.1479], [0.3947, 0.1436], [0.3977, 0.239], [0.3983, 0.3023], [0.3217, 0.27], [0.292, 0.3606], [0.2709, 0.4203], [0.21, 0.3637], [0.1511, 0.4387], [0.1108, 0.4876], [0.0729, 0.4136], [-0.0081, 0.4639], [-0.0627, 0.4961], [-0.0729, 0.4136], [-0.1663, 0.4332], [-0.2285, 0.4447], [-0.21, 0.3637], [-0.3044, 0.3502], [-0.3669, 0.3397], [-0.3217, 0.27], [-0.4058, 0.225], [-0.4609, 0.1938], [-0.3947, 0.1436], [-0.4583, 0.0726], [-0.4994, 0.0244], [-0.42, 0], [-0.4555, -0.0885], [-0.4776, -0.1479], [-0.3947, -0.1436], [-0.3977, -0.239], [-0.3983, -0.3023], [-0.3217, -0.27], [-0.292, -0.3606], [-0.2709, -0.4203], [-0.21, -0.3637], [-0.1511, -0.4387], [-0.1108, -0.4876], [-0.0729, -0.4136], [0.0081, -0.4639], [0.0627, -0.4961], [0.0729, -0.4136], [0.1663, -0.4332], [0.2285, -0.4447], [0.21, -0.3637], [0.3044, -0.3502], [0.3669, -0.3397], [0.3217, -0.27], [0.4058, -0.225], [0.4609, -0.1938], [0.3947, -0.1436], [0.4583, -0.0726], [0.4994, -0.0244]], 0.025, [[[0.2488, 0.1838], [0.2364, 0.2221], [0.2039, 0.2457], [0.1638, 0.2457], [0.1313, 0.2221], [0.1188, 0.1838], [0.1313, 0.1456], [0.1638, 0.122], [0.2039, 0.122], [0.2364, 0.1456]], [[-0.1188, 0.1838], [-0.1313, 0.2221], [-0.1638, 0.2457], [-0.2039, 0.2457], [-0.2364, 0.2221], [-0.2488, 0.1838], [-0.2364, 0.1456], [-0.2039, 0.122], [-0.1638, 0.122], [-0.1313, 0.1456]], [[-0.1188, -0.1838], [-0.1313, -0.1456], [-0.1638, -0.122], [-0.2039, -0.122], [-0.2364, -0.1456], [-0.2488, -0.1838], [-0.2364, -0.2221], [-0.2039, -0.2457], [-0.1638, -0.2457], [-0.1313, -0.2221]], [[0.2488, -0.1838], [0.2364, -0.1456], [0.2039, -0.122], [0.1638, -0.122], [0.1313, -0.1456], [0.1188, -0.1838], [0.1313, -0.2221], [0.1638, -0.2457], [0.2039, -0.2457], [0.2364, -0.2221]], [[0.315, 0], [0.3025, 0.0217], [0.2775, 0.0217], [0.265, 0], [0.2775, -0.0217], [0.3025, -0.0217]], [[0.025, 0.29], [0.0125, 0.3117], [-0.0125, 0.3117], [-0.025, 0.29], [-0.0125, 0.2683], [0.0125, 0.2683]], [[-0.265, 0], [-0.2775, 0.0217], [-0.3025, 0.0217], [-0.315, 0], [-0.3025, -0.0217], [-0.2775, -0.0217]], [[0.025, -0.29], [0.0125, -0.2683], [-0.0125, -0.2683], [-0.025, -0.29], [-0.0125, -0.3117], [0.0125, -0.3117]]]), bladeMat);
  blade.name = 'blade';
  sawBlade.add(blade);
  const hubFront = new THREE.Mesh(cylinderGeo, hubMat);
  hubFront.name = 'hubFront';
  hubFront.position.set(0, 0, 0.028);
  hubFront.rotation.set(deg(90), 0, 0);
  sawBlade.add(hubFront);
  const hubBack = new THREE.Mesh(cylinderGeo, hubMat);
  hubBack.name = 'hubBack';
  hubBack.position.set(0, 0, -0.028);
  hubBack.rotation.set(deg(90), 0, 0);
  sawBlade.add(hubBack);
  const axleCap = new THREE.Mesh(new THREE.CylinderGeometry(0.0385, 0.0385, 0.028, 6), boltMat);
  axleCap.name = 'axleCap';
  axleCap.position.set(0, 0, 0.058);
  axleCap.rotation.set(deg(90), 0, 0);
  sawBlade.add(axleCap);
  const cylinder17 = new THREE.Mesh(cylinderGeo2, boltMat);
  cylinder17.position.set(0.0726, 0, 0.0528);
  cylinder17.rotation.set(deg(90), 0, 0);
  sawBlade.add(cylinder17);
  const cylinder18 = new THREE.Mesh(cylinderGeo2, boltMat);
  cylinder18.position.set(0.0224, 0.069, 0.0528);
  cylinder18.rotation.set(deg(90), 0, 0);
  sawBlade.add(cylinder18);
  const cylinder19 = new THREE.Mesh(cylinderGeo2, boltMat);
  cylinder19.position.set(-0.0587, 0.0427, 0.0528);
  cylinder19.rotation.set(deg(90), 0, 0);
  sawBlade.add(cylinder19);
  const cylinder20 = new THREE.Mesh(cylinderGeo2, boltMat);
  cylinder20.position.set(-0.0587, -0.0427, 0.0528);
  cylinder20.rotation.set(deg(90), 0, 0);
  sawBlade.add(cylinder20);
  const cylinder21 = new THREE.Mesh(cylinderGeo2, boltMat);
  cylinder21.position.set(0.0224, -0.069, 0.0528);
  cylinder21.rotation.set(deg(90), 0, 0);
  sawBlade.add(cylinder21);

  // Animations ('idle') — play them with THREE.AnimationMixer
  root.animations = codelsClips(THREE, [
    { target: sawBlade, anim: [{ type: 'spin', axis: 'z', speed: 720 }] },
  ]);

  root.traverse((o) => {
    if (o.isMesh) {
      o.castShadow = true;
      o.receiveShadow = true;
    }
  });
  return root;
}

export default createSawBlade;
```

---
Codels — 3D models as code for three.js. Search more: https://codels.download/api/v1/models?q=<query> · Guide: https://codels.download/llms.txt
