Files
generalFunctions/src/state/movementManager.js

293 lines
9.4 KiB
JavaScript

//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;
console.log(`MovementManager: Initial speed=${this.speed}, 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);
const velocity = this.getVelocity(); // 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);
const velocity = this.getVelocity();
if (velocity <= 0) {
return reject(new Error("Movement aborted: zero speed"));
}
const duration = distance / velocity;
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;
const velocity = this.getVelocity();
if (velocity <= 0) {
return reject(new Error("Movement aborted: zero speed"));
}
const easeFunction = (t) =>
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
let elapsedTime = 0;
const duration = totalDistance / velocity;
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);
}
getNormalizedSpeed() {
const rawSpeed = Number.isFinite(this.speed) ? this.speed : 0;
const clampedSpeed = Math.max(0, rawSpeed);
const hasMax = Number.isFinite(this.maxSpeed) && this.maxSpeed > 0;
const effectiveSpeed = hasMax ? Math.min(clampedSpeed, this.maxSpeed) : clampedSpeed;
return effectiveSpeed / 100; // convert %/s -> fraction of range per second
}
getVelocity() {
const normalizedSpeed = this.getNormalizedSpeed();
const fullRange = this.maxPosition - this.minPosition;
return normalizedSpeed * fullRange;
}
}
module.exports = movementManager;