How to Slice a Matter.js Body Along a Line

Slicing a rigid body cleanly into two halves in Matter.js requires geometry processing because the engine does not have a built-in polygon clipping API. The process involves identifying where an arbitrary cutting line intersects the edges of a body's polygon, dividing the original ordered vertices along those intersection points into two separate closed loops, and replacing the original body with two newly instantiated bodies in the physics world while preserving linear and angular momentum.

1. Define the Cutting Line

Represent the cutting line with two distinct 2D points: \(A(x_1, y_1)\) and \(B(x_2, y_2)\). To determine which side of the line a vertex falls on, use the 2D cross product (determinant):

\[\text{side}(P, A, B) = (B.x - A.x)(P.y - A.y) - (B.y - A.y)(P.x - A.x)\]

2. Find Edge Intersections

Iterate through the perimeter edges of the body. An edge connects vertex \(V_i\) to vertex \(V_{i+1}\) (wrapping around to the first vertex). An intersection exists if \(V_i\) and \(V_{i+1}\) have opposite signs when evaluated against the cutting line equation.

Calculate the exact intersection point \(I\) between segment \(V_i V_{i+1}\) and line \(AB\):

function getLineIntersection(p1, p2, p3, p4) {
  const d = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);
  if (d === 0) return null; // Parallel lines

  const ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / d;
  return {
    x: p1.x + ua * (p2.x - p1.x),
    y: p1.y + ua * (p2.y - p1.y)
  };
}

3. Reconstruct the Polygons

To build the two new shapes, traverse the original vertices in sequence and construct two separate vertex arrays (polyA and polyB):

  1. Initialize empty arrays polyA and polyB.
  2. For each vertex \(V_i\):
    • Classify \(V_i\) using \(\text{side}(V_i, A, B)\). If positive, push it to polyA; if negative, push it to polyB.
    • Check if the segment connecting \(V_i\) to \(V_{i+1}\) crosses the cutting line.
    • If a cut occurs, compute intersection point \(I\) and push a copy of \(I\) to both polyA and polyB.
  3. Ensure both resulting arrays have at least 3 vertices. If a cut did not fully pass through two separate edges, the body was not cleanly split.

4. Replace the Original Body in Matter.js

Once two valid sets of vertices are formed:

  1. Calculate Geometric Centers: Matter.js expects body positions at their center of mass. Use Matter.Vertices.centre(vertices) to find the centroid of each new polygon, and offset the vertices relative to this center before creating the bodies.
  2. Create New Bodies: Use Matter.Bodies.fromVertices() to instantiate the two new halves. Note that Matter.js relies on the poly-decomp library internally if the sliced geometry produces concave shapes.
  3. Inherit Physics Properties: Assign the original body's velocity, angular velocity, friction, and restitution to the new bodies so the motion remains continuous.
  4. Update the World: Remove the parent body from the composite world and add the two new halves.
function sliceBody(world, body, lineStart, lineEnd) {
  const vertices = body.vertices;
  const polyA = [];
  const polyB = [];

  for (let i = 0; i < vertices.length; i++) {
    const current = vertices[i];
    const next = vertices[(i + 1) % vertices.length];

    const currentSide = (lineEnd.x - lineStart.x) * (current.y - lineStart.y) - 
                        (lineEnd.y - lineStart.y) * (current.x - lineStart.x);
    const nextSide = (lineEnd.x - lineStart.x) * (next.y - lineStart.y) - 
                     (lineEnd.y - lineStart.y) * (next.x - lineStart.x);

    if (currentSide >= 0) polyA.push({ x: current.x, y: current.y });
    if (currentSide <= 0) polyB.push({ x: current.x, y: current.y });

    if ((currentSide > 0 && nextSide < 0) || (currentSide < 0 && nextSide > 0)) {
      const intersect = getLineIntersection(current, next, lineStart, lineEnd);
      if (intersect) {
        polyA.push({ x: intersect.x, y: intersect.y });
        polyB.push({ x: intersect.x, y: intersect.y });
      }
    }
  }

  if (polyA.length < 3 || polyB.length < 3) return false;

  const centerA = Matter.Vertices.centre(polyA);
  const centerB = Matter.Vertices.centre(polyB);

  const bodyA = Matter.Bodies.fromVertices(centerA.x, centerA.y, polyA, {
    render: body.render
  });
  const bodyB = Matter.Bodies.fromVertices(centerB.x, centerB.y, polyB, {
    render: body.render
  });

  if (!bodyA || !bodyB) return false;

  Matter.Body.setVelocity(bodyA, body.velocity);
  Matter.Body.setVelocity(bodyB, body.velocity);
  Matter.Body.setAngularVelocity(bodyA, body.angularVelocity);
  Matter.Body.setAngularVelocity(bodyB, body.angularVelocity);

  Matter.Composite.remove(world, body);
  Matter.Composite.add(world, [bodyA, bodyB]);

  return true;
}

5. Handling Concave Polygons

If slicing a shape results in concave polygons, ensure poly-decomp.js is installed and registered globally on the window object prior to loading Matter.js:

window.decomp = require('poly-decomp');

Without poly-decomp, Matter.Bodies.fromVertices() falls back to computing the convex hull, which distorts complex sliced shapes.