Matter.js Rack and Pinion Steering with Travel Stops

This article explains how to build a robust rack-and-pinion steering mechanism with mechanical travel stops using the Matter.js 2D physics engine. You will learn how to create the structural components, constrain the motion of both the pinion and the rack, synchronize rotational and linear motion reliably, and install static physical barriers to limit maximum steering lock.

Core Concepts of the Assembly

A physical rack-and-pinion translates rotational input from a circular gear (the pinion) into linear motion along a toothed bar (the rack). In real-time physics engines like Matter.js, simulating microscopic gear teeth with rigid bodies often results in jitter, high computational cost, and tooth clipping under torque.

A production-ready implementation combines:

  1. A pinned pinion that rotates freely around an anchor point.
  2. A sliding rack constrained to a single axis of linear translation.
  3. Kinematic coupling to transfer angular displacement directly to linear displacement.
  4. Mechanical stop blocks positioned to physically arrest the rack at the ends of its travel path.

1. Setting Up the Environment

Begin by importing the required Matter.js modules and initializing the engine, world, and renderer.

const { Engine, Render, Runner, Bodies, Body, Constraint, Composite, Events, Vector } = Matter;

const engine = Engine.create();
const world = engine.world;

const render = Render.create({
    element: document.body,
    engine: engine,
    options: {
        width: 800,
        height: 600,
        wireframes: false
    }
});

Render.run(render);
Runner.run(Runner.create(), engine);

2. Creating the Pinion and Pivot Constraint

The pinion is a circular body anchored to a fixed coordinate via a revolute constraint. Setting a fixed pivot allows user input or a motor script to rotate the wheel while keeping its center of mass stationary.

const pinionX = 400;
const pinionY = 250;
const pinionRadius = 40;

const pinion = Bodies.circle(pinionX, pinionY, pinionRadius, {
    density: 0.05,
    friction: 0.8,
    render: { fillStyle: '#2ecc71' }
});

// Anchor the pinion to its initial position
const pinionPivot = Constraint.create({
    pointA: { x: pinionX, y: pinionY },
    bodyB: pinion,
    pointB: { x: 0, y: 0 },
    stiffness: 1,
    length: 0
});

Composite.add(world, [pinion, pinionPivot]);

3. Building the Linear Rack and Guide Track

The rack is a horizontal rectangular body. To ensure it translates only along the X-axis without tipping over, set its inertia to infinity (inertia: Infinity) or use guide rails. Setting infinite inertia prevents unwanted rotation while allowing linear forces to act normally.

const rackWidth = 300;
const rackHeight = 20;
const rackY = pinionY + pinionRadius + (rackHeight / 2);

const rack = Bodies.rectangle(pinionX, rackY, rackWidth, rackHeight, {
    inertia: Infinity, // Prevents rotation
    friction: 0.1,
    restitution: 0,
    render: { fillStyle: '#3498db' }
});

// Guide rails to prevent vertical drift
const upperRail = Bodies.rectangle(400, rackY - 15, 600, 10, { isStatic: true, isSensor: true });
const lowerRail = Bodies.rectangle(400, rackY + 15, 600, 10, { isStatic: true, friction: 0 });

Composite.add(world, [rack, lowerRail]);

4. Adding Mechanical Travel Stops

Mechanical stops are rigid static bodies placed on either side of the rack's designated path. When the rack translates too far in either direction, it collides with these barriers, physically halting both the rack and any coupled movement.

const maxTravel = 80; // Maximum allowed distance left or right from center
const stopWidth = 20;
const stopHeight = 40;

const leftStop = Bodies.rectangle(
    pinionX - (rackWidth / 2) - maxTravel - (stopWidth / 2),
    rackY,
    stopWidth,
    stopHeight,
    { isStatic: true, render: { fillStyle: '#e74c3c' } }
);

const rightStop = Bodies.rectangle(
    pinionX + (rackWidth / 2) + maxTravel + (stopWidth / 2),
    rackY,
    stopWidth,
    stopHeight,
    { isStatic: true, render: { fillStyle: '#e74c3c' } }
);

Composite.add(world, [leftStop, rightStop]);

5. Kinematic Coupling and Collision-Aware Synchronization

To couple the rack and pinion without slipping, use the engine's beforeUpdate event. The displacement formula equates linear movement (\(\Delta x\)) to angular movement (\(\Delta \theta \times r\)). If the rack encounters a mechanical stop, its horizontal velocity drops to zero, and that resistance is transferred back to the pinion to stop its rotation.

let lastPinionAngle = pinion.angle;

Events.on(engine, 'beforeUpdate', () => {
    const deltaAngle = pinion.angle - lastPinionAngle;
    lastPinionAngle = pinion.angle;

    // Expected linear displacement: dx = r * dTheta
    const targetVelocityX = deltaAngle * pinionRadius * 60; // Scaled to engine frame rate

    // Apply linear velocity to rack if not impeded
    Body.setVelocity(rack, {
        x: targetVelocityX,
        y: 0 // Constrain vertical movement entirely
    });

    // Check if rack has struck travel stops
    const rackLeftEdge = rack.position.x - rackWidth / 2;
    const rackRightEdge = rack.position.x + rackWidth / 2;

    const atLeftLimit = rackLeftEdge <= leftStop.position.x + stopWidth / 2;
    const atRightLimit = rackRightEdge >= rightStop.position.x - stopWidth / 2;

    // Zero out velocity when hitting limits to prevent jitter
    if ((atLeftLimit && targetVelocityX < 0) || (atRightLimit && targetVelocityX > 0)) {
        Body.setVelocity(rack, { x: 0, y: 0 });
        Body.setAngularVelocity(pinion, 0);
    }
});

6. Controlling the Steering Assembly

To operate the assembly, apply an external torque or set the angular velocity of the pinion directly. Because of the programmatic coupling and static stop blocks, the entire assembly responds predictably:

// Example input handlers for steering control
window.addEventListener('keydown', (event) => {
    const steerSpeed = 0.05;
    if (event.key === 'ArrowLeft') {
        Body.setAngularVelocity(pinion, -steerSpeed);
    } else if (event.key === 'ArrowRight') {
        Body.setAngularVelocity(pinion, steerSpeed);
    }
});

This hybrid approach ensures stable mechanics, eliminates teeth slipping, and enforces precise physical limits on your steering linkage.