@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
220 lines (189 loc) • 7.56 kB
text/typescript
import { MathUtils, Mesh, MeshBasicMaterial, Object3D } from "three";
import { TransformControls } from "three/examples/jsm/controls/TransformControls.js";
import * as params from "../engine/engine_default_parameters.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { Behaviour, GameObject } from "./Component.js";
import { OrbitControls } from "./OrbitControls.js";
import { SyncedTransform } from "./SyncedTransform.js";
/**
* TransformGizmo displays manipulation controls for translating, rotating, and scaling objects in the scene.
* It wraps three.js {@link TransformControls} and provides keyboard shortcuts for changing modes and settings.
* @category Helpers
* @group Components
*/
export class TransformGizmo extends Behaviour {
/**
* When true, this is considered a helper gizmo and will only be shown if showGizmos is enabled in engine parameters.
*/
public isGizmo: boolean = false;
/**
* Specifies the translation grid snap value in world units.
* Applied when holding Shift while translating an object.
*/
public translationSnap: number = 1;
/**
* Specifies the rotation snap angle in degrees.
* Applied when holding Shift while rotating an object.
*/
public rotationSnapAngle: number = 15;
/**
* Specifies the scale snapping value.
* Applied when holding Shift while scaling an object.
*/
public scaleSnap: number = .25;
/**
* Gets the underlying three.js {@link TransformControls} instance.
* @returns The TransformControls instance or undefined if not initialized.
*/
get control() {
return this._control;
}
private _control?: TransformControls;
private orbit?: OrbitControls;
/** @internal */
onEnable() {
if (this.isGizmo && !params.showGizmos) return;
if (!this.context.mainCamera) return;
if (!this._control) {
this._control = new TransformControls(this.context.mainCamera, this.context.renderer.domElement);
this._control.enabled = true;
this._control.getRaycaster().layers.set(2);
this._control.size = 1;
const obj = ("_root" in this._control ? this._control._root : this._control) as Object3D;
obj.traverse(x => {
const mesh = x as Mesh;
mesh.layers.set(2);
if (mesh) {
const gizmoMat = mesh.material as MeshBasicMaterial;
if (gizmoMat) {
gizmoMat.opacity = 0.3;
}
}
});
this.orbit = GameObject.getComponentInParent(this.context.mainCamera, OrbitControls) ?? undefined;
}
if (this._control) {
const obj = this._control.getHelper();
this.context.scene.add(obj);
this._control.attach(this.gameObject);
this._control?.addEventListener('dragging-changed', this.onControlChangedEvent);
window.addEventListener('keydown', this.windowKeyDownListener);
window.addEventListener('keyup', this.windowKeyUpListener);
}
}
/** @internal */
onDisable() {
this._control?.getHelper()?.removeFromParent();
this._control?.removeEventListener('dragging-changed', this.onControlChangedEvent);
window.removeEventListener('keydown', this.windowKeyDownListener);
window.removeEventListener('keyup', this.windowKeyUpListener);
}
/**
* Enables grid snapping for transform operations according to set snap values.
* This applies the translationSnap, rotationSnapAngle, and scaleSnap properties to the controls.
*/
enableSnapping() {
if (this._control) {
this._control.setTranslationSnap(this.translationSnap);
this._control.setRotationSnap(MathUtils.degToRad(this.rotationSnapAngle));
this._control.setScaleSnap(this.scaleSnap);
}
}
/**
* Disables grid snapping for transform operations.
* Removes all snapping constraints from the transform controls.
*/
disableSnapping() {
if (this._control) {
this._control.setTranslationSnap(null);
this._control.setRotationSnap(null);
this._control.setScaleSnap(null);
}
}
/**
* Event handler for when dragging state changes.
* Disables orbit controls during dragging and requests ownership of the transform if it's synchronized.
* @param event The drag change event
*/
private onControlChangedEvent = (event) => {
const orbit = this.orbit;
if (orbit) orbit.enabled = !event.value;
if (event.value) {
// request ownership on drag start
const sync = GameObject.getComponentInParent(this.gameObject, SyncedTransform);
if (sync) {
sync.requestOwnership();
}
}
}
/**
* Handles keyboard shortcuts for transform operations:
* - Q: Toggle local/world space
* - W: Translation mode
* - E: Rotation mode
* - R: Scale mode
* - Shift: Enable snapping (while held)
* - +/-: Adjust gizmo size
* - X/Y/Z: Toggle visibility of respective axis
* - Spacebar: Toggle controls enabled state
* @param event The keyboard event
*/
private windowKeyDownListener = (event) => {
if (!this.enabled) return;
if (!this._control) return;
switch (event.keyCode) {
case 81: // Q
this._control.setSpace(this._control.space === 'local' ? 'world' : 'local');
break;
case 16: // Shift
this.enableSnapping();
break;
case 87: // W
this._control.setMode('translate');
break;
case 69: // E
this._control.setMode('rotate');
break;
case 82: // R
this._control.setMode('scale');
break;
case 187:
case 107: // +, =, num+
this._control.setSize(this._control.size + 0.1);
break;
case 189:
case 109: // -, _, num-
this._control.setSize(Math.max(this._control.size - 0.1, 0.1));
break;
case 88: // X
this._control.showX = !this._control.showX;
break;
case 89: // Y
this._control.showY = !this._control.showY;
break;
case 90: // Z
this._control.showZ = !this._control.showZ;
break;
case 32: // Spacebar
this._control.enabled = !this._control.enabled;
break;
}
}
/**
* Handles keyboard key release events.
* Currently only handles releasing Shift key to disable snapping.
* @param event The keyboard event
*/
private windowKeyUpListener = (event) => {
if (!this.enabled) return;
switch (event.keyCode) {
case 16: // Shift
this.disableSnapping();
break;
}
}
}