@ue-too/board
Version:
<h1 align="center"> uē-tôo </h1> <p align="center"> pan, zoom, rotate, and more with your html canvas. </p>
401 lines (400 loc) • 14.9 kB
TypeScript
import { BoardCamera } from "../interface";
/**
* Combined configuration for rotation handler behavior, merging restriction and clamping settings.
*
* @remarks
* This type combines {@link RotationHandlerRestrictConfig} and {@link RotationHandlerClampConfig}
* to provide complete control over camera rotation behavior.
*
* Rotation handlers use this configuration to:
* - Completely disable rotation operations (restriction)
* - Clamp rotation angle to stay within defined angular limits
*
* @category Camera Rig
* @see {@link RotationHandlerRestrictConfig} for rotation locking options
* @see {@link RotationHandlerClampConfig} for angular boundary options
*/
export type RotationHandlerConfig = RotationHandlerRestrictConfig & RotationHandlerClampConfig;
/**
* Configuration for completely disabling rotation operations.
*
* @remarks
* Provides a global "rotation lock" to prevent any rotation changes.
*
* When `restrictRotation` is true:
* - Rotate-to operations return current rotation (no change)
* - Rotate-by operations return zero delta (no change)
*
* This is useful for:
* - Locking rotation during specific application states
* - Fixed-orientation viewing modes (north-up maps, etc.)
* - Preventing user rotation in certain contexts
*
* @example
* ```typescript
* const config: RotationHandlerRestrictConfig = {
* restrictRotation: true // Lock rotation
* };
*
* // Any rotation attempt will be ignored
* ```
*
* @category Camera Rig
*/
export type RotationHandlerRestrictConfig = {
/**
* Whether to completely prevent rotation operations.
*/
restrictRotation: boolean;
};
/**
* Configuration for rotation angle boundary clamping.
*
* @remarks
* Controls whether rotation operations should be constrained to camera's rotation boundaries.
*
* When `clampRotation` is true, rotation handlers enforce {@link BoardCamera.rotationBoundaries}
* limits (min/max angles in radians). When false, rotation can exceed configured boundaries.
*
* Rotation boundaries allow limiting camera rotation to a specific angular range,
* useful for scenarios like:
* - Restricting rotation to ±45 degrees from north
* - Allowing only certain cardinal directions
* - Preventing full 360-degree rotation
*
* @example
* ```typescript
* const config: RotationHandlerClampConfig = {
* clampRotation: true // Enforce rotation boundaries
* };
*
* camera.rotationBoundaries = { min: 0, max: Math.PI / 2 };
* // Rotation clamped to [0, 90 degrees] range
* ```
*
* @category Camera Rig
*/
export type RotationHandlerClampConfig = {
/**
* Whether to enforce rotation angle boundaries.
*/
clampRotation: boolean;
};
/**
* Handler function type for relative "rotate by" camera operations.
*
* @param delta - Rotation angle change in radians (positive = counter-clockwise)
* @param camera - Current camera instance
* @param config - Rotation behavior configuration
* @returns Transformed rotation delta (after applying restrictions and clamping)
*
* @remarks
* Rotate-by handlers process relative rotation change requests. They form a pipeline
* that can apply restrictions, clamping, and other transformations to the delta.
*
* Handler pipeline pattern:
* - Each handler receives the rotation delta, camera state, and config
* - Returns a potentially modified delta
* - Handlers can be chained using {@link createHandlerChain}
*
* Common transformations:
* - Angular boundary clamping (prevent exceeding min/max angles)
* - Rotation locking (return zero delta)
* - Delta dampening or snapping
*
* Rotation angles are in radians where:
* - 0 = North (no rotation)
* - Positive values = Counter-clockwise rotation
* - Negative values = Clockwise rotation
*
* @example
* ```typescript
* const myRotateByHandler: RotateByHandlerFunction = (delta, camera, config) => {
* // Custom logic: snap to 45-degree increments
* const totalRotation = camera.rotation + delta;
* const snapped = Math.round(totalRotation / (Math.PI / 4)) * (Math.PI / 4);
* return snapped - camera.rotation;
* };
* ```
*
* @category Camera Rig
* @see {@link createHandlerChain} for composing handler pipelines
* @see {@link createDefaultRotateByHandler} for the default implementation
*/
export type RotateByHandlerFunction = (delta: number, camera: BoardCamera, config: RotationHandlerConfig) => number;
/**
* Handler function type for absolute "rotate to" camera operations.
*
* @param targetRotation - Target rotation angle in radians
* @param camera - Current camera instance
* @param config - Rotation behavior configuration
* @returns Transformed rotation angle (after applying restrictions and clamping)
*
* @remarks
* Rotate-to handlers process absolute rotation angle requests. They form a pipeline
* that can apply restrictions, clamping, and other transformations.
*
* Handler pipeline pattern:
* - Each handler receives the target angle, camera state, and config
* - Returns a potentially modified angle
* - Handlers can be chained using {@link createHandlerChain}
*
* Common transformations:
* - Angular boundary clamping (enforce min/max angles)
* - Rotation locking (return current angle)
* - Angle snapping or normalization
*
* Rotation angles are in radians where:
* - 0 = North (no rotation)
* - π/2 = West (90° counter-clockwise)
* - π = South (180°)
* - 3π/2 = East (270° counter-clockwise)
*
* @example
* ```typescript
* const myRotateToHandler: RotateToHandlerFunction = (target, camera, config) => {
* // Custom logic: snap to cardinal directions
* const cardinals = [0, Math.PI/2, Math.PI, 3*Math.PI/2];
* return cardinals.reduce((prev, curr) =>
* Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev
* );
* };
* ```
*
* @category Camera Rig
* @see {@link createHandlerChain} for composing handler pipelines
* @see {@link createDefaultRotateToHandler} for the default implementation
*/
export type RotateToHandlerFunction = (targetRotation: number, camera: BoardCamera, config: RotationHandlerConfig) => number;
/**
* Handler pipeline step that clamps "rotate by" deltas to prevent angular boundary violations.
*
* @param delta - Rotation angle change in radians
* @param camera - Current camera instance (provides current rotation and boundaries)
* @param config - Clamping configuration
* @returns Adjusted delta that respects rotation boundaries
*
* @remarks
* This handler ensures that applying the delta won't exceed rotation boundaries.
*
* Algorithm:
* 1. Calculate potential new rotation (current + delta)
* 2. Normalize angle to [0, 2π) range
* 3. Clamp to rotation boundaries
* 4. Calculate shortest angular distance from current to clamped angle
* 5. Return that distance as the new delta
*
* Behavior:
* - If `clampRotation` is false: Returns delta unchanged
* - If `clampRotation` is true: Adjusts delta to stay within boundaries
*
* The resulting delta may be zero if already at a boundary and trying to rotate further.
*
* @example
* ```typescript
* camera.rotation = Math.PI * 0.4; // 72 degrees
* camera.rotationBoundaries = { max: Math.PI / 2 }; // Max 90 degrees
*
* const config: RotationHandlerClampConfig = {
* clampRotation: true
* };
*
* const delta = Math.PI * 0.2; // Try to rotate 36 degrees (would exceed max)
* const clamped = clampRotateByHandler(delta, camera, config);
* // clamped ≈ 0.314 radians (18 degrees - only rotate to boundary)
* ```
*
* @category Camera Rig
* @see {@link normalizeAngleZero2TwoPI} for angle normalization
* @see {@link clampRotation} for boundary clamping
* @see {@link angleSpan} for calculating angular distance
*/
export declare function clampRotateByHandler(delta: number, camera: BoardCamera, config: RotationHandlerClampConfig): number;
/**
* Handler pipeline step that prevents "rotate by" operations when rotation is locked.
*
* @param delta - Rotation angle change in radians
* @param camera - Current camera instance
* @param config - Restriction configuration
* @returns Zero (if locked) or delta (if unlocked)
*
* @remarks
* This handler implements a global rotation lock for relative rotation operations.
*
* Behavior:
* - If `restrictRotation` is true: Returns 0 (prevents any change)
* - If `restrictRotation` is false: Returns delta unchanged
*
* @example
* ```typescript
* const config: RotationHandlerRestrictConfig = {
* restrictRotation: true // Lock rotation
* };
*
* const delta = Math.PI / 4; // Try to rotate 45 degrees
* const result = restrictRotateByHandler(delta, camera, config);
* // result = 0 (rotation locked, no change allowed)
* ```
*
* @category Camera Rig
* @see {@link createDefaultRotateByHandler} for default pipeline usage
*/
export declare function restrictRotateByHandler(delta: number, camera: BoardCamera, config: RotationHandlerRestrictConfig): number;
/**
* Handler pipeline step that clamps "rotate to" targets to camera rotation boundaries.
*
* @param targetRotation - Target rotation angle in radians
* @param camera - Current camera instance (provides rotationBoundaries)
* @param config - Clamping configuration
* @returns Clamped rotation angle
*
* @remarks
* This handler enforces angular limits on absolute rotation requests.
*
* Behavior:
* - If `clampRotation` is false: Returns target unchanged
* - If `clampRotation` is true: Clamps target to {@link BoardCamera.rotationBoundaries}
*
* The clamping handles:
* - Missing boundaries (undefined min/max)
* - One-sided constraints (only min or only max)
* - Full range constraints
*
* @example
* ```typescript
* camera.rotationBoundaries = { min: 0, max: Math.PI }; // [0°, 180°]
*
* const config: RotationHandlerClampConfig = {
* clampRotation: true
* };
*
* const target = Math.PI * 1.5; // 270 degrees (exceeds max)
* const clamped = clampRotateToHandler(target, camera, config);
* // clamped = π (180 degrees - clamped to max boundary)
* ```
*
* @category Camera Rig
* @see {@link clampRotation} for clamping implementation
* @see {@link createDefaultRotateToHandler} for default pipeline usage
*/
export declare function clampRotateToHandler(targetRotation: number, camera: BoardCamera, config: RotationHandlerClampConfig): number;
/**
* Handler pipeline step that prevents "rotate to" operations when rotation is locked.
*
* @param targetRotation - Target rotation angle in radians
* @param camera - Current camera instance
* @param config - Restriction configuration
* @returns Current rotation (if locked) or target (if unlocked)
*
* @remarks
* This handler implements a global rotation lock for absolute rotation operations.
*
* Behavior:
* - If `restrictRotation` is true: Returns current rotation (prevents any change)
* - If `restrictRotation` is false: Returns target unchanged
*
* @example
* ```typescript
* camera.rotation = Math.PI / 2; // Currently at 90 degrees
*
* const config: RotationHandlerRestrictConfig = {
* restrictRotation: true // Lock rotation
* };
*
* const target = Math.PI; // Try to rotate to 180 degrees
* const result = restrictRotateToHandler(target, camera, config);
* // result = π/2 (rotation locked, returns current angle)
* ```
*
* @category Camera Rig
* @see {@link createDefaultRotateToHandler} for default pipeline usage
*/
export declare function restrictRotateToHandler(targetRotation: number, camera: BoardCamera, config: RotationHandlerRestrictConfig): number;
/**
* Creates a default "rotate by" handler pipeline for relative rotation operations.
*
* @returns Rotate-by handler function with restriction and clamping
*
* @remarks
* The default handler pipeline applies transformations in this order:
* 1. **Restriction** ({@link restrictRotateByHandler}): Returns zero delta if locked
* 2. **Clamping** ({@link clampRotateByHandler}): Adjusts delta to respect boundaries
*
* This ensures that:
* - Rotation can be completely disabled via `restrictRotation` flag
* - Resulting rotation angle stays within configured angular boundaries
* - Delta is adjusted to prevent boundary violations
*
* @example
* ```typescript
* const rotateBy = createDefaultRotateByHandler();
*
* camera.rotation = Math.PI * 0.4; // 72 degrees
* camera.rotationBoundaries = { max: Math.PI / 2 }; // Max 90 degrees
*
* const delta = Math.PI * 0.3; // Try to rotate 54 degrees (would exceed max)
* const constrained = rotateBy(delta, camera, {
* clampRotation: true,
* restrictRotation: false
* });
* // constrained adjusted to only rotate to boundary
* camera.setRotation(camera.rotation + constrained);
* ```
*
* @category Camera Rig
* @see {@link createHandlerChain} for creating custom handler pipelines
* @see {@link restrictRotateByHandler} for the restriction step
* @see {@link clampRotateByHandler} for the clamping step
*/
export declare function createDefaultRotateByHandler(): RotateByHandlerFunction;
/**
* Creates a default "rotate to" handler pipeline for absolute rotation operations.
*
* @returns Rotate-to handler function with restriction and clamping
*
* @remarks
* The default handler pipeline applies transformations in this order:
* 1. **Restriction** ({@link restrictRotateToHandler}): Returns current angle if locked
* 2. **Clamping** ({@link clampRotateToHandler}): Clamps angle to configured boundaries
*
* This ensures that:
* - Rotation can be completely disabled via `restrictRotation` flag
* - Rotation angle stays within configured angular boundaries
*
* @example
* ```typescript
* const rotateTo = createDefaultRotateToHandler();
*
* camera.rotationBoundaries = { min: 0, max: Math.PI }; // [0°, 180°]
*
* const target = Math.PI * 1.5; // 270 degrees (exceeds max)
* const constrained = rotateTo(target, camera, {
* clampRotation: true,
* restrictRotation: false
* });
* // constrained = π (clamped to max boundary of 180 degrees)
* camera.setRotation(constrained);
* ```
*
* @example
* ```typescript
* // Create custom pipeline with snapping
* const cardinalRotateTo = createHandlerChain<number, [BoardCamera, RotationHandlerConfig]>(
* restrictRotateToHandler,
* (angle) => {
* // Snap to cardinal directions (0°, 90°, 180°, 270°)
* const cardinals = [0, Math.PI/2, Math.PI, 3*Math.PI/2];
* return cardinals.reduce((prev, curr) =>
* Math.abs(curr - angle) < Math.abs(prev - angle) ? curr : prev
* );
* },
* clampRotateToHandler
* );
* ```
*
* @category Camera Rig
* @see {@link createHandlerChain} for creating custom handler pipelines
* @see {@link restrictRotateToHandler} for the restriction step
* @see {@link clampRotateToHandler} for the clamping step
*/
export declare function createDefaultRotateToHandler(): RotateToHandlerFunction;