@niuee/board
Version:
<h1 align="center"> board </h1> <p align="center"> board supercharges your html canvas element giving it the capabilities to pan, zoom, rotate, and much more. </p> <p align="center"> <a href="https://www.npmjs.com/package/@niuee/board">
1 lines • 336 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../src/being/interfaces.ts","../src/board-camera/utils/zoom.ts","../node_modules/.pnpm/point2point@0.0.95/node_modules/point2point/esm/index.js","../src/board-camera/utils/coordinate-conversion.ts","../src/board-camera/utils/position.ts","../src/board-camera/utils/rotation.ts","../src/util/handler-pipeline.ts","../src/board-camera/pan/pan-handlers.ts","../src/board-camera/rotation/rotation-handler.ts","../src/board-camera/zoom/zoom-handler.ts","../src/util/observable.ts","../src/camera-update-publisher/camera-update-publisher.ts","../src/board-camera/base-camera.ts","../src/board-camera/board-camera-v2.ts","../src/board-camera/camera-rig.ts","../src/kmt-event-parser/vanilla-kmt-event-parser.ts","../src/touch-event-parser/vanilla-touch-event-parser.ts","../src/boardify/utils/zoomlevel-adjustment.ts","../src/util/ruler.ts","../src/boardify/utils/canvas-position-dimension.ts","../src/boardify/utils/canvas.ts","../src/input-flow-control/pan-control-state-machine.ts","../src/input-flow-control/zoom-control-state-machine.ts","../src/input-flow-control/flow-control-with-animation-and-lock.ts","../src/input-flow-control/simple-relay-flow-control.ts","../src/raw-input-publisher/raw-input-publisher.ts","../src/input-state-machine/kmt-input-state-machine.ts","../src/input-state-machine/touch-input-state-machine.ts","../src/input-state-machine/kmt-input-context.ts","../src/input-state-machine/touch-input-context.ts","../src/boardify/board.ts","../src/drawing-engine/driver.ts","../src/board-camera/alt-camera/alt-camera.ts","../src/drawing-engine/selection-box.ts","../src/boardify/utils/drawing-utils.ts"],"sourcesContent":["export interface BaseContext {\n setup(): void;\n cleanup(): void;\n}\n\ntype NOOP = () => void;\n\nexport const NO_OP: NOOP = ()=>{};\n\n/**\n * @description This is the interface for the state machine. The interface takes in a few generic parameters.\n * \n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * \n * You can probably get by using the TemplateStateMachine class.\n * The naming is that an event would \"happen\" and the state of the state machine would \"handle\" it.\n *\n * @see {@link TemplateStateMachine}\n * @see {@link KmtInputStateMachine}\n * \n * @category being\n */\nexport interface StateMachine<EventPayloadMapping, Context extends BaseContext, States extends string = 'IDLE'> {\n switchTo(state: States): void;\n happens<K extends keyof EventPayloadMapping>(event: K, payload: EventPayloadMapping[K], context: Context): States | undefined;\n setContext(context: Context): void;\n states: Record<States, State<EventPayloadMapping, Context, string extends States ? string : States>>;\n onStateChange(callback: StateChangeCallback<States>): void;\n possibleStates: States[];\n onHappens(callback: (event: keyof EventPayloadMapping, payload: EventPayloadMapping[keyof EventPayloadMapping], context: Context) => void): void;\n}\n\n/**\n * @description This is the type for the callback that is called when the state changes.\n *\n * @category being\n */\nexport type StateChangeCallback<States extends string = 'IDLE'> = (currentState: States, nextState: States) => void;\n\n/**\n * @description This is the interface for the state. The interface takes in a few generic parameters:\n * You can probably get by extending the TemplateState class. \n *\n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * \n * A state's all possible states can be only a subset of the possible states of the state machine. (a state only needs to know what states it can transition to)\n * This allows for a state to be reusable across different state machines.\n *\n * @see {@link TemplateState}\n * \n * @category being\n */\nexport interface State<EventPayloadMapping, Context extends BaseContext, States extends string = 'IDLE'> { \n uponEnter(context: Context): void;\n uponLeave(context: Context): void;\n handles<K extends keyof Partial<EventPayloadMapping>>(event: K, payload: EventPayloadMapping[K], context: Context): States | undefined;\n eventReactions: EventReactions<EventPayloadMapping, Context, States>;\n guards: Guard<Context>;\n eventGuards: Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>>;\n}\n\n/**\n * @description This is the type for the event reactions of a state.\n * \n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * \n * @category being\n */\nexport type EventReactions<EventPayloadMapping, Context extends BaseContext, States extends string> = {\n [K in keyof Partial<EventPayloadMapping>]: { \n action: (context: Context, event: EventPayloadMapping[K]) => void; \n defaultTargetState: States;\n };\n};\n\n/**\n * @description This is the type for the guard evaluation when a state transition is happening.\n * \n * Guard evaluations are evaluated after the state has handled the event with the action.\n * Guard evaluations can be defined in an array and the first guard that evaluates to true will be used to determine the next state.\n * \n * Generic parameters:\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * \n * @category being\n */\nexport type GuardEvaluation<Context extends BaseContext> = (context: Context) => boolean;\n\n/**\n * @description This is the type for the guard of a state.\n * \n * guard is an object that maps a key to a guard evaluation.\n * K is all the possible keys that can be used to evaluate the guard.\n * K is optional but if it is not provided, typescript won't be able to type guard in the EventGuards type.\n * \n * @category being\n */\nexport type Guard<Context extends BaseContext, K extends string = string> = {\n [P in K]: GuardEvaluation<Context>;\n}\n\n/**\n * @description This is a mapping of a guard to a target state.\n * \n * Generic parameters:\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - G: The guard type.\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * \n * You probably don't need to use this type directly.\n * \n * @see {@link TemplateState['eventGuards']}\n * \n * @category being\n */\nexport type GuardMapping<Context extends BaseContext, G, States extends string> = {\n guard: G extends Guard<Context, infer K> ? K : never;\n target: States;\n}\n\n/**\n * @description This is a mapping of a guard to a target state.\n * \n * Generic parameters:\n * - EventPayloadMapping: A mapping of events to their payloads.\n * - States: All of the possible states that the state machine can be in. e.g. a string literal union like \"IDLE\" | \"SELECTING\" | \"PAN\" | \"ZOOM\"\n * - Context: The context of the state machine. (which can be used by each state to do calculations that would persist across states)\n * - T: The guard type.\n * \n * You probably don't need to use this type directly.\n * This is a mapping of an event to a guard evaluation.\n * \n * @see {@link TemplateState['eventGuards']}\n * \n * @category being\n */\nexport type EventGuards<EventPayloadMapping, States extends string, Context extends BaseContext, T extends Guard<Context>> = {\n [K in keyof EventPayloadMapping]: GuardMapping<Context, T, States>[];\n}\n\n/**\n * @description This is the template for the state machine.\n * \n * You can use this class to create a state machine. Usually this is all you need for the state machine. Unless you need extra functionality.\n * To create a state machine, just instantiate this class and pass in the states, initial state and context.\n * \n * @see {@link createKmtInputStateMachine} for an example of how to create a state machine.\n * \n * @category being\n */\nexport class TemplateStateMachine<EventPayloadMapping, Context extends BaseContext, States extends string = 'IDLE'> implements StateMachine<EventPayloadMapping, Context, States> {\n\n protected _currentState: States;\n protected _states: Record<States, State<EventPayloadMapping, Context, States>>;\n protected _context: Context;\n protected _statesArray: States[];\n protected _stateChangeCallbacks: StateChangeCallback<States>[];\n protected _happensCallbacks: ((event: keyof EventPayloadMapping, payload: EventPayloadMapping[keyof EventPayloadMapping], context: Context) => void)[];\n\n constructor(states: Record<States, State<EventPayloadMapping, Context, States>>, initialState: States, context: Context){\n this._states = states;\n this._currentState = initialState;\n this._context = context;\n this._statesArray = Object.keys(states) as States[];\n this._stateChangeCallbacks = [];\n this._happensCallbacks = [];\n }\n\n switchTo(state: States): void {\n this._currentState = state;\n }\n \n happens<K extends keyof EventPayloadMapping>(event: K, payload: EventPayloadMapping[K]): States | undefined {\n this._happensCallbacks.forEach(callback => callback(event, payload, this._context));\n const nextState = this._states[this._currentState].handles(event, payload, this._context);\n if(nextState !== undefined && nextState !== this._currentState){\n const originalState = this._currentState;\n this._states[this._currentState].uponLeave(this._context);\n this.switchTo(nextState);\n this._states[this._currentState].uponEnter(this._context);\n this._stateChangeCallbacks.forEach(callback => callback(originalState, this._currentState));\n }\n return nextState;\n }\n\n onStateChange(callback: StateChangeCallback<States>): void {\n this._stateChangeCallbacks.push(callback);\n }\n\n onHappens(callback: (event: keyof EventPayloadMapping, payload: EventPayloadMapping[keyof EventPayloadMapping], context: Context) => void): void {\n this._happensCallbacks.push(callback);\n }\n\n get currentState(): States {\n return this._currentState;\n }\n\n setContext(context: Context): void {\n this._context = context;\n }\n\n get possibleStates(): States[] {\n return this._statesArray;\n }\n\n get states(): Record<States, State<EventPayloadMapping, Context, States>> {\n return this._states;\n }\n}\n\n/**\n * @description This is the template for the state.\n * \n * This is a base template that you can extend to create a state.\n * Unlike the TemplateStateMachine, this class is abstract. You need to implement the specific methods that you need.\n * The core part off the state is the event reactions in which you would define how to handle each event in a state.\n * You can define an eventReactions object that maps only the events that you need. If this state does not need to handle a specific event, you can just not define it in the eventReactions object.\n * \n * @category being\n */\nexport abstract class TemplateState<EventPayloadMapping, Context extends BaseContext, States extends string = 'IDLE'> implements State<EventPayloadMapping, Context, States> {\n\n abstract eventReactions: EventReactions<EventPayloadMapping, Context, States>;\n protected _guards: Guard<Context> = {};\n protected _eventGuards: Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>> = {};\n\n get guards(): Guard<Context> {\n return this._guards;\n }\n\n get eventGuards(): Partial<EventGuards<EventPayloadMapping, States, Context, Guard<Context>>> {\n return this._eventGuards;\n }\n\n uponEnter(context: Context): void {\n // console.log(\"enter\");\n }\n\n uponLeave(context: Context): void {\n // console.log('leave');\n }\n\n handles<K extends keyof EventPayloadMapping>(event: K, payload: EventPayloadMapping[K], context: Context): States | undefined {\n if (this.eventReactions[event]) {\n this.eventReactions[event].action(context, payload);\n const targetState = this.eventReactions[event].defaultTargetState;\n const guardToEvaluate = this._eventGuards[event];\n if(guardToEvaluate){\n const target = guardToEvaluate.find((guard)=>{\n if(this.guards[guard.guard]){\n return this.guards[guard.guard](context);\n }\n return false;\n });\n return target ? target.target : targetState;\n }\n return targetState;\n }\n return undefined;\n }\n}\n","/**\n * @description The limits of the zoom level.\n * \n * @category Camera\n */\nexport type ZoomLevelLimits = {min?: number, max?: number};\n\n/**\n * @description Checks if the zoom level limits are valid.\n */\nexport function isValidZoomLevelLimits(zoomLevelLimits: ZoomLevelLimits | undefined): boolean{\n if(zoomLevelLimits === undefined){\n return true;\n }\n if(zoomLevelLimits.min !== undefined && zoomLevelLimits.max !== undefined && zoomLevelLimits.min > zoomLevelLimits.max){\n return false;\n }\n return true;\n}\n\n/**\n * @description Clamps the zoom level within the limits.\n * \n * @category Camera\n */\nexport function clampZoomLevel(zoomLevel: number, zoomLevelLimits?: ZoomLevelLimits): number{\n if(zoomLevelWithinLimits(zoomLevel, zoomLevelLimits) || zoomLevelLimits === undefined){\n return zoomLevel;\n }\n if(zoomLevelLimits.max){\n zoomLevel = Math.min(zoomLevelLimits.max, zoomLevel);\n }\n if(zoomLevelLimits.min){\n zoomLevel = Math.max(zoomLevelLimits.min, zoomLevel);\n }\n return zoomLevel;\n}\n\n/**\n * @description Checks if the zoom level is within the limits.\n * \n * @category Camera\n */\nexport function zoomLevelWithinLimits(zoomLevel: number, zoomLevelLimits?: ZoomLevelLimits): boolean{\n if(zoomLevelLimits === undefined){\n return true;\n }\n if(zoomLevel <= 0 || (zoomLevelLimits !== undefined && \n ((zoomLevelLimits.max !== undefined && zoomLevelLimits.max < zoomLevel) || \n (zoomLevelLimits.min !== undefined && zoomLevelLimits.min > zoomLevel)\n ))){\n return false;\n }\n return true;\n}\n","class PointCal{static addVector(a,b){return null==a.z&&null==b.z?{x:a.x+b.x,y:a.y+b.y}:(null!=a.z&&null!=b.z||(null==a.z&&(a.z=0),null==b.z&&(b.z=0)),{x:a.x+b.x,y:a.y+b.y,z:a.z+b.z})}static subVector(a,b){return null==a.z&&null==b.z?{x:a.x-b.x,y:a.y-b.y}:(null!=a.z&&null!=b.z||(null==a.z&&(a.z=0),null==b.z&&(b.z=0)),{x:a.x-b.x,y:a.y-b.y,z:a.z-b.z})}static multiplyVectorByScalar(a,b){return null==a.z?{x:a.x*b,y:a.y*b}:{x:a.x*b,y:a.y*b,z:a.z*b}}static divideVectorByScalar(a,b){return 0==b?{x:a.x,y:a.y}:null==a.z?{x:a.x/b,y:a.y/b}:{x:a.x/b,y:a.y/b,z:a.z/b}}static magnitude(a){return null==a.z&&(a.z=0),Math.sqrt(a.x*a.x+a.y*a.y+a.z*a.z)}static unitVector(a){return null==a.z&&(a.z=0),0!=this.magnitude(a)?{x:a.x/this.magnitude(a),y:a.y/this.magnitude(a),z:a.z/this.magnitude(a)}:{x:0,y:0,z:0}}static dotProduct(a,b){return null==a.z&&null==b.z?a.x*b.x+a.y*b.y:(null!=a.z&&null!=b.z||(null==a.z&&(a.z=0),null==b.z&&(b.z=0)),a.x*b.x+a.y*b.y+a.z*b.z)}static crossProduct(a,b){return null!=a.z&&null!=b.z||(null==a.z&&(a.z=0),null==b.z&&(b.z=0)),{x:a.y*b.z-a.z*b.y,y:a.z*b.x-a.x*b.z,z:a.x*b.y-a.y*b.x}}static unitVectorFromA2B(a,b){return this.unitVector(this.subVector(b,a))}static rotatePoint(point,angle){return{x:point.x*Math.cos(angle)-point.y*Math.sin(angle),y:point.x*Math.sin(angle)+point.y*Math.cos(angle)}}static transform2NewAxis(point,angleFromOriginalAxis2DestAxis){return{x:point.x*Math.cos(angleFromOriginalAxis2DestAxis)+point.y*Math.sin(angleFromOriginalAxis2DestAxis),y:-point.x*Math.sin(angleFromOriginalAxis2DestAxis)+point.y*Math.cos(angleFromOriginalAxis2DestAxis)}}static angleFromA2B(a,b){return Math.atan2(a.x*b.y-a.y*b.x,a.x*b.x+a.y*b.y)}static transformPointWRTAnchor(point,anchor,angle){let newPoint=this.rotatePoint(this.subVector(point,anchor),angle);return this.addVector(newPoint,anchor)}static distanceBetweenPoints(a,b){return this.magnitude(this.subVector(a,b))}static flipYAxis(point){return{x:point.x,y:-point.y,z:point.z}}static linearInterpolation(a,b,t){return null==a.z||null==b.z?{x:a.x+(b.x-a.x)*t,y:a.y+(b.y-a.y)*t}:{x:a.x+(b.x-a.x)*t,y:a.y+(b.y-a.y)*t,z:a.z+(b.z-a.z)*t}}static isEqual(a,b){return null==a.z&&(a.z=0),null==b.z&&(b.z=0),a.x==b.x&&a.y==b.y&&a.z==b.z}static getLineIntersection(startPoint,endPoint,startPoint2,endPoint2){const numerator=(endPoint2.x-startPoint2.x)*(startPoint.y-startPoint2.y)-(endPoint2.y-startPoint2.y)*(startPoint.x-startPoint2.x),denominator=(endPoint2.y-startPoint2.y)*(endPoint.x-startPoint.x)-(endPoint2.x-startPoint2.x)*(endPoint.y-startPoint.y);if(0===denominator)return{intersects:!1};const t=numerator/denominator;return t>=0&&t<=1?{intersects:!0,intersection:PointCal.linearInterpolation(startPoint,endPoint,t),offset:t}:{intersects:!1}}}export{PointCal};\n//# sourceMappingURL=index.js.map\n","import { Point } from \"src/util/misc\";\nimport { PointCal } from \"point2point\";\n\n/**\n * @description Finds the world space coordinate of the interest point if the camera is at target position.\n * The target position is the \"would be\" position of the camera in world space.\n * The interest point is the point in view port space where the \"bottom left\" corner is the origin.\n * \n * @category Camera\n */\nexport function convert2WorldSpaceWRT(targetPosition: Point, interestPoint: Point, viewPortWidth: number, viewPortHeight: number, cameraZoomLevel: number, cameraRotation: number): Point{\n let cameraFrameCenter = {x: viewPortWidth / 2, y: viewPortHeight / 2};\n let delta2Point = PointCal.subVector(interestPoint, cameraFrameCenter);\n delta2Point = PointCal.multiplyVectorByScalar(delta2Point, 1 / cameraZoomLevel);\n delta2Point = PointCal.rotatePoint(delta2Point, cameraRotation);\n return PointCal.addVector(targetPosition, delta2Point);\n}\n\n/**\n * @description Converts the point to world space.\n * The point is in the viewport space where the \"bottom left\" corner is the origin.\n * Camera position is the position of the camera in world space.\n * \n * @category Camera\n */\nexport function convert2WorldSpace(point: Point, viewPortWidth: number, viewPortHeight: number, cameraPosition: Point, cameraZoomLevel: number, cameraRotation: number): Point{\n let cameraFrameCenter = {x: viewPortWidth / 2, y: viewPortHeight / 2};\n let delta2Point = PointCal.subVector(point, cameraFrameCenter);\n delta2Point = PointCal.multiplyVectorByScalar(delta2Point, 1 / cameraZoomLevel);\n delta2Point = PointCal.rotatePoint(delta2Point, cameraRotation);\n return PointCal.addVector(cameraPosition, delta2Point);\n}\n\n/**\n * @description Converts the point to world space.\n * The point is in the viewport space where the origin is at the center of the viewport.\n * Camera position is the position of the camera in world space.\n * \n * @category Camera\n */\nexport function convert2WorldSpaceAnchorAtCenter(point: Point, cameraPosition: Point, cameraZoomLevel: number, cameraRotation: number): Point{\n const scaledBack = PointCal.multiplyVectorByScalar(point, 1 / cameraZoomLevel);\n const rotatedBack = PointCal.rotatePoint(scaledBack, cameraRotation);\n const withOffset = PointCal.addVector(rotatedBack, cameraPosition);\n return withOffset;\n}\n\n/**\n * @description Converts a point in \"stage/context/world\" space to view port space.\n * The origin of the viewport is at the center of the viewport.\n * The point is in world space.\n * The camera position is the position of the camera in world space.\n * \n * @category Camera\n */\nexport function convert2ViewPortSpaceAnchorAtCenter(point: Point, cameraPosition: Point, cameraZoomLevel: number, cameraRotation: number): Point{\n const withOffset = PointCal.subVector(point, cameraPosition);\n const scaled = PointCal.multiplyVectorByScalar(withOffset, cameraZoomLevel);\n const rotated = PointCal.rotatePoint(scaled, -cameraRotation);\n return rotated;\n}\n\n/**\n * @description Converts a point in \"stage/context/world\" space to view port space.\n * The origin of the view port is at the bottom left corner.\n * The point is in world space.\n * The camera position is the position of the camera in world space.\n * \n * @category Camera\n */\nexport function invertFromWorldSpace(point: Point, viewPortWidth: number, viewPortHeight: number, cameraPosition: Point, cameraZoomLevel: number, cameraRotation: number): Point{\n let cameraFrameCenter = {x: viewPortWidth / 2, y: viewPortHeight / 2};\n let delta2Point = PointCal.subVector(point, cameraPosition);\n delta2Point = PointCal.rotatePoint(delta2Point, -cameraRotation);\n delta2Point = PointCal.multiplyVectorByScalar(delta2Point, cameraZoomLevel);\n return PointCal.addVector(cameraFrameCenter, delta2Point);\n}\n\n/**\n * @description Checks if a point is in the view port.\n * The point is in world space.\n * The camera position is the position of the camera in world space.\n * \n * @category Camera\n */\nexport function pointIsInViewPort(point: Point, viewPortWidth: number, viewPortHeight: number, cameraPosition: Point, cameraZoomLevel: number, cameraRotation: number): boolean{\n const pointInCameraFrame = invertFromWorldSpace(point, viewPortWidth, viewPortHeight, cameraPosition, cameraZoomLevel, cameraRotation);\n if(pointInCameraFrame.x < 0 || pointInCameraFrame.x > viewPortWidth || pointInCameraFrame.y < 0 || pointInCameraFrame.y > viewPortHeight){\n return false;\n }\n return true;\n}\n\n/**\n * @description Converts a delta in view port space to world space.\n * The delta is in view port space.\n * \n * @category Camera\n */\nexport function convertDeltaInViewPortToWorldSpace(delta: Point, cameraZoomLevel: number, cameraRotation: number): Point{\n return PointCal.multiplyVectorByScalar(PointCal.rotatePoint(delta, cameraRotation), 1 / cameraZoomLevel);\n}\n\n/**\n * @description Converts a delta in world space to view port space.\n * The delta is in world space.\n * \n * @category Camera\n */\nexport function convertDeltaInWorldToViewPortSpace(delta: Point, cameraZoomLevel: number, cameraRotation: number): Point{\n return PointCal.multiplyVectorByScalar(PointCal.rotatePoint(delta, -cameraRotation), cameraZoomLevel);\n}\n\n/**\n * @description Calculates the camera position to get a point in \"stage/context/world\" space to be at a certain point in view port space.\n * This is useful to coordinate camera pan and zoom at the same time.\n * \n * @category Camera\n */\nexport function cameraPositionToGet(pointInWorld: Point, toPointInViewPort: Point, cameraZoomLevel: number, cameraRotation: number): Point {\n const scaled = PointCal.multiplyVectorByScalar(toPointInViewPort, 1 / cameraZoomLevel);\n const rotated = PointCal.rotatePoint(scaled, cameraRotation);\n return PointCal.subVector(pointInWorld, rotated);\n}\n","import { Point } from \"src/util/misc\";\nimport { PointCal } from \"point2point\";\n\nimport { convert2WorldSpaceWRT } from \"./coordinate-conversion\";\n\n/**\n * @description The boundaries of a camera.\n * The x and y are in world space.\n * \n * @category Camera\n */\nexport type Boundaries = {\n min?: {x?: number, y?: number};\n max?: {x?: number, y?: number};\n}\n\n/**\n * @description Checks if a point is within the boundaries.\n * \n * @category Camera\n */\nexport function withinBoundaries(point: Point, boundaries: Boundaries | undefined): boolean{\n if(boundaries == undefined){\n // no boundaries \n return true;\n }\n let leftSide = false;\n let rightSide = false;\n let topSide = false;\n let bottomSide = false;\n // check within boundaries horizontally\n if(boundaries.max == undefined || boundaries.max.x == undefined || point.x <= boundaries.max.x){\n rightSide = true;\n }\n if(boundaries.min == undefined || boundaries.min.x == undefined || point.x >= boundaries.min.x){\n leftSide = true;\n }\n if(boundaries.max == undefined || boundaries.max.y == undefined || point.y <= boundaries.max.y){\n topSide = true;\n }\n if(boundaries.min == undefined || boundaries.min.y == undefined || point.y >= boundaries.min.y){\n bottomSide = true;\n }\n return leftSide && rightSide && topSide && bottomSide;\n}\n\n/**\n * @description Checks if the boundaries are valid.\n * \n * @category Camera\n */\nexport function isValidBoundaries(boundaries: Boundaries | undefined): boolean{\n if(boundaries == undefined){\n return true;\n }\n const minX = boundaries.min?.x;\n const maxX = boundaries.max?.x;\n if (minX != undefined && maxX != undefined && minX >= maxX){\n return false;\n }\n const minY = boundaries.min?.y;\n const maxY = boundaries.max?.y;\n if (minY != undefined && maxY != undefined && minY >= maxY){\n return false;\n }\n return true;\n}\n\n/**\n * @description Checks if the boundaries are fully defined.\n * \n * @category Camera\n */\nexport function boundariesFullyDefined(boundaries: Boundaries | undefined): boolean{\n if(boundaries == undefined){\n return false;\n }\n if(boundaries.max == undefined || boundaries.min == undefined){\n return false;\n }\n if(boundaries.max.x == undefined || boundaries.max.y == undefined || boundaries.min.x == undefined || boundaries.min.y == undefined){\n return false;\n }\n return true;\n}\n\n/**\n * @description Clamps a point to the boundaries.\n * \n * @category Camera\n */\nexport function clampPoint(point: Point, boundaries: Boundaries | undefined): Point{\n if(withinBoundaries(point, boundaries) || boundaries == undefined){\n return point;\n }\n let manipulatePoint = {x: point.x, y: point.y};\n let limit = boundaries.min;\n if (limit != undefined){\n if(limit.x != undefined){\n manipulatePoint.x = Math.max(manipulatePoint.x, limit.x);\n }\n if(limit.y != undefined){\n manipulatePoint.y = Math.max(manipulatePoint.y, limit.y);\n }\n }\n limit = boundaries.max;\n if(limit != undefined){\n if(limit.x != undefined){\n manipulatePoint.x = Math.min(manipulatePoint.x, limit.x);\n }\n if(limit.y != undefined){\n manipulatePoint.y = Math.min(manipulatePoint.y, limit.y);\n }\n }\n return manipulatePoint;\n}\n\n/**\n * @description Gets the translation width of the boundaries.\n * \n * @category Camera\n */\nexport function translationWidthOf(boundaries: Boundaries | undefined): number | undefined{\n if(boundaries == undefined || boundaries.min == undefined || boundaries.max == undefined || boundaries.min.x == undefined || boundaries.max.x == undefined){\n return undefined;\n }\n return boundaries.max.x - boundaries.min.x;\n}\n\n/**\n * @description Gets the half translation width of the boundaries.\n * \n * @category Camera\n */\nexport function halfTranslationWidthOf(boundaries: Boundaries): number | undefined{\n const translationWidth = translationWidthOf(boundaries);\n return translationWidth != undefined ? translationWidth / 2 : undefined;\n}\n\n/**\n * @description Gets the translation height of the boundaries.\n * \n * @category Camera\n */\nexport function translationHeightOf(boundaries: Boundaries | undefined): number | undefined{\n if(boundaries == undefined || boundaries.min == undefined || boundaries.max == undefined || boundaries.min.y == undefined || boundaries.max.y == undefined){\n return undefined;\n }\n return boundaries.max.y - boundaries.min.y;\n}\n\n/**\n * @description Gets the half translation height of the boundaries.\n * \n * @category Camera\n */\nexport function halfTranslationHeightOf(boundaries: Boundaries): number | undefined{\n const translationHeight = translationHeightOf(boundaries);\n return translationHeight != undefined ? translationHeight / 2 : undefined;\n}\n\n/**\n * @description Clamps the entire viewport within the boundaries\n * \n * @category Camera\n */\nexport function clampPointEntireViewPort(point: Point, viewPortWidth: number, viewPortHeight: number, boundaries: Boundaries | undefined, cameraZoomLevel: number, cameraRotation: number): Point{\n if(boundaries == undefined){\n return point;\n }\n let topLeftCorner = convert2WorldSpaceWRT(point, {x: 0, y: viewPortHeight}, viewPortWidth, viewPortHeight, cameraZoomLevel, cameraRotation);\n let bottomLeftCorner = convert2WorldSpaceWRT(point, {x: 0, y: 0}, viewPortWidth, viewPortHeight, cameraZoomLevel, cameraRotation);\n let topRightCorner = convert2WorldSpaceWRT(point, {x: viewPortWidth, y: viewPortHeight}, viewPortWidth, viewPortHeight, cameraZoomLevel, cameraRotation);\n let bottomRightCorner = convert2WorldSpaceWRT(point, {x: viewPortWidth, y: 0}, viewPortWidth, viewPortHeight, cameraZoomLevel, cameraRotation);\n let topLeftCornerClamped = clampPoint(topLeftCorner, boundaries);\n let topRightCornerClamped = clampPoint(topRightCorner, boundaries);\n let bottomLeftCornerClamped = clampPoint(bottomLeftCorner, boundaries);\n let bottomRightCornerClamped = clampPoint(bottomRightCorner, boundaries);\n let topLeftCornerDiff = PointCal.subVector(topLeftCornerClamped, topLeftCorner);\n let topRightCornerDiff = PointCal.subVector(topRightCornerClamped, topRightCorner);\n let bottomLeftCornerDiff = PointCal.subVector(bottomLeftCornerClamped, bottomLeftCorner);\n let bottomRightCornerDiff = PointCal.subVector(bottomRightCornerClamped, bottomRightCorner);\n let diffs = [topLeftCornerDiff, topRightCornerDiff, bottomLeftCornerDiff, bottomRightCornerDiff];\n let maxXDiff = Math.abs(diffs[0].x);\n let maxYDiff = Math.abs(diffs[0].y);\n let delta = diffs[0];\n diffs.forEach((diff)=>{\n if(Math.abs(diff.x) > maxXDiff){\n maxXDiff = Math.abs(diff.x);\n delta.x = diff.x;\n }\n if(Math.abs(diff.y) > maxYDiff){\n maxYDiff = Math.abs(diff.y);\n delta.y = diff.y;\n }\n });\n return PointCal.addVector(point, delta);\n}\n","/**\n * @description The limits of the rotation.\n * \n * @category Camera\n */\nexport type RotationLimits = {start: number, end: number, ccw: boolean, startAsTieBreaker: boolean};\n\n/**\n * @description The boundary of the rotation. (experimental)\n * \n * @category Camera\n */\nexport type RotationBoundary = {start: number, end: number, positiveDirection: boolean, startAsTieBreaker: boolean};\n\n/**\n * @description Clamps the rotation within the limits.\n * \n * @category Camera\n */\nexport function clampRotation(rotation: number, rotationLimits?: RotationLimits): number{\n if(rotationWithinLimits(rotation, rotationLimits) || rotationLimits === undefined){\n return rotation;\n }\n rotation = normalizeAngleZero2TwoPI(rotation);\n const angleSpanFromStart = angleSpan(rotationLimits.start, rotation);\n const angleSpanFromEnd = angleSpan(rotationLimits.end, rotation);\n if((rotationLimits.ccw && (angleSpanFromStart < 0 || angleSpanFromEnd > 0)) || (!rotationLimits.ccw && (angleSpanFromStart > 0 || angleSpanFromEnd < 0))){\n // ccw out of bounds\n if(Math.abs(angleSpanFromStart) === Math.abs(angleSpanFromEnd)){\n // console.log(\"tie\", \"start:\", rotationLimits.start, \"end:\", rotationLimits.end, \"rotation:\", rotation);\n return rotationLimits.startAsTieBreaker ? rotationLimits.start : rotationLimits.end;\n }\n const closerToStart = Math.abs(angleSpanFromStart) < Math.abs(angleSpanFromEnd);\n return closerToStart ? rotationLimits.start : rotationLimits.end;\n }\n return rotation;\n}\n\n/**\n * @description Checks if the rotation is within the limits.\n * \n * @category Camera\n */\nexport function rotationWithinLimits(rotation: number, rotationLimits?: RotationLimits): boolean{\n if(rotationLimits === undefined){\n return true;\n }\n if(normalizeAngleZero2TwoPI(rotationLimits.start) === normalizeAngleZero2TwoPI(rotationLimits.end)){\n return true;\n }\n const normalizedRotation = normalizeAngleZero2TwoPI(rotation);\n const angleSpanFromStart = angleSpan(rotationLimits.start, normalizedRotation);\n const angleSpanFromEnd = angleSpan(rotationLimits.end, normalizedRotation);\n if((rotationLimits.ccw && (angleSpanFromStart < 0 || angleSpanFromEnd > 0)) || (!rotationLimits.ccw && (angleSpanFromStart > 0 || angleSpanFromEnd < 0))){\n return false;\n }\n return true;\n}\n\n/**\n * @description Checks if the rotation is within the boundary. (experimental)\n * \n * @category Camera\n */\nexport function rotationWithinBoundary(rotation: number, rotationBoundary: RotationBoundary): boolean {\n const normalizedRotation = normalizeAngleZero2TwoPI(rotation);\n\n let angleFromStart = normalizedRotation - normalizeAngleZero2TwoPI(rotationBoundary.start);\n if (angleFromStart < 0){\n angleFromStart += (Math.PI * 2);\n }\n if (!rotationBoundary.positiveDirection && angleFromStart > 0){\n angleFromStart = Math.PI * 2 - angleFromStart;\n }\n\n let angleRange = normalizeAngleZero2TwoPI(rotationBoundary.end) - normalizeAngleZero2TwoPI(rotationBoundary.start);\n if(angleRange < 0){\n angleRange += (Math.PI * 2);\n }\n if(!rotationBoundary.positiveDirection && angleRange > 0){\n angleRange = Math.PI * 2 - angleRange;\n }\n\n return angleRange >= angleFromStart;\n}\n\n/**\n * @description Normalizes the angle to be between 0 and 2π.\n * \n * @category Camera\n */\nexport function normalizeAngleZero2TwoPI(angle: number){\n // reduce the angle \n angle = angle % (Math.PI * 2);\n\n // force it to be the positive remainder, so that 0 <= angle < 2 * Math.PI \n angle = (angle + Math.PI * 2) % (Math.PI * 2); \n return angle;\n}\n\n/**\n * @description Gets the smaller angle span between two angles. (in radians)\n * \n * @category Camera\n */\nexport function angleSpan(from: number, to: number): number{\n // in radians\n from = normalizeAngleZero2TwoPI(from);\n to = normalizeAngleZero2TwoPI(to);\n let angleDiff = to - from;\n \n if(angleDiff > Math.PI){\n angleDiff = - (Math.PI * 2 - angleDiff);\n }\n\n if(angleDiff < -Math.PI){\n angleDiff += (Math.PI * 2);\n }\n return angleDiff;\n}\n\n/**\n * @description Converts degrees to radians.\n * \n * @category Camera\n */\nexport function deg2rad(deg: number): number{\n return deg * Math.PI / 180;\n}\n\n/**\n * @description Converts radians to degrees.\n * \n * @category Camera\n */\nexport function rad2deg(rad: number): number{\n return rad * 180 / Math.PI;\n}\n","/**\n * @description Type definition for a handler function that takes a generic value and additional arguments\n * The handler must return the same type as its first argument\n * This is a utility type to be used in the handler pipeline. (Probably don't need to use this directly)\n * Using the {@link createHandlerChain} function to create a handler chain would have typescript infer the correct type for the handler chain.\n * \n * @category Utils\n */\nexport type Handler<T, Args extends any[]> = (value: T, ...args: Args) => T;\n\n/**\n * @description Creates a handler chain from an array of handlers.\n * \n * Use it like this:\n * ```typescript\n * const handlerChain = createHandlerChain(handler1, handler2, handler3);\n * ```\n * or like this:\n * ```typescript\n * const handlers = [handler1, handler2, handler3];\n * const handlerChain = createHandlerChain(handlers);\n * ```\n * \n * The function signature of all the handlers must be the same.\n * \n * @param handlers Array of handler functions to be chained\n * @returns A single handler function that executes all handlers in sequence\n * \n * @category Utils\n */\nexport function createHandlerChain<T, Args extends any[]>(\n ...handlers: Handler<T, Args>[] | [Handler<T, Args>[]]\n): Handler<T, Args> {\n const normalizedHandlers = Array.isArray(handlers[0]) ? handlers[0] : handlers as Handler<T, Args>[];\n return (value: T, ...args: Args): T => {\n return normalizedHandlers.reduce(\n (acc, handler) => handler(acc, ...args),\n value\n );\n };\n}\n","import { PointCal } from \"point2point\";\nimport type { Point } from \"src/util/misc\";\nimport { BoardCamera } from \"src/board-camera/interface\";\nimport { createHandlerChain } from \"src/util/handler-pipeline\";\nimport { clampPoint, clampPointEntireViewPort } from \"src/board-camera/utils/position\";\n\n/**\n * @description Configuration for the pan handler functions.\n * \n * @category Camera\n */\nexport type PanHandlerConfig = PanHandlerRestrictionConfig & PanHandlerClampConfig;\n\nexport type PanHandlerClampConfig = {\n /**\n * @description Whether to limit the pan to the entire view port.\n */\n limitEntireViewPort: boolean;\n /**\n * @description Whether to clamp the translation.\n */\n clampTranslation: boolean;\n};\n\nexport type PanHandlerRestrictionConfig = {\n /**\n * @description Whether to restrict the x translation.\n */\n restrictXTranslation: boolean;\n /**\n * @description Whether to restrict the y translation.\n */\n restrictYTranslation: boolean;\n /**\n * @description Whether to restrict the relative x translation. (because the camera can be rotated, the relative x translation is the horizontal direction of what the user sees on the screen)\n */\n restrictRelativeXTranslation: boolean;\n /**\n * @description Whether to restrict the relative y translation. (because the camera can be rotated, the relative y translation is the vertical direction of what the user sees on the screen)\n */\n restrictRelativeYTranslation: boolean;\n};\n\n/**\n * @description Function Type that is used to define the \"pan to\" handler.\n * The destination is in \"stage/context/world\" space.\n * This is structured as a handler pipeline. \n * @see {@link createHandlerChain}\n * @category Camera\n */\nexport type PanToHandlerFunction = (destination: Point, camera: BoardCamera, config: PanHandlerConfig) => Point;\n/**\n * @description Function Type that is used to define the \"pan by\" handler.\n * The delta is in \"stage/context/world\" space.\n * This is structured as a handler pipeline. \n * @see {@link createHandlerChain}\n * @category Camera\n */\nexport type PanByHandlerFunction = (delta: Point, camera: BoardCamera, config: PanHandlerConfig) => Point;\n\n/**\n * @description Helper function that creates a default \"pan to\" handler.\n * The default pan to handler will first restrict the pan to the view port, then clamp the pan to the boundaries, and then pan to the destination.\n * \n * @see {@link createHandlerChain} to create your own custom pan handler pipeline. (you can also use this function as a part of your own custom pan handler pipeline)\n * @category Camera\n */\nexport function createDefaultPanToHandler(): PanToHandlerFunction {\n return createHandlerChain<Point, [BoardCamera, PanHandlerConfig]>(\n restrictPanToHandler,\n clampToHandler,\n );\n}\n\n/**\n * @description Helper function that creates a default \"pan by\" handler.\n * The resulting pan by handler takes in a delta that is in \"stage/context/world\" space.\n * The default pan by handler will first restrict the pan by the view port, then clamp the pan by the boundaries, and then pan by the delta.\n * \n * @see {@link createHandlerChain} to create your own custom pan handler pipeline. (you can also use this function as a part of your own custom pan handler pipeline)\n * @category Camera\n */\nexport function createDefaultPanByHandler(): PanByHandlerFunction {\n return createHandlerChain<Point, [BoardCamera, PanHandlerConfig]>(\n restrictPanByHandler,\n clampByHandler,\n );\n}\n\n/**\n * @description Function that is part of the \"pan to\" handler pipeline. It restricts the \"pan to\" destination to within a single axis based on the config. (relative to the current camera position)\n * You can use this function standalone to restrict the \"pan to\" destination to within a single axis based on the config. \n * But it is recommended to use this kind of function as part of the pan handler pipeline. (to include this function in your own custom pan handler pipeline)\n * \n * @category Camera\n */\nexport function restrictPanToHandler(destination: Point, camera: BoardCamera, config: PanHandlerRestrictionConfig): Point {\n let delta = PointCal.subVector(destination, camera.position);\n delta = convertDeltaToComplyWithRestriction(delta, camera, config);\n if (delta.x === 0 && delta.y === 0) {\n return destination;\n }\n const dest = PointCal.addVector(camera.position, delta);\n return dest;\n}\n\n/**\n * @description Function that is part of the \"pan by\" handler pipeline. It restricts the pan delta to within a single axis based on the config. (relative to the current camera position)\n * You can use this function standalone to restrict the pan delta to within a single axis based on the config. \n * But it is recommended to use this kind of function as part of the pan handler pipeline. (to include this function in your own custom pan handler pipeline)\n * \n * @category Camera\n */\nexport function restrictPanByHandler(delta: Point, camera: BoardCamera, config: PanHandlerRestrictionConfig): Point {\n delta = convertDeltaToComplyWithRestriction(delta, camera, config);\n return delta;\n}\n\n/**\n * @description Function that is part of the \"pan to\" handler pipeline. It clamps the pan destination within the boundaries of the view port.\n * You can use this function standalone to clamp the pan destination within the boundaries of the view port. \n * But it is recommended to use this kind of function as part of the pan handler pipeline. (to include this function in your own custom pan handler pipeline)\n * \n * @category Camera\n */\nexport function clampToHandler(destination: Point, camera: BoardCamera, config: PanHandlerClampConfig): Point {\n if(!config.clampTranslation){\n return destination;\n }\n let actualDest = clampPoint(destination, camera.boundaries);\n if(config.limitEntireViewPort){\n actualDest = clampPointEntireViewPort(destination, camera.viewPortWidth, camera.viewPortHeight, camera.boundaries, camera.zoomLevel, camera.rotation);\n }\n return actualDest;\n}\n\n/**\n * @description Function that is part of the \"pan by\" handler pipeline. It clamps the pan delta within the boundaries of the view port.\n * You can use this function standalone to clamp the pan delta within the boundaries of the view port. \n * But it is recommended to use this kind of function as part of the pan handler pipeline. (to include this function in your own custom pan handler pipeline)\n * \n * @category Camera\n */\nexport function clampByHandler(delta: Point, camera: BoardCamera, config: PanHandlerClampConfig): Point {\n if(!config.clampTranslation){\n return delta;\n }\n let actualDelta = PointCal.subVector(clampPoint(PointCal.addVector(camera.position, delta), camera.boundaries), camera.position);\n if(config.limitEntireViewPort){\n actualDelta = PointCal.subVector(clampPointEntireViewPort(PointCal.addVector(camera.position, delta), camera.viewPortWidth, camera.viewPortHeight, camera.boundaries, camera.zoomLevel, camera.rotation), camera.position);\n }\n return actualDelta;\n}\n\n/**\n * @description Function that is part of the \"pan by\" handler pipeline. It pans the camera by the delta.\n * You can use this function standalone to pan the camera by the delta. \n * But it is recommended to use this kind of function as part of the pan handler pipeline. (to include this function in your own custom pan handler pipeline)\n * \n * @category Camera\n */\nfunction PanByBaseHandler(delta: Point, camera: BoardCamera, config: PanHandlerConfig): Point {\n const target = PointCal.addVector(camera.position, delta);\n camera.setPosition(target);\n return delta;\n}\n\n/**\n * @description Function that is part of the \"pan to\" handler pipeline. It pans the camera to the destination.\n * You can use this function standalone to pan the camera to the destination. \n * But it is recommended to use this kind of function as part of the pan handler pipeline. (to include this function in your own custom pan handler pipeline)\n * \n * @category Camera\n */\nfunction PanToBaseHandler(destination: Point, camera: BoardCamera, config: PanHandlerConfig): Point {\n camera.setPosition(destination);\n return destination;\n}\n\n/**\n * @description Helper function that converts the delta to comply with the restrictions of the config.\n * \n * @category Camera\n */\nexport function convertDeltaToComplyWithRestriction(delta: Point, camera: BoardCamera, config: PanHandlerRestrictionConfig): Point {\n if(config.restrictXTranslation && config.restrictYTranslation){\n return {x: 0, y: 0};\n }\n if(config.restrictRelativeXTranslation && config.restrictRelativeYTranslation){\n return {x: 0, y: 0};\n }\n if(config.restrictXTranslation){\n delta.x = 0;\n }\n if(config.restrictYTranslation){\n delta.y = 0;\n }\n if(config.restrictRelativeXTranslation){\n const upDirection = PointCal.rotatePoint({x: 0, y: 1}, camera.rotation);\n const value = PointCal.dotProduct(upDirection, delta);\n delta = PointCal.multiplyVectorByScalar(upDirection, value);\n }\n if(config.restrictRelativeYTranslation){\n const rightDirection = PointCal.rotatePoint({x: 1, y: 0}, camera.rotation);\n const value = PointCal.dotProduct(rightDirection, delta);\n delta = PointCal.multiplyVectorByScalar(rightDirection, value);\n }\n return delta;\n}\n\n/**\n * @description Helper function that converts the user input delta to the camera delta.\n * \n * @category Camera\n */\nexport function convertUserInputDeltaToCameraDelta(delta: Point, camera: BoardCamera): Point {\n return PointCal.multiplyVectorByScalar(PointCal.rotatePoint(delta, camera.rotation), 1 / camera.zoomLevel);\n}\n","import { BoardCamera } from \"src/board-camera/interface\";\nimport { createHandlerChain } from \"src/util/handler-pipeline\";\nimport { normalizeAngleZero2TwoPI, angleSpan, clampRotation } from \"src/board-camera/utils/rotation\";\n\n/**\n * @description This is the configuration for the rotation handler functions.\n * This is the configuration object that is passed to the rotation handler functions.\n * \n * @category Camera\n */\nexport type RotationHandlerConfig = RotationHandlerRestrictConfig & RotationHandlerClampConfig;\n\nexport type RotationHandlerRestrictConfig = {\n /**\n * @description Whether to restrict the rotation. (if true, rotation input will be ignored)\n */\n restrictRotation: boolean;\n}\n\nexport type RotationHandlerClampConfig = {\n /**\n * @description Whether to clamp the rotation if the rotation is out of the rotation boundaries.\n */\n clampRotation: boolean;\n}\n\n/**\n * @description The function that is used to rotate the camera by a specific delta. \n * The delta is in radians.\n * This is structured as a handler pipeline. \n * \n * @see {@link createHandlerChain}\n * @category Camera\n */\nexport type RotateByHandlerFunction = (delta: number, camera: BoardCamera, config: RotationHandlerConfig) => number;\n/**\n * @description The function that is used to rotate the camera to a specific target rotation.\n * The target rotation is in radians.\n * This is structured as a handler pipeline. \n * \n * @see {@link createHandlerChain}\n * @category Camera\n */\nexport type RotateToHandlerFunction = (targetRotation: number, camera: BoardCamera, config: RotationHandlerConfig) => number;\n\n/**\n * @description This is the base handler for the \"rotate by\" handler pipeline.\n * It normalizes the delta to the range of 0 to 2π, and then sets the rotation of the camera to the new rotation.\n * \n * @category Camera\n */\nfunction baseRotateByHandler(delta: number, camera: BoardCamera, config: RotationHandlerConfig): number {\n const targetRotation = normalizeAngleZero2TwoPI(camera.rotation + delta);\n camera.setRotation(targetRotation);\n return delta;\n}\n\n/**\n * @description This is the clamp handler for the \"rotate by\" handler pipeline.\n * It clamps the delta to the range of the camera's rotation boundaries.\n * \n * @category Camera\n */\nexport function clampRotateByHandler(delta: number, camera: BoardCamera, config: RotationHandlerClampConfig): numb