Compare commits
4 Commits
nicerRobot
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
83049815db
|
|||
|
ba710fcf5d
|
|||
|
0bc7417f35
|
|||
|
2684ed2c72
|
200
index.html
200
index.html
@ -14,6 +14,179 @@
|
|||||||
<p>Interactive simulation of a swerve drive robot with configurable size, speeds, and number of wheels</p>
|
<p>Interactive simulation of a swerve drive robot with configurable size, speeds, and number of wheels</p>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
<section class="documentation">
|
||||||
|
<h2>About This Project</h2>
|
||||||
|
<details>
|
||||||
|
<summary>How To Use</summary>
|
||||||
|
<div class="documentation-content">
|
||||||
|
<h3>Getting Started</h3>
|
||||||
|
<p>This interactive visualizer demonstrates how a swerve drive robot moves based on commanded
|
||||||
|
velocities. Use the controls to experiment with different configurations and movement patterns.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Drive Controls</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Strafe Left/Right:</strong> Controls the robot's velocity in the X direction
|
||||||
|
(field-relative). Positive values move right, negative values move left.</li>
|
||||||
|
<li><strong>Move Forward/Backward:</strong> Controls the robot's velocity in the Y direction
|
||||||
|
(field-relative). Positive values move forward, negative values move backward.</li>
|
||||||
|
<li><strong>Rotation:</strong> Controls the robot's angular velocity (turn rate) in radians per
|
||||||
|
second. Positive values rotate counter-clockwise.</li>
|
||||||
|
<li><strong>Max Module Speed:</strong> Sets the maximum speed limit for any individual swerve
|
||||||
|
module. If calculated speeds exceed this, all modules are scaled proportionally.</li>
|
||||||
|
<li><strong>Reset Controls:</strong> Returns all velocity sliders to zero.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Preset Configurations</h3>
|
||||||
|
<p>Choose from 9 pre-built robot configurations ranging from 2 to 16 wheels. Each preset
|
||||||
|
demonstrates different module arrangements:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>2-Wheel:</strong> Differential drive arrangement</li>
|
||||||
|
<li><strong>3-Wheel Triangle:</strong> Three modules in an equilateral triangle</li>
|
||||||
|
<li><strong>4-Wheel Square:</strong> Classic square configuration</li>
|
||||||
|
<li><strong>4-Wheel Rectangle:</strong> Rectangular configuration for longer robots</li>
|
||||||
|
<li><strong>6-Wheel Hexagon:</strong> Hexagonal arrangement</li>
|
||||||
|
<li><strong>8-Wheel Octagon:</strong> Octagonal arrangement</li>
|
||||||
|
<li><strong>8-Wheel Square:</strong> Double-layered square with inner and outer modules</li>
|
||||||
|
<li><strong>12-Wheel Hexagon:</strong> Double-layered hexagonal arrangement</li>
|
||||||
|
<li><strong>16-Wheel Octagon:</strong> Double-layered octagonal arrangement</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Custom Configurations</h3>
|
||||||
|
<p>Create your own robot configuration:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Enter the desired number of modules (Minimum of 2)</li>
|
||||||
|
<li>Click <strong>Generate Position Inputs</strong> to create input fields</li>
|
||||||
|
<li>For each module, specify:
|
||||||
|
<ul>
|
||||||
|
<li><strong>Module Name:</strong> A label for the module</li>
|
||||||
|
<li><strong>X Position:</strong> Distance from robot center (pixels, positive = right)
|
||||||
|
</li>
|
||||||
|
<li><strong>Y Position:</strong> Distance from robot center (pixels, positive = up)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Click <strong>Apply Custom Configuration</strong> to see your design</li>
|
||||||
|
<li>Use <strong>Remove Position Inputs</strong> to clear the custom fields. This does not reset
|
||||||
|
the robot, only clears the input box</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Understanding the Visualization</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Robot Frame:</strong> The filled polygon connecting the outer-most module positions
|
||||||
|
</li>
|
||||||
|
<li><strong>Modules:</strong> Circular markers at each wheel position</li>
|
||||||
|
<li><strong>Velocity Arrows:</strong> Red arrows showing the direction and magnitude of each
|
||||||
|
module's velocity</li>
|
||||||
|
<li><strong>Grid:</strong> Moves relative to the robot to show field-relative motion</li>
|
||||||
|
<li><strong>Gyro Heading:</strong> The current rotation angle of the robot in degrees</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Module States Panel</h3>
|
||||||
|
<p>Displays real-time information for each module:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Angle:</strong> The direction the module is pointing (in degrees)</li>
|
||||||
|
<li><strong>Speed:</strong> The velocity of the module (in pixels/second)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Explanation of Swerve Kinematics</summary>
|
||||||
|
<div class="documentation-content">
|
||||||
|
<h3>What is Swerve Drive?</h3>
|
||||||
|
<p>Swerve drive (also called holonomic drive) is a drivetrain design where each wheel module can
|
||||||
|
independently rotate and drive in any direction. This allows the robot to move in any direction
|
||||||
|
while simultaneously rotating, providing exceptional maneuverability.</p>
|
||||||
|
|
||||||
|
<h3>Kinematic Equations</h3>
|
||||||
|
<p>The simulator calculates each module's state using inverse kinematics. Given a desired robot
|
||||||
|
velocity (v<sub>x</sub>, v<sub>y</sub>) and rotation rate (ω), we calculate each module's
|
||||||
|
required velocity.</p>
|
||||||
|
|
||||||
|
<h4>Field-Relative vs Robot-Relative</h4>
|
||||||
|
<p>This simulator uses <strong>field-relative control</strong>, meaning the velocity commands are
|
||||||
|
relative to the field, not the robot's current orientation. The inputs are transformed to
|
||||||
|
robot-relative coordinates using the current gyro heading:</p>
|
||||||
|
<pre>
|
||||||
|
v<sub>robot_x</sub> = v<sub>field_x</sub> × cos(-θ) - v<sub>field_y</sub> × sin(-θ)
|
||||||
|
v<sub>robot_y</sub> = v<sub>field_x</sub> × sin(-θ) + v<sub>field_y</sub> × cos(-θ)
|
||||||
|
</pre>
|
||||||
|
<p>Where θ is the robot's heading angle (gyro reading).</p>
|
||||||
|
|
||||||
|
<h4>Module Velocity Calculation</h4>
|
||||||
|
<p>For each module at position (x<sub>i</sub>, y<sub>i</sub>) relative to the robot's center of
|
||||||
|
rotation:</p>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Translation component:</strong> The robot's linear velocity (v<sub>robot_x</sub>,
|
||||||
|
v<sub>robot_y</sub>)</li>
|
||||||
|
<li><strong>Rotation component:</strong> Perpendicular to the position vector, with magnitude
|
||||||
|
proportional to distance from center:
|
||||||
|
<pre>
|
||||||
|
v<sub>rot_x</sub> = -y<sub>i</sub> × ω
|
||||||
|
v<sub>rot_y</sub> = x<sub>i</sub> × ω
|
||||||
|
</pre>
|
||||||
|
</li>
|
||||||
|
<li><strong>Combined velocity:</strong> Vector sum of translation and rotation:
|
||||||
|
<pre>
|
||||||
|
v<sub>module_x</sub> = v<sub>robot_x</sub> + v<sub>rot_x</sub>
|
||||||
|
v<sub>module_y</sub> = v<sub>robot_y</sub> + v<sub>rot_y</sub>
|
||||||
|
</pre>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h4>Module Angle and Speed</h4>
|
||||||
|
<p>From the module's velocity vector, we calculate:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Speed:</strong> The magnitude of the velocity vector: √(v<sub>x</sub>² +
|
||||||
|
v<sub>y</sub>²)</li>
|
||||||
|
<li><strong>Angle:</strong> The direction of the velocity vector: arctan2(v<sub>y</sub>,
|
||||||
|
v<sub>x</sub>)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Speed Normalization</h4>
|
||||||
|
<p>If any module's calculated speed exceeds the maximum allowed speed, all module velocities are
|
||||||
|
scaled proportionally. This preserves the movement direction while respecting hardware limits:
|
||||||
|
</p>
|
||||||
|
<pre>
|
||||||
|
scale = max_speed / max(calculated_speeds)
|
||||||
|
if scale < 1:
|
||||||
|
all_module_speeds × scale
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h3>Gyro Integration</h3>
|
||||||
|
<p>The robot's heading (gyro angle) is continuously updated by integrating the rotation rate:</p>
|
||||||
|
<pre>
|
||||||
|
θ<sub>new</sub> = θ<sub>old</sub> + ω × Δt
|
||||||
|
</pre>
|
||||||
|
<p>Where Δt is the time step. The heading is normalized to stay within the range [-π, π].</p>
|
||||||
|
|
||||||
|
<h3>Real-World Applications</h3>
|
||||||
|
<p>Swerve drive systems are commonly used in:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>FRC (FIRST Robotics Competition):</strong> For competitive robots requiring precise
|
||||||
|
positioning</li>
|
||||||
|
<li><strong>Industrial AGVs:</strong> Automated guided vehicles in warehouses</li>
|
||||||
|
<li><strong>Research Platforms:</strong> Mobile robots requiring omnidirectional movement</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Key Advantages</h3>
|
||||||
|
<ul>
|
||||||
|
<li>True holonomic motion (can move in any direction without rotating)</li>
|
||||||
|
<li>Can translate and rotate simultaneously</li>
|
||||||
|
<li>Excellent maneuverability in constrained spaces</li>
|
||||||
|
<li>No "drift" or unwanted rotation during translation</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Implementation Considerations</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Mechanical Complexity:</strong> Each module requires two motors (drive and steering)
|
||||||
|
</li>
|
||||||
|
<li><strong>Control Complexity:</strong> Requires coordinated control of all modules</li>
|
||||||
|
<li><strong>Sensor Requirements:</strong> Absolute encoders recommended for module angles</li>
|
||||||
|
<li><strong>Cost:</strong> More expensive than traditional drivetrains</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
<section class="visualization-canvas">
|
<section class="visualization-canvas">
|
||||||
<h2>Robot Visualization</h2>
|
<h2>Robot Visualization</h2>
|
||||||
<canvas id="swerve-canvas" width="800" height="800"></canvas>
|
<canvas id="swerve-canvas" width="800" height="800"></canvas>
|
||||||
@ -25,18 +198,18 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Translation & Rotation</legend>
|
<legend>Translation & Rotation</legend>
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label for="vx-slider">Strafe Left/Right (pixels/s)</label>
|
|
||||||
<input type="range" id="vx-slider" min="-300" max="300" step="10" value="0">
|
|
||||||
<output id="vx-value">0</output>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="vy-slider">Move Forward/Backward (pixels/s)</label>
|
<label for="vy-slider">Move Forward/Backward (pixels/s)</label>
|
||||||
<input type="range" id="vy-slider" min="-300" max="300" step="10" value="0">
|
<input type="range" id="vy-slider" min="-300" max="300" step="10" value="0">
|
||||||
<output id="vy-value">0</output>
|
<output id="vy-value">0</output>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="vx-slider">Strafe Left/Right (pixels/s)</label>
|
||||||
|
<input type="range" id="vx-slider" min="-300" max="300" step="10" value="0">
|
||||||
|
<output id="vx-value">0</output>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="omega-slider">Rotation (rad/s)</label>
|
<label for="omega-slider">Rotation (rad/s)</label>
|
||||||
<input type="range" id="omega-slider" min="-3" max="3" step="0.1" value="0">
|
<input type="range" id="omega-slider" min="-3" max="3" step="0.1" value="0">
|
||||||
@ -51,7 +224,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="1" 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>
|
||||||
@ -67,6 +240,9 @@
|
|||||||
<button id="preset-4rect" type="button">4-Wheel Rectangle</button>
|
<button id="preset-4rect" type="button">4-Wheel Rectangle</button>
|
||||||
<button id="preset-6wheel" type="button">6-Wheel Hexagon</button>
|
<button id="preset-6wheel" type="button">6-Wheel Hexagon</button>
|
||||||
<button id="preset-8wheel" type="button">8-Wheel Octagon</button>
|
<button id="preset-8wheel" type="button">8-Wheel Octagon</button>
|
||||||
|
<button id="preset-8square" type="button">8-Wheel Square</button>
|
||||||
|
<button id="preset-12hex" type="button">12-Wheel Hexagon</button>
|
||||||
|
<button id="preset-16oct" type="button">16-Wheel Octogon</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@ -99,15 +275,7 @@
|
|||||||
<!-- Dynamically generated module data will appear here -->
|
<!-- Dynamically generated module data will appear here -->
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="documentation">
|
|
||||||
<h2>About This Project</h2>
|
|
||||||
<details>
|
|
||||||
<summary>How To Use</summary>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary>Explaination of Swerve Kinematics</summary>
|
|
||||||
</details>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script type="module" src="vendor/lucio/graham-scan.mjs"></script>
|
<script type="module" src="vendor/lucio/graham-scan.mjs"></script>
|
||||||
|
|||||||
176
script.js
176
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +192,7 @@ const PresetConfigs = {
|
|||||||
return modules;
|
return modules;
|
||||||
},
|
},
|
||||||
|
|
||||||
eightWheel: (size) => {
|
eightWheelOctogon: (size) => {
|
||||||
const radius = size / 2;
|
const radius = size / 2;
|
||||||
const modules = [];
|
const modules = [];
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
@ -171,7 +204,64 @@ const PresetConfigs = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return modules;
|
return modules;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
eightWheelSquare: (size) => {
|
||||||
|
const full = size;
|
||||||
|
const half = size / 2;
|
||||||
|
return [
|
||||||
|
{ x: full, y: full, name: "Outer FL" },
|
||||||
|
{ x: full, y: -full, name: "Outer FR" },
|
||||||
|
{ x: -full, y: full, name: "Outer BL" },
|
||||||
|
{ x: -full, y: -full, name: "Outer BR" },
|
||||||
|
{ x: half, y: half, name: "Inner FL" },
|
||||||
|
{ x: half, y: -half, name: "Inner FR" },
|
||||||
|
{ x: -half, y: half, name: "Inner BL" },
|
||||||
|
{ x: -half, y: -half, name: "Inner BR" }
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
twelveWheelHexagon: (size) => {
|
||||||
|
const outerRadius = size;
|
||||||
|
const innerRadius = size / 2;
|
||||||
|
const modules = [];
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const angle = (Math.PI / 2) + (i * Math.PI / 3);
|
||||||
|
modules.push({
|
||||||
|
x: outerRadius * Math.cos(angle),
|
||||||
|
y: outerRadius * Math.sin(angle),
|
||||||
|
name: `Module ${i + 1}`
|
||||||
|
});
|
||||||
|
|
||||||
|
modules.push({
|
||||||
|
x: innerRadius * Math.cos(angle),
|
||||||
|
y: innerRadius * Math.sin(angle),
|
||||||
|
name: `Module ${i + 7}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return modules;
|
||||||
|
},
|
||||||
|
|
||||||
|
sixteenWheelOctogon: (size) => {
|
||||||
|
const outerRadius = size;
|
||||||
|
const innerRadius = size / 2;
|
||||||
|
const modules = [];
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const angle = (Math.PI / 2) + (i * Math.PI / 4);
|
||||||
|
modules.push({
|
||||||
|
x: outerRadius * Math.cos(angle),
|
||||||
|
y: outerRadius * Math.sin(angle),
|
||||||
|
name: `Module ${i + 1}`
|
||||||
|
});
|
||||||
|
|
||||||
|
modules.push({
|
||||||
|
x: innerRadius * Math.cos(angle),
|
||||||
|
y: innerRadius * Math.sin(angle),
|
||||||
|
name: `Module ${i + 9}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return modules;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -205,32 +295,19 @@ const preset4WheelBtn = document.getElementById('preset-4wheel');
|
|||||||
const preset4RectBtn = document.getElementById('preset-4rect');
|
const preset4RectBtn = document.getElementById('preset-4rect');
|
||||||
const preset6WheelBtn = document.getElementById('preset-6wheel');
|
const preset6WheelBtn = document.getElementById('preset-6wheel');
|
||||||
const preset8WheelBtn = document.getElementById('preset-8wheel');
|
const preset8WheelBtn = document.getElementById('preset-8wheel');
|
||||||
|
const preset8SquareBtn = document.getElementById('preset-8square');
|
||||||
|
const preset12HexBtn = document.getElementById('preset-12hex');
|
||||||
|
const preset16OctBtn = document.getElementById('preset-16oct');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* END DOM VARIABLES
|
* END DOM VARIABLES
|
||||||
* 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;
|
||||||
@ -284,13 +361,39 @@ preset6WheelBtn.addEventListener('click', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
preset8WheelBtn.addEventListener('click', () => {
|
preset8WheelBtn.addEventListener('click', () => {
|
||||||
const positions = PresetConfigs.eightWheel(robotSize);
|
const positions = PresetConfigs.eightWheelOctogon(robotSize);
|
||||||
robot.setModules(positions);
|
robot.setModules(positions);
|
||||||
robot.setName("8-Wheel Octogon");
|
robot.setName("8-Wheel Octogon");
|
||||||
createModuleDisplays(robot);
|
createModuleDisplays(robot);
|
||||||
updateModuleDisplays(robot);
|
updateModuleDisplays(robot);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
preset8SquareBtn.addEventListener('click', () => {
|
||||||
|
const positions = PresetConfigs.eightWheelSquare(robotSize);
|
||||||
|
robot.setModules(positions);
|
||||||
|
robot.setName("8-Wheel Square");
|
||||||
|
createModuleDisplays(robot);
|
||||||
|
updateModuleDisplays(robot);
|
||||||
|
});
|
||||||
|
|
||||||
|
preset12HexBtn.addEventListener('click', () => {
|
||||||
|
const positions = PresetConfigs.twelveWheelHexagon(robotSize);
|
||||||
|
robot.setModules(positions);
|
||||||
|
robot.setName("12-Wheel Hexagon");
|
||||||
|
createModuleDisplays(robot);
|
||||||
|
updateModuleDisplays(robot);
|
||||||
|
});
|
||||||
|
|
||||||
|
preset16OctBtn.addEventListener('click', () => {
|
||||||
|
const positions = PresetConfigs.sixteenWheelOctogon(robotSize);
|
||||||
|
robot.setModules(positions);
|
||||||
|
robot.setName("16-Wheel Octogon");
|
||||||
|
createModuleDisplays(robot);
|
||||||
|
updateModuleDisplays(robot);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
generateInputsBtn.addEventListener('click', () => {
|
generateInputsBtn.addEventListener('click', () => {
|
||||||
const count = parseInt(moduleCountInput.value);
|
const count = parseInt(moduleCountInput.value);
|
||||||
|
|
||||||
@ -554,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);
|
||||||
|
|||||||
10
styles.css
10
styles.css
@ -238,7 +238,7 @@ tr:hover {
|
|||||||
|
|
||||||
.visualization-area {
|
.visualization-area {
|
||||||
grid-column: 1 / 2;
|
grid-column: 1 / 2;
|
||||||
grid-row: 1 / 3;
|
grid-row: 2 / 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
#swerve-canvas {
|
#swerve-canvas {
|
||||||
@ -252,22 +252,22 @@ tr:hover {
|
|||||||
|
|
||||||
.controls-panel {
|
.controls-panel {
|
||||||
grid-column: 1 / 2;
|
grid-column: 1 / 2;
|
||||||
grid-row: 2 / 3;
|
grid-row: 3 / 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-panel {
|
.config-panel {
|
||||||
grid-column: 2 / 3;
|
grid-column: 2 / 3;
|
||||||
grid-row: 1 / 2;
|
grid-row: 2 / 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-states {
|
.module-states {
|
||||||
grid-column: 2 / 3;
|
grid-column: 2 / 3;
|
||||||
grid-row: 2 / 3;
|
grid-row: 3 / 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.documentation {
|
.documentation {
|
||||||
grid-column: 1 / 3;
|
grid-column: 1 / 3;
|
||||||
grid-row: 4 / 5;
|
grid-row: 1 / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
|
|||||||
Reference in New Issue
Block a user