//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;