changed the folder and added index.js
This commit is contained in:
277
src/state/movementManager.js
Normal file
277
src/state/movementManager.js
Normal file
@@ -0,0 +1,277 @@
|
||||
//const EventEmitter = require('events');
|
||||
|
||||
class movementManager {
|
||||
constructor(config, logger, emitter) {
|
||||
this.emitter = emitter; //new EventEmitter(); //state class emitter
|
||||
|
||||
const { min, max, initial } = config.position;
|
||||
const { speed, maxSpeed, interval } = config.movement;
|
||||
|
||||
this.minPosition = min;
|
||||
this.maxPosition = max;
|
||||
this.currentPosition = initial;
|
||||
|
||||
this.speed = speed;
|
||||
this.maxSpeed = maxSpeed;
|
||||
this.interval = interval;
|
||||
this.timeleft = 0; // timeleft of current movement
|
||||
|
||||
this.logger = logger;
|
||||
this.movementMode = config.movement.mode;
|
||||
}
|
||||
|
||||
getCurrentPosition() {
|
||||
return this.currentPosition;
|
||||
}
|
||||
|
||||
async moveTo(targetPosition, signal) {
|
||||
// Constrain target position if necessary
|
||||
if (
|
||||
targetPosition < this.minPosition ||
|
||||
targetPosition > this.maxPosition
|
||||
) {
|
||||
targetPosition = this.constrain(targetPosition);
|
||||
this.logger.warn(
|
||||
`New target position=${targetPosition} is constrained to fit between min=${this.minPosition} and max=${this.maxPosition}`
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`Starting movement to position ${targetPosition} in ${this.movementMode} with avg speed=${this.speed}%/s.`
|
||||
);
|
||||
|
||||
if (signal && signal.aborted) {
|
||||
this.logger.debug("Movement aborted.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute the movement logic based on the mode
|
||||
switch (this.movementMode) {
|
||||
case "staticspeed":
|
||||
const movelinFeedback = await this.moveLinear(targetPosition,signal);
|
||||
this.logger.info(`Linear move: ${movelinFeedback} `);
|
||||
break;
|
||||
|
||||
case "dynspeed":
|
||||
const moveDynFeedback = await this.moveEaseInOut(targetPosition,signal);
|
||||
this.logger.info(`Dynamic move : ${moveDynFeedback}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported movement mode: ${this.movementMode}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
moveLinear(targetPosition, signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Immediate abort if already signalled
|
||||
if (signal?.aborted) {
|
||||
return reject(new Error("Movement aborted"));
|
||||
}
|
||||
|
||||
// Clamp the final target into [minPosition, maxPosition]
|
||||
targetPosition = this.constrain(targetPosition);
|
||||
|
||||
// Compute direction and remaining distance
|
||||
const direction = targetPosition > this.currentPosition ? 1 : -1;
|
||||
const distance = Math.abs(targetPosition - this.currentPosition);
|
||||
|
||||
// Speed is a fraction [0,1] of full-range per second
|
||||
this.speed = Math.min(Math.max(this.speed, 0), 1);
|
||||
const fullRange = this.maxPosition - this.minPosition;
|
||||
const velocity = this.speed * fullRange; // units per second
|
||||
if (velocity === 0) {
|
||||
return reject(new Error("Movement aborted: zero speed"));
|
||||
}
|
||||
|
||||
// Duration and bookkeeping
|
||||
const duration = distance / velocity; // seconds to go the remaining distance
|
||||
this.timeleft = duration;
|
||||
this.logger.debug(
|
||||
`Linear move: dir=${direction}, dist=${distance}, vel=${velocity.toFixed(2)} u/s, dur=${duration.toFixed(2)}s`
|
||||
);
|
||||
|
||||
// Compute how much to move each tick
|
||||
const intervalMs = this.interval;
|
||||
const intervalSec = intervalMs / 1000;
|
||||
const stepSize = direction * velocity * intervalSec;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Kick off the loop
|
||||
const intervalId = setInterval(() => {
|
||||
// 7a) Abort check
|
||||
if (signal?.aborted) {
|
||||
clearInterval(intervalId);
|
||||
return reject(new Error("Movement aborted"));
|
||||
}
|
||||
|
||||
// Advance position and clamp
|
||||
this.currentPosition += stepSize;
|
||||
this.currentPosition = this.constrain(this.currentPosition);
|
||||
this.emitPos(this.currentPosition);
|
||||
|
||||
// Update timeleft
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
this.timeleft = Math.max(0, duration - elapsed);
|
||||
|
||||
this.logger.debug(
|
||||
`pos=${this.currentPosition.toFixed(2)}, timeleft=${this.timeleft.toFixed(2)}`
|
||||
);
|
||||
|
||||
// Completed the move?
|
||||
if (
|
||||
(direction > 0 && this.currentPosition >= targetPosition) ||
|
||||
(direction < 0 && this.currentPosition <= targetPosition)
|
||||
) {
|
||||
clearInterval(intervalId);
|
||||
this.currentPosition = targetPosition;
|
||||
this.emitPos(this.currentPosition);
|
||||
return resolve("Reached target move.");
|
||||
}
|
||||
}, intervalMs);
|
||||
|
||||
// 8) Also catch aborts that happen before the first tick
|
||||
signal?.addEventListener("abort", () => {
|
||||
clearInterval(intervalId);
|
||||
reject(new Error("Movement aborted"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
moveLinearinTime(targetPosition,signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Abort immediately if already signalled
|
||||
if (signal?.aborted) {
|
||||
return reject(new Error("Movement aborted"));
|
||||
}
|
||||
|
||||
const direction = targetPosition > this.currentPosition ? 1 : -1;
|
||||
const distance = Math.abs(targetPosition - this.currentPosition);
|
||||
|
||||
// Ensure speed is a percentage [0, 1]
|
||||
this.speed = Math.min(Math.max(this.speed, 0), 1);
|
||||
|
||||
// Calculate duration based on percentage of distance per second
|
||||
const duration = 1 / this.speed; // 1 second for 100% of the distance
|
||||
|
||||
this.timeleft = duration; //set this so other classes can use it
|
||||
this.logger.debug(
|
||||
`Linear movement: Direction=${direction}, Distance=${distance}, Duration=${duration}s`
|
||||
);
|
||||
|
||||
let elapsedTime = 0;
|
||||
const interval = this.interval; // Update every x ms
|
||||
const totalSteps = Math.ceil((duration * 1000) / interval);
|
||||
const stepSize = direction * (distance / totalSteps);
|
||||
|
||||
// 2) Set up the abort listener once
|
||||
const intervalId = setInterval(() => {
|
||||
// 3) Check for abort on each tick
|
||||
if (signal?.aborted) {
|
||||
clearInterval(intervalId);
|
||||
return reject(new Error("Movement aborted"));
|
||||
}
|
||||
// Update elapsed time
|
||||
elapsedTime += interval / 1000;
|
||||
|
||||
this.timeleft = duration - elapsedTime; //set this so other classes can use it
|
||||
|
||||
// Update the position incrementally
|
||||
this.currentPosition += stepSize;
|
||||
this.emitPos(this.currentPosition);
|
||||
this.logger.debug(
|
||||
`Using ${this.movementMode} => Current position ${this.currentPosition}`
|
||||
);
|
||||
|
||||
// Check if the target position has been reached
|
||||
if (
|
||||
(direction > 0 && this.currentPosition >= targetPosition) ||
|
||||
(direction < 0 && this.currentPosition <= targetPosition)
|
||||
) {
|
||||
clearInterval(intervalId);
|
||||
this.currentPosition = targetPosition;
|
||||
resolve(`Reached target move.`);
|
||||
}
|
||||
}, interval);
|
||||
// Also attach abort outside the interval in case it fires before the first tick:
|
||||
signal?.addEventListener("abort", () => {
|
||||
clearInterval(intervalId);
|
||||
reject(new Error("Movement aborted"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
moveEaseInOut(targetPosition, signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 1) Bail immediately if already aborted
|
||||
if (signal?.aborted) {
|
||||
return reject(new Error("Movement aborted"));
|
||||
}
|
||||
|
||||
const direction = targetPosition > this.currentPosition ? 1 : -1;
|
||||
const totalDistance = Math.abs(targetPosition - this.currentPosition);
|
||||
const startPosition = this.currentPosition;
|
||||
this.speed = Math.min(Math.max(this.speed, 0), 1);
|
||||
|
||||
const easeFunction = (t) =>
|
||||
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
|
||||
let elapsedTime = 0;
|
||||
const duration = totalDistance / this.speed;
|
||||
this.timeleft = duration;
|
||||
const interval = this.interval;
|
||||
|
||||
// 2) Start the moving loop
|
||||
const intervalId = setInterval(() => {
|
||||
// 3) Check for abort on each tick
|
||||
if (signal?.aborted) {
|
||||
clearInterval(intervalId);
|
||||
return reject(new Error("Movement aborted"));
|
||||
}
|
||||
|
||||
elapsedTime += interval / 1000;
|
||||
const progress = Math.min(elapsedTime / duration, 1);
|
||||
this.timeleft = duration - elapsedTime;
|
||||
const easedProgress = easeFunction(progress);
|
||||
const newPosition =
|
||||
startPosition + (targetPosition - startPosition) * easedProgress;
|
||||
|
||||
this.emitPos(newPosition);
|
||||
this.logger.debug(
|
||||
`Using ${this.movementMode} => Progress=${progress.toFixed(
|
||||
2
|
||||
)}, Eased=${easedProgress.toFixed(2)}`
|
||||
);
|
||||
|
||||
if (progress >= 1) {
|
||||
clearInterval(intervalId);
|
||||
this.currentPosition = targetPosition;
|
||||
resolve(`Reached target move.`);
|
||||
} else {
|
||||
this.currentPosition = newPosition;
|
||||
}
|
||||
}, interval);
|
||||
|
||||
// 4) Also listen once for abort before first tick
|
||||
signal?.addEventListener("abort", () => {
|
||||
clearInterval(intervalId);
|
||||
reject(new Error("Movement aborted"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
emitPos(newPosition) {
|
||||
this.emitter.emit("positionChange", newPosition);
|
||||
}
|
||||
|
||||
constrain(value) {
|
||||
return Math.min(Math.max(value, this.minPosition), this.maxPosition);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = movementManager;
|
||||
Reference in New Issue
Block a user