How does TransformControlsPlane work?

I’m trying to adapt TransformControls to scale a box by dragging on the faces (see images).
image
image
image

My attempt kind of works, but with some starting positions, the box scales in the wrong direction, or suddenly scales way faster/slower. I am guessing this is due to my TransformControlsPlane being placed wrong.
My question thus is: how does the TransformControlsPlane get aligned in the TransformControls?
In the source code, I can see that it doesn’t just get oriented towards the camera, but some transformations are applied depending on the current axis. Could someone help me with the reasoning behind this, so I can correctly replicate the behavior?

In case everyone ever runs into an issue trying to adjust TransformControlsPlane, here’s what worked for me:

  • Try to visually debug the plane by making it smaller and visible
  • By thinking it through visually, I noticed that I had to transform the plane to be centered on the face intersection point

By applying this simple tranformation, my face scaling now works :slight_smile:

1 Like

Hi @tobiascornille this is something I am trying to achieve as well. Can you share an example code snippet? Thanks!!!

@tobiascornille hi, do you have a code for this?

Here’s the code that updates the (invisible) plane that is used for intersections later. Hope this helps:

import {
  Vector3,
  PlaneGeometry,
  DoubleSide,
  Mesh,
  MeshBasicMaterial,
  Matrix4,
  Scene,
  BoxGeometry,
  PerspectiveCamera,
} from "three";

export const ORIGIN = new Vector3(0, 0, 0);
export const UNIT_X = new Vector3(1, 0, 0);
export const UNIT_Y = new Vector3(0, 1, 0);
export const UNIT_Z = new Vector3(0, 0, 1);
export const UP_VECTOR = UNIT_Z;

const scene = new Scene();
const object = new Mesh(new BoxGeometry(1, 1, 1));
scene.add(object);

const camera = new PerspectiveCamera();

// attributes for extending cuboids
const extendPlane = new Mesh(
  new PlaneGeometry(100, 100, 2, 2),
  new MeshBasicMaterial({
    visible: false,
    side: DoubleSide,
    toneMapped: false,
  })
);
scene.add(extendPlane);

const up = new Vector3();
const target = new Vector3();
const extendPlaneRotationMatrix = new Matrix4();
const eye = new Vector3();

const unitXLocal = new Vector3();
const unitYLocal = new Vector3();
const unitZLocal = new Vector3();

function updateExtendPlane(axis) {
  unitXLocal.copy(UNIT_X).applyQuaternion(object.quaternion);
  unitYLocal.copy(UNIT_Y).applyQuaternion(object.quaternion);
  unitZLocal.copy(UNIT_Z).applyQuaternion(object.quaternion);

  if (camera.type === "PerspectiveCamera") {
    eye.copy(camera.position).sub(object.position).normalize();
    switch (axis) {
      case "x":
        up.copy(eye).cross(unitXLocal);
        target.copy(unitXLocal).cross(up);
        break;
      case "y":
        up.copy(eye).cross(unitYLocal);
        target.copy(unitYLocal).cross(up);
        break;
      case "z":
        up.copy(eye).cross(unitZLocal);
        target.copy(unitZLocal).cross(up);
        break;
    }
  }

  extendPlaneRotationMatrix.lookAt(ORIGIN, target, up);
  extendPlane.setRotationFromMatrix(extendPlaneRotationMatrix);
  extendPlane.updateMatrixWorld();
}

function onFaceIntersection(intersectedFace) {
  let axis;
  if (intersectedFace.faceIndex < 4) {
    axis = "x";
  } else if (intersectedFace.faceIndex < 8) {
    axis = "y";
  } else {
    axis = "z";
  }
  updateExtendPlane(axis);
}