Migrated visualization to use values calculated from modules to better visualize it
This commit is contained in:
@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="max-speed-slider">Max Module Speed (pixels/s)</label>
|
<label for="max-speed-slider">Max Module Speed (pixels/s)</label>
|
||||||
<input type="range" id="max-speed-slider" min="0" max="300" step="10" value="150">
|
<input type="range" id="max-speed-slider" min="200" max="1000" step="10" value="400">
|
||||||
<output id="max-speed-value">0</output>
|
<output id="max-speed-value">0</output>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
84
script.js
84
script.js
@ -86,18 +86,19 @@ class SwerveDrive {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drive(velocityX, velocityY, turnSpeed, maxModuleSpeed, deltaTime = 0.01) {
|
drive(velocityX, velocityY, turnSpeed, maxModuleSpeed, deltaTime = 0.01) {
|
||||||
// Update gyro heading first
|
// Store the requested turn speed for later calculation of actual turn speed
|
||||||
this.updateHeading(turnSpeed, deltaTime);
|
this.requestedTurnSpeed = turnSpeed;
|
||||||
|
|
||||||
// Take in a requested speeds and update every module
|
// Take in a requested speeds and update every module (but don't update heading yet)
|
||||||
this.modules.forEach(module =>
|
this.modules.forEach(module =>
|
||||||
module.calculateState(velocityX, velocityY, turnSpeed, this.gyroHeading)
|
module.calculateState(velocityX, velocityY, turnSpeed, this.gyroHeading)
|
||||||
);
|
);
|
||||||
|
|
||||||
// If any speeds exceed the max speed, normalize down so we don't effect movement direction
|
// If any speeds exceed the max speed, normalize down so we don't effect movement direction
|
||||||
const maxCalculated = Math.max(...this.modules.map(m => m.speed), 0);
|
const maxCalculated = Math.max(...this.modules.map(m => m.speed), 0);
|
||||||
|
let scale = 1.0;
|
||||||
if (maxCalculated > maxModuleSpeed) {
|
if (maxCalculated > maxModuleSpeed) {
|
||||||
const scale = maxModuleSpeed / maxCalculated;
|
scale = maxModuleSpeed / maxCalculated;
|
||||||
this.modules.forEach(module => {
|
this.modules.forEach(module => {
|
||||||
module.velocity.x *= scale;
|
module.velocity.x *= scale;
|
||||||
module.velocity.y *= scale;
|
module.velocity.y *= scale;
|
||||||
@ -105,6 +106,38 @@ class SwerveDrive {
|
|||||||
module.angle = module.velocity.angle();
|
module.angle = module.velocity.angle();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update heading with the actual turn speed (scaled if modules were limited)
|
||||||
|
const actualTurnSpeed = turnSpeed * scale;
|
||||||
|
this.updateHeading(actualTurnSpeed, deltaTime);
|
||||||
|
this.actualTurnSpeed = actualTurnSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
getActualVelocity() {
|
||||||
|
// Calculate the actual robot velocity from the average of module velocities
|
||||||
|
// This returns the velocity in robot-relative coordinates
|
||||||
|
if (this.modules.length === 0) return new Vec2D(0, 0);
|
||||||
|
|
||||||
|
let sumX = 0;
|
||||||
|
let sumY = 0;
|
||||||
|
|
||||||
|
// Average the module velocities (they're in robot frame)
|
||||||
|
this.modules.forEach(module => {
|
||||||
|
sumX += module.velocity.x;
|
||||||
|
sumY += module.velocity.y;
|
||||||
|
});
|
||||||
|
|
||||||
|
const avgX = sumX / this.modules.length;
|
||||||
|
const avgY = sumY / this.modules.length;
|
||||||
|
|
||||||
|
// Transform back to field-relative coordinates
|
||||||
|
const cosHeading = Math.cos(this.gyroHeading);
|
||||||
|
const sinHeading = Math.sin(this.gyroHeading);
|
||||||
|
|
||||||
|
const fieldVelX = avgX * cosHeading - avgY * sinHeading;
|
||||||
|
const fieldVelY = avgX * sinHeading + avgY * cosHeading;
|
||||||
|
|
||||||
|
return new Vec2D(fieldVelX, fieldVelY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,26 +304,10 @@ const preset16OctBtn = document.getElementById('preset-16oct');
|
|||||||
* BEGIN LISTENER CODE
|
* BEGIN LISTENER CODE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
vxSlider.addEventListener('input', (e) => {
|
|
||||||
vxOutput.textContent = parseFloat(e.target.value);
|
|
||||||
});
|
|
||||||
vxOutput.textContent = parseFloat(vxSlider.value);
|
|
||||||
|
|
||||||
vySlider.addEventListener('input', (e) => {
|
|
||||||
vyOutput.textContent = parseFloat(e.target.value);
|
|
||||||
});
|
|
||||||
vyOutput.textContent = parseFloat(vySlider.value);
|
|
||||||
|
|
||||||
omegaSlider.addEventListener('input', (e) => {
|
|
||||||
omegaOutput.textContent = parseFloat(e.target.value);
|
|
||||||
});
|
|
||||||
omegaOutput.textContent = parseFloat(omegaSlider.value);
|
|
||||||
|
|
||||||
maxSpeedSlider.addEventListener('input', (e) => {
|
maxSpeedSlider.addEventListener('input', (e) => {
|
||||||
maxSpeedOutput.textContent = parseFloat(e.target.value);
|
maxSpeedOutput.textContent = e.target.value;
|
||||||
});
|
});
|
||||||
maxSpeedOutput.textContent = parseFloat(maxSpeedSlider.value);
|
maxSpeedOutput.textContent = maxSpeedSlider.value;
|
||||||
|
|
||||||
resetBtn.addEventListener('click', (e) => {
|
resetBtn.addEventListener('click', (e) => {
|
||||||
vxSlider.value = 0;
|
vxSlider.value = 0;
|
||||||
@ -640,18 +657,27 @@ function animate() {
|
|||||||
ySpeed = -parseFloat(vySlider.value);
|
ySpeed = -parseFloat(vySlider.value);
|
||||||
turnSpeed = parseFloat(omegaSlider.value);
|
turnSpeed = parseFloat(omegaSlider.value);
|
||||||
|
|
||||||
// Animate the grid with robot movement
|
|
||||||
let offsetSpeedDivisor = (100 - gridSquareSize <= 0 ? 1 : 100 - gridSquareSize);
|
|
||||||
|
|
||||||
// Update grid offsets based on robot movement
|
|
||||||
xGridOffset = (xGridOffset + (xSpeed / offsetSpeedDivisor)) % gridSquareSize;
|
|
||||||
yGridOffset = (yGridOffset + (ySpeed / offsetSpeedDivisor)) % gridSquareSize;
|
|
||||||
|
|
||||||
// Update module states before drawing the robot
|
// Update module states before drawing the robot
|
||||||
// The drive() method will update the gyroHeading internally
|
// The drive() method will update the gyroHeading internally
|
||||||
robot.drive(xSpeed, ySpeed, turnSpeed, parseFloat(maxSpeedSlider.value));
|
robot.drive(xSpeed, ySpeed, turnSpeed, parseFloat(maxSpeedSlider.value));
|
||||||
updateModuleDisplays(robot);
|
updateModuleDisplays(robot);
|
||||||
|
|
||||||
|
// Get the actual robot velocity (after scaling to max module speed) for grid animation
|
||||||
|
const actualVelocity = robot.getActualVelocity();
|
||||||
|
|
||||||
|
|
||||||
|
// Update control outputs with actual speeds
|
||||||
|
vxOutput.textContent = `Requested: ${vxSlider.value} | Actual: ${actualVelocity.x.toFixed(2)}`;
|
||||||
|
vyOutput.textContent = `Requested: ${vySlider.value} | Actual: ${-actualVelocity.y.toFixed(2)}`;
|
||||||
|
omegaOutput.textContent = `Requested: ${omegaSlider.value} | Actual: ${robot.actualTurnSpeed.toFixed(2)}`;
|
||||||
|
|
||||||
|
// Animate the grid
|
||||||
|
let offsetSpeedDivisor = (100 - gridSquareSize <= 0 ? 1 : 100 - gridSquareSize);
|
||||||
|
|
||||||
|
// Update grid offsets based on robot movement
|
||||||
|
xGridOffset = (xGridOffset + (actualVelocity.x / offsetSpeedDivisor)) % gridSquareSize;
|
||||||
|
yGridOffset = (yGridOffset + (actualVelocity.y / offsetSpeedDivisor)) % gridSquareSize;
|
||||||
|
|
||||||
// Draw the robot and it's movement. Grid should be oversized so movement
|
// Draw the robot and it's movement. Grid should be oversized so movement
|
||||||
// doesn't find the edge of the grid
|
// doesn't find the edge of the grid
|
||||||
drawGrid(ctx, canvas.width * 2, gridSquareSize, xGridOffset, yGridOffset);
|
drawGrid(ctx, canvas.width * 2, gridSquareSize, xGridOffset, yGridOffset);
|
||||||
|
|||||||
Reference in New Issue
Block a user