@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.
204 lines (203 loc) • 9.81 kB
TypeScript
import { Box3, Object3D, Plane, Vector3 } from "three";
import type { Context } from "../engine/engine_setup.js";
import { GameObject } from "./Component.js";
/**
* Context passed to {@link IDragConstraint.init} at drag start and whenever the drag context
* resets (e.g. transitioning from multi-touch back to single-pointer).
* Provides a snapshot of the dragged object and attachment data a constraint needs
* to initialize or re-initialize its locked reference values.
*/
export interface IDragConstraintContext {
/** The object being dragged. Cast to IGameObject for world-space properties. */
readonly gameObject: Object3D;
/** Drag attachment point in the dragged object's local space. */
readonly hitPointInLocalSpace: Vector3;
/** Surface normal at drag attachment in the dragged object's local space. */
readonly hitNormalInLocalSpace: Vector3;
/**
* Local-space bounding box at scale=1. Non-null only for multi-touch drags
* where scale-aware constraints (e.g. keeping the bounds bottom grounded) are needed.
*/
readonly boundsAtScaleOne: Box3 | null;
/**
* World scale of the object captured at the start of the current drag phase.
* Non-null alongside boundsAtScaleOne for multi-touch; null for single-pointer.
*/
readonly initialWorldScale: Vector3 | null;
}
/**
* Contract for drag constraints applied after a follow object's position is resolved each frame.
* Implement this interface to add custom position/rotation/scale restrictions.
*/
export interface IDragConstraint {
/**
* Called once at drag start and again whenever the drag context resets
* (e.g. multi-touch → single-pointer transition). Capture any object snapshot
* (position, rotation, scale) you need to hold fixed during the drag.
* Constraints that need no dynamic initialization may omit this method.
*/
init?(context: IDragConstraintContext): void;
/** Modifies followObject in-place. Invoked after position is resolved each frame. */
apply(followObject: GameObject): void;
}
/** Snaps the follow object's world position to a uniform grid. Resolution ≤ 0 is a no-op.
* For XZ-plane mode, use {@link GrabPointPlaneConstraint} instead (it handles both
* plane-clamping and optional grid-snapping on the plane). */
export declare class GridSnapConstraint implements IDragConstraint {
snapGridResolution: number;
constructor(snapGridResolution: number);
apply(followObject: GameObject): void;
}
/** Projects the grabbed attachment point back onto a plane after position is resolved,
* with optional grid snapping on that plane.
* Owned by the strategy that requires plane-clamping (e.g. {@link XZPlaneDragStrategy}). */
export declare class GrabPointPlaneConstraint implements IDragConstraint {
/** Plane to project the grabbed point onto. Typically the strategy's fixed drag plane. */
readonly plane: Plane;
/** Attachment point in dragged-object local space. Set once at drag start; the handler
* mutates this Vector3 in-place so updates are reflected automatically. */
hitPointInLocalSpace: Vector3 | null;
/** Grid snap resolution on the plane. 0 = projection only, no snapping. */
snapResolution: number;
constructor(plane: Plane);
init(ctx: IDragConstraintContext): void;
apply(followObject: GameObject): void;
}
/** Locks the follow object's world rotation to the quaternion captured at drag-start. */
export declare class KeepRotationConstraint implements IDragConstraint {
private readonly _savedQuat;
init(ctx: IDragConstraintContext): void;
apply(followObject: GameObject): void;
}
/** Locks the follow object's world scale to the value captured at drag-start. */
export declare class KeepScaleConstraint implements IDragConstraint {
private readonly _savedScale;
init(ctx: IDragConstraintContext): void;
apply(followObject: GameObject): void;
}
/**
* Clamps the world scale of the dragged object to a [min, max] range.
*
* When `relativeToInitialScale` is `true` (default), `min` and `max` are treated as
* multipliers of the object's world scale captured at drag start — for example, `min=0.01`
* means the object cannot shrink below 1% of its original size.
*
* When `relativeToInitialScale` is `false`, `min` and `max` are applied directly as
* absolute world-space scale values to each axis independently.
*
* @example Prevent negative / near-zero scale with default relative mode:
* ```ts
* new ScaleLimitConstraint(0.01, 10000, true, object.worldScale.clone());
* ```
*/
export declare class ScaleLimitConstraint implements IDragConstraint {
min: number;
max: number;
relativeToInitialScale: boolean;
/**
* @param min Lower bound. Relative mode: fraction of initial scale. Raw mode: absolute world-scale per axis.
* @param max Upper bound. Relative mode: fraction of initial scale. Raw mode: absolute world-scale per axis.
* @param relativeToInitialScale When `true`, clamp is relative to `initialWorldScale`. When `false`, each axis is clamped independently.
* @param initialWorldScale Reference to the object's world scale captured at drag start. Required for relative mode.
*/
private readonly _initialWorldScale;
constructor(min: number, max: number, relativeToInitialScale?: boolean);
init(ctx: IDragConstraintContext): void;
apply(followObject: GameObject): void;
}
/**
* Flags controlling which rotation axes are frozen by {@link FixedRotationAxesConstraint}.
* Values can be combined with the bitwise OR operator (`|`).
* @example Freeze X and Z: `RotationAxis.X | RotationAxis.Z`
*/
export declare enum RotationAxis {
X = 1,
Y = 2,
Z = 4
}
/**
* Freezes individual rotation axes (X, Y, and/or Z) of the dragged object.
* The locked axis values are captured once at construction time and restored every frame.
*
* Set {@link useLocalSpace} to `true` to lock axes in the object's local space;
* leave it `false` (default) to lock axes in world space.
*
* @example Lock Y-axis rotation in world space:
* ```ts
* const c = new FixedRotationAxesConstraint(RotationAxis.Y, false);
* // init is called automatically by DragControls, or call manually: c.init(ctx);
* ```
*/
export declare class FixedRotationAxesConstraint implements IDragConstraint {
frozenAxes: RotationAxis;
useLocalSpace: boolean;
private readonly _startEuler;
private readonly _eulerCache;
/**
* @param frozenAxes Bitfield of {@link RotationAxis} values indicating which axes to lock.
* @param useLocalSpace When `true`, axes are locked in the object's local space; otherwise world space.
*/
constructor(frozenAxes: RotationAxis, useLocalSpace?: boolean);
init(ctx: IDragConstraintContext): void;
apply(followObject: GameObject): void;
}
/**
* Constrains a dragged object to rotate only around a fixed world-space axis.
* Uses swing-twist decomposition to extract only the twist component around
* the given axis from the delta rotation since drag start, discarding any
* perpendicular swing that would tilt the object.
*
* This is the correct constraint for XZ-plane dragging when the parent is
* rotated — it restricts the object to yaw around the plane normal regardless
* of how the plane is oriented in world space.
*/
export declare class AxisRotationConstraint implements IDragConstraint {
private readonly _startQuat;
private readonly _axis;
constructor(axis: Vector3);
init(ctx: IDragConstraintContext): void;
apply(followObject: GameObject): void;
}
/** Locks the follow object's signed distance from a plane to a value set at drag start.
* Owned by {@link XZPlaneDragStrategy} to prevent slow Y drift in the side-view fallback path.
* Immune to grab-point projection errors, stale matrixWorld, and epsilon guards.
*
* When {@link boundsBottomSignedDistFromPivot} is non-zero (set by {@link MultiTouchDragHandler}
* for XZPlane scaling), the effective locked height is shifted each frame so the **bottom of the
* object's bounds** stays at the height captured at drag start instead of the pivot. */
export declare class PlaneHeightLockConstraint implements IDragConstraint {
private readonly plane;
private _lockedHeight;
private _boundsBottomSignedDistFromPivot;
/** Current pinch-scale ratio, updated each frame by {@link MultiTouchDragHandler}. */
currentScaleRatio: number;
constructor(plane: Plane);
init(ctx: IDragConstraintContext): void;
apply(followObject: GameObject): void;
}
/**
* Used by {@link SnapToSurfacesDragStrategy} for two-pointer (multi-touch) drags.
* Casts a downward ray each frame from the follow object's world position and adjusts
* it so the dragged object's bounding-box contact face rests on the detected surface.
*
* Only injected into the constraint pipeline for multi-touch; the single-pointer path
* continues to use {@link SnapToSurfacesDragStrategy.update} (pointer ray + drag plane
* intersection) unchanged.
*/
export declare class SnapToSurfaceConstraint implements IDragConstraint {
private readonly _context;
private _gameObject;
private _boundsAtScaleOne;
private readonly _t1;
private readonly _t2;
private readonly _t3;
private readonly _t4;
constructor(_context: Context);
init(ctx: IDragConstraintContext): void;
apply(followObject: GameObject): void;
}
/**
* Runs the constraint pipeline — applies each constraint to followObject in order.
* Shared by {@link DragPointerHandler} and {@link MultiTouchDragHandler}.
*/
export declare function applyFollowObjectConstraints(followObject: GameObject, constraints: readonly IDragConstraint[]): void;