Compare commits
	
		
			6 Commits
		
	
	
		
			nicerRobot
			...
			evenlySpac
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						07204a476e
	
				 | 
					
					
						|||
| 
						
						
							
						
						5662142274
	
				 | 
					
					
						|||
| 
						
						
							
						
						83049815db
	
				 | 
					
					
						|||
| 
						
						
							
						
						ba710fcf5d
	
				 | 
					
					
						|||
| 
						
						
							
						
						0bc7417f35
	
				 | 
					
					
						|||
| 
						
						
							
						
						2684ed2c72
	
				 | 
					
					
						
							
								
								
									
										249
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										249
									
								
								index.html
									
									
									
									
									
								
							@ -14,6 +14,179 @@
 | 
			
		||||
        <p>Interactive simulation of a swerve drive robot with configurable size, speeds, and number of wheels</p>
 | 
			
		||||
    </header>
 | 
			
		||||
    <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">
 | 
			
		||||
            <h2>Robot Visualization</h2>
 | 
			
		||||
            <canvas id="swerve-canvas" width="800" height="800"></canvas>
 | 
			
		||||
@ -26,24 +199,57 @@
 | 
			
		||||
                <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>
 | 
			
		||||
                    <button id="control-mode-toggle" type="button">Switch to Keyboard Controls (WASD + QE)</button>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="control-group">
 | 
			
		||||
                    <label for="vy-slider">Move Forward/Backward (pixels/s)</label>
 | 
			
		||||
                    <input type="range" id="vy-slider" min="-300" max="300" step="10" value="0">
 | 
			
		||||
                    <output id="vy-value">0</output>
 | 
			
		||||
                <div id="slider-controls">
 | 
			
		||||
                    <div class="control-group">
 | 
			
		||||
                        <label for="vy-slider">Move Forward/Backward (pixels/s)</label>
 | 
			
		||||
                        <input type="range" id="vy-slider" min="-300" max="300" step="10" value="0">
 | 
			
		||||
                        <output id="vy-value">0</output>
 | 
			
		||||
                    </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">
 | 
			
		||||
                        <label for="omega-slider">Rotation (rad/s)</label>
 | 
			
		||||
                        <input type="range" id="omega-slider" min="-3" max="3" step="0.1" value="0">
 | 
			
		||||
                        <output id="omega-value">0</output>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <button id="reset-btn" type="button">Reset Controls</button>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="control-group">
 | 
			
		||||
                    <label for="omega-slider">Rotation (rad/s)</label>
 | 
			
		||||
                    <input type="range" id="omega-slider" min="-3" max="3" step="0.1" value="0">
 | 
			
		||||
                    <output id="omega-value">0</output>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div id="keyboard-controls" style="display: none;">
 | 
			
		||||
                    <div class="control-group">
 | 
			
		||||
                        <label for="keyboard-max-speed">Max Speed (pixels/s)</label>
 | 
			
		||||
                        <input type="range" id="keyboard-max-speed" min="50" max="300" step="10" value="150">
 | 
			
		||||
                        <output id="keyboard-max-speed-value">150</output>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                <button id="reset-btn" type="button">Reset Controls</button>
 | 
			
		||||
                    <div class="control-group">
 | 
			
		||||
                        <label for="keyboard-max-rotation">Max Rotation (rad/s)</label>
 | 
			
		||||
                        <input type="range" id="keyboard-max-rotation" min="0.5" max="3" step="0.1" value="1.5">
 | 
			
		||||
                        <output id="keyboard-max-rotation-value">1.5</output>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="keyboard-instructions">
 | 
			
		||||
                        <h4>Keyboard Controls:</h4>
 | 
			
		||||
                        <ul>
 | 
			
		||||
                            <li><strong>W:</strong> Move Forward</li>
 | 
			
		||||
                            <li><strong>A:</strong> Strafe Left</li>
 | 
			
		||||
                            <li><strong>S:</strong> Move Backward</li>
 | 
			
		||||
                            <li><strong>D:</strong> Strafe Right</li>
 | 
			
		||||
                            <li><strong>Q:</strong> Rotate Counter-Clockwise</li>
 | 
			
		||||
                            <li><strong>E:</strong> Rotate Clockwise</li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                        <p><em>Click on the canvas area to focus for keyboard input</em></p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </fieldset>
 | 
			
		||||
 | 
			
		||||
            <fieldset>
 | 
			
		||||
@ -51,7 +257,7 @@
 | 
			
		||||
 | 
			
		||||
                <div class="control-group">
 | 
			
		||||
                    <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>
 | 
			
		||||
                </div>
 | 
			
		||||
            </fieldset>
 | 
			
		||||
@ -67,6 +273,9 @@
 | 
			
		||||
                    <button id="preset-4rect" type="button">4-Wheel Rectangle</button>
 | 
			
		||||
                    <button id="preset-6wheel" type="button">6-Wheel Hexagon</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>
 | 
			
		||||
            </fieldset>
 | 
			
		||||
            <fieldset>
 | 
			
		||||
@ -74,7 +283,7 @@
 | 
			
		||||
 | 
			
		||||
                <div class="control-group">
 | 
			
		||||
                    <label for="module-count">Number of Modules</label>
 | 
			
		||||
                    <input type="number" id="module-count" min="2" max="12" value="4" step="1">
 | 
			
		||||
                    <input type="number" id="module-count" min="2" max="64" value="4" step="1">
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <button id="generate-inputs-btn" type="button">Generate Position Inputs</button>
 | 
			
		||||
@ -99,15 +308,7 @@
 | 
			
		||||
                <!-- Dynamically generated module data will appear here -->
 | 
			
		||||
            </div>
 | 
			
		||||
        </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>
 | 
			
		||||
 | 
			
		||||
    <script type="module" src="vendor/lucio/graham-scan.mjs"></script>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										381
									
								
								script.js
									
									
									
									
									
								
							
							
						
						
									
										381
									
								
								script.js
									
									
									
									
									
								
							@ -86,18 +86,19 @@ class SwerveDrive {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    drive(velocityX, velocityY, turnSpeed, maxModuleSpeed, deltaTime = 0.01) {
 | 
			
		||||
        // Update gyro heading first
 | 
			
		||||
        this.updateHeading(turnSpeed, deltaTime);
 | 
			
		||||
        // Store the requested turn speed for later calculation of actual turn speed
 | 
			
		||||
        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 =>
 | 
			
		||||
            module.calculateState(velocityX, velocityY, turnSpeed, this.gyroHeading)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // 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);
 | 
			
		||||
        let scale = 1.0;
 | 
			
		||||
        if (maxCalculated > maxModuleSpeed) {
 | 
			
		||||
            const scale = maxModuleSpeed / maxCalculated;
 | 
			
		||||
            scale = maxModuleSpeed / maxCalculated;
 | 
			
		||||
            this.modules.forEach(module => {
 | 
			
		||||
                module.velocity.x *= scale;
 | 
			
		||||
                module.velocity.y *= scale;
 | 
			
		||||
@ -105,6 +106,38 @@ class SwerveDrive {
 | 
			
		||||
                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;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    eightWheel: (size) => {
 | 
			
		||||
    eightWheelOctogon: (size) => {
 | 
			
		||||
        const radius = size / 2;
 | 
			
		||||
        const modules = [];
 | 
			
		||||
        for (let i = 0; i < 8; i++) {
 | 
			
		||||
@ -171,7 +204,64 @@ const PresetConfigs = {
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        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;
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@ -198,6 +288,15 @@ const generateInputsBtn = document.getElementById('generate-inputs-btn');
 | 
			
		||||
const clearInputsBtn = document.getElementById('delete-inputs-btn');
 | 
			
		||||
const applyCustomBtn = document.getElementById('apply-custom-btn');
 | 
			
		||||
 | 
			
		||||
// Get control mode elements
 | 
			
		||||
const controlModeToggle = document.getElementById('control-mode-toggle');
 | 
			
		||||
const sliderControls = document.getElementById('slider-controls');
 | 
			
		||||
const keyboardControls = document.getElementById('keyboard-controls');
 | 
			
		||||
const keyboardMaxSpeed = document.getElementById('keyboard-max-speed');
 | 
			
		||||
const keyboardMaxSpeedOutput = document.getElementById('keyboard-max-speed-value');
 | 
			
		||||
const keyboardMaxRotation = document.getElementById('keyboard-max-rotation');
 | 
			
		||||
const keyboardMaxRotationOutput = document.getElementById('keyboard-max-rotation-value');
 | 
			
		||||
 | 
			
		||||
// Preset buttons
 | 
			
		||||
const preset2WheelBtn = document.getElementById('preset-2wheel');
 | 
			
		||||
const preset3WheelBtn = document.getElementById('preset-3wheel');
 | 
			
		||||
@ -205,32 +304,42 @@ const preset4WheelBtn = document.getElementById('preset-4wheel');
 | 
			
		||||
const preset4RectBtn = document.getElementById('preset-4rect');
 | 
			
		||||
const preset6WheelBtn = document.getElementById('preset-6wheel');
 | 
			
		||||
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
 | 
			
		||||
* BEGIN CONTROL MODE AND INPUT DEVICE VARIABLES
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
// Control mode state
 | 
			
		||||
let isManualInputMode = false; // true = keyboard/gamepad mode, false = slider mode
 | 
			
		||||
 | 
			
		||||
// Keyboard state tracking
 | 
			
		||||
const keyState = {
 | 
			
		||||
    w: false,  // Forward
 | 
			
		||||
    a: false,  // Left
 | 
			
		||||
    s: false,  // Backward
 | 
			
		||||
    d: false,  // Right
 | 
			
		||||
    q: false,  // Counter-clockwise
 | 
			
		||||
    e: false   // Clockwise
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Current manual input velocities (from keyboard or gamepad)
 | 
			
		||||
let manualInputVelX = 0;
 | 
			
		||||
let manualInputVelY = 0;
 | 
			
		||||
let manualInputOmega = 0;
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
* END CONTROL MODE AND INPUT DEVICE VARIABLES
 | 
			
		||||
* 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) => {
 | 
			
		||||
    maxSpeedOutput.textContent = parseFloat(e.target.value);
 | 
			
		||||
    maxSpeedOutput.textContent = e.target.value;
 | 
			
		||||
});
 | 
			
		||||
maxSpeedOutput.textContent = parseFloat(maxSpeedSlider.value);
 | 
			
		||||
maxSpeedOutput.textContent = maxSpeedSlider.value;
 | 
			
		||||
 | 
			
		||||
resetBtn.addEventListener('click', (e) => {
 | 
			
		||||
    vxSlider.value = 0;
 | 
			
		||||
@ -242,6 +351,93 @@ resetBtn.addEventListener('click', (e) => {
 | 
			
		||||
    omegaOutput.textContent = parseFloat(omegaSlider.value);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Keyboard control sliders
 | 
			
		||||
keyboardMaxSpeed.addEventListener('input', (e) => {
 | 
			
		||||
    keyboardMaxSpeedOutput.textContent = parseFloat(e.target.value);
 | 
			
		||||
});
 | 
			
		||||
keyboardMaxSpeedOutput.textContent = parseFloat(keyboardMaxSpeed.value);
 | 
			
		||||
 | 
			
		||||
keyboardMaxRotation.addEventListener('input', (e) => {
 | 
			
		||||
    keyboardMaxRotationOutput.textContent = parseFloat(e.target.value);
 | 
			
		||||
});
 | 
			
		||||
keyboardMaxRotationOutput.textContent = parseFloat(keyboardMaxRotation.value);
 | 
			
		||||
 | 
			
		||||
// Control mode toggle
 | 
			
		||||
controlModeToggle.addEventListener('click', () => {
 | 
			
		||||
    isManualInputMode = !isManualInputMode;
 | 
			
		||||
 | 
			
		||||
    if (isManualInputMode) {
 | 
			
		||||
        // Switch to manual input mode (keyboard/gamepad)
 | 
			
		||||
        sliderControls.style.display = 'none';
 | 
			
		||||
        keyboardControls.style.display = 'block';
 | 
			
		||||
        controlModeToggle.textContent = 'Switch to Slider Controls';
 | 
			
		||||
 | 
			
		||||
        // Reset slider values when switching to manual input
 | 
			
		||||
        vxSlider.value = 0;
 | 
			
		||||
        vySlider.value = 0;
 | 
			
		||||
        omegaSlider.value = 0;
 | 
			
		||||
        vxOutput.textContent = '0';
 | 
			
		||||
        vyOutput.textContent = '0';
 | 
			
		||||
        omegaOutput.textContent = '0';
 | 
			
		||||
    } else {
 | 
			
		||||
        // Switch to slider mode
 | 
			
		||||
        sliderControls.style.display = 'block';
 | 
			
		||||
        keyboardControls.style.display = 'none';
 | 
			
		||||
        controlModeToggle.textContent = 'Switch to Keyboard Controls (WASD + QE)';
 | 
			
		||||
 | 
			
		||||
        // Reset manual input state
 | 
			
		||||
        Object.keys(keyState).forEach(key => keyState[key] = false);
 | 
			
		||||
        manualInputVelX = 0;
 | 
			
		||||
        manualInputVelY = 0;
 | 
			
		||||
        manualInputOmega = 0;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Keyboard event listeners
 | 
			
		||||
document.addEventListener('keydown', (e) => {
 | 
			
		||||
    if (!isManualInputMode) return;
 | 
			
		||||
 | 
			
		||||
    const key = e.key.toLowerCase();
 | 
			
		||||
    if (key in keyState) {
 | 
			
		||||
        keyState[key] = true;
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        updateManualInputVelocities();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
document.addEventListener('keyup', (e) => {
 | 
			
		||||
    if (!isManualInputMode) return;
 | 
			
		||||
 | 
			
		||||
    const key = e.key.toLowerCase();
 | 
			
		||||
    if (key in keyState) {
 | 
			
		||||
        keyState[key] = false;
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        updateManualInputVelocities();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Function to update velocities based on manual input devices (keyboard/gamepad)
 | 
			
		||||
function updateManualInputVelocities() {
 | 
			
		||||
    const maxSpeed = parseFloat(keyboardMaxSpeed.value);
 | 
			
		||||
    const maxRotation = parseFloat(keyboardMaxRotation.value);
 | 
			
		||||
 | 
			
		||||
    // Calculate translation velocities from keyboard input
 | 
			
		||||
    manualInputVelX = 0;
 | 
			
		||||
    manualInputVelY = 0;
 | 
			
		||||
 | 
			
		||||
    if (keyState.d) manualInputVelX += maxSpeed;  // Right
 | 
			
		||||
    if (keyState.a) manualInputVelX -= maxSpeed;  // Left
 | 
			
		||||
    if (keyState.w) manualInputVelY += maxSpeed;  // Forward  
 | 
			
		||||
    if (keyState.s) manualInputVelY -= maxSpeed;  // Backward
 | 
			
		||||
 | 
			
		||||
    // Calculate rotation velocity from keyboard input
 | 
			
		||||
    manualInputOmega = 0;
 | 
			
		||||
    if (keyState.e) manualInputOmega += maxRotation;  // Clockwise
 | 
			
		||||
    if (keyState.q) manualInputOmega -= maxRotation;  // Counter-clockwise
 | 
			
		||||
 | 
			
		||||
    // TODO: Add gamepad input processing here
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Preset button event listeners
 | 
			
		||||
preset2WheelBtn.addEventListener('click', () => {
 | 
			
		||||
    const positions = PresetConfigs.twoWheel(robotSize);
 | 
			
		||||
@ -284,18 +480,44 @@ preset6WheelBtn.addEventListener('click', () => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
preset8WheelBtn.addEventListener('click', () => {
 | 
			
		||||
    const positions = PresetConfigs.eightWheel(robotSize);
 | 
			
		||||
    const positions = PresetConfigs.eightWheelOctogon(robotSize);
 | 
			
		||||
    robot.setModules(positions);
 | 
			
		||||
    robot.setName("8-Wheel Octogon");
 | 
			
		||||
    createModuleDisplays(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', () => {
 | 
			
		||||
    const count = parseInt(moduleCountInput.value);
 | 
			
		||||
 | 
			
		||||
    if (isNaN(count) || count < 2) {
 | 
			
		||||
        alert('Please enter a valid number of modules above or equal to 2.');
 | 
			
		||||
    if (isNaN(count) || count < 2 || count > 64) {
 | 
			
		||||
        alert('Please enter a valid number of modules between 2 and 64.');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    generateModuleInputs(count);
 | 
			
		||||
@ -335,11 +557,67 @@ applyCustomBtn.addEventListener('click', () => {
 | 
			
		||||
* BEGIN DYNAMIC DOM FUNCTIONS
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
// Function to calculate evenly spaced module positions in circular layers
 | 
			
		||||
function calculateModulePositions(count) {
 | 
			
		||||
    if (count <= 0) return [];
 | 
			
		||||
 | 
			
		||||
    const baseRadius = 50; // Base radius for the first layer
 | 
			
		||||
    const positions = [];
 | 
			
		||||
    let remainingModules = count;
 | 
			
		||||
    let numberOfLayers = Math.ceil(count / 6);
 | 
			
		||||
    let modulesPerLayer = Math.ceil(count / numberOfLayers);
 | 
			
		||||
    let angleOffset = 0;
 | 
			
		||||
    let currentLayer = 0;
 | 
			
		||||
    let moduleIndex = 0;
 | 
			
		||||
 | 
			
		||||
    while (remainingModules > 0) {
 | 
			
		||||
        // Determine modules for this layer
 | 
			
		||||
        let modulesInThisLayer;
 | 
			
		||||
 | 
			
		||||
        if (remainingModules <= modulesPerLayer) {
 | 
			
		||||
            // Last layer gets all remaining modules
 | 
			
		||||
            modulesInThisLayer = remainingModules;
 | 
			
		||||
        } else {
 | 
			
		||||
            // All other layers: try to distribute evenly with max modulesPerLayer
 | 
			
		||||
            const remainingLayers = Math.ceil(remainingModules / modulesPerLayer);
 | 
			
		||||
            modulesInThisLayer = Math.min(modulesPerLayer, Math.ceil(remainingModules / remainingLayers));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Calculate radius for this layer
 | 
			
		||||
        const radius = currentLayer === 0 ? baseRadius : baseRadius * (1 + currentLayer * 0.75);
 | 
			
		||||
 | 
			
		||||
        // Calculate positions for modules in this layer
 | 
			
		||||
        for (let i = 0; i < modulesInThisLayer; i++) {
 | 
			
		||||
            const angle = (angleOffset + 2 * Math.PI * i) / modulesInThisLayer;
 | 
			
		||||
            const x = Math.round(radius * Math.cos(angle));
 | 
			
		||||
            const y = Math.round(radius * Math.sin(angle));
 | 
			
		||||
 | 
			
		||||
            positions.push({
 | 
			
		||||
                x: x,
 | 
			
		||||
                y: y,
 | 
			
		||||
                name: `Layer ${currentLayer + 1} Module ${(moduleIndex % modulesPerLayer) + 1}`
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            moduleIndex++;
 | 
			
		||||
        }
 | 
			
		||||
        angleOffset += Math.PI;
 | 
			
		||||
 | 
			
		||||
        remainingModules -= modulesInThisLayer;
 | 
			
		||||
        currentLayer++;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return positions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateModuleInputs(count) {
 | 
			
		||||
    const container = document.getElementById('module-position-inputs');
 | 
			
		||||
    container.innerHTML = ''; // Clear existing inputs
 | 
			
		||||
 | 
			
		||||
    // Calculate evenly spaced positions
 | 
			
		||||
    const positions = calculateModulePositions(count);
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < count; i++) {
 | 
			
		||||
        const position = positions[i];
 | 
			
		||||
        const moduleFieldset = document.createElement('fieldset');
 | 
			
		||||
        moduleFieldset.className = 'module-input-group';
 | 
			
		||||
        moduleFieldset.innerHTML = `
 | 
			
		||||
@ -347,15 +625,15 @@ function generateModuleInputs(count) {
 | 
			
		||||
 | 
			
		||||
        <div class="control-group">
 | 
			
		||||
            <label for="module-${i}-name">Module Name</label>
 | 
			
		||||
            <input type="text" id="module-${i}-name" value="Module ${i + 1}" required>
 | 
			
		||||
            <input type="text" id="module-${i}-name" value="${position.name}" required>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="control-group">
 | 
			
		||||
            <label for="module-${i}-x">X Position (pixels)</label>
 | 
			
		||||
            <input type="number" id="module-${i}-x" step="1" value="0" required>
 | 
			
		||||
            <input type="number" id="module-${i}-x" step="1" value="${position.x}" required>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="control-group">
 | 
			
		||||
            <label for="module-${i}-y">Y Position (pixels)</label>
 | 
			
		||||
            <input type="number" id="module-${i}-y" step="0.1" value="0" required>
 | 
			
		||||
            <input type="number" id="module-${i}-y" step="0.1" value="${position.y}" required>
 | 
			
		||||
        </div>
 | 
			
		||||
        `;
 | 
			
		||||
        container.appendChild(moduleFieldset);
 | 
			
		||||
@ -549,23 +827,44 @@ function animate() {
 | 
			
		||||
    ctx.save();
 | 
			
		||||
    ctx.translate(canvas.width / 2, canvas.height / 2);
 | 
			
		||||
 | 
			
		||||
    // Update speeds based on sliders
 | 
			
		||||
    xSpeed = parseFloat(vxSlider.value);
 | 
			
		||||
    ySpeed = -parseFloat(vySlider.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 speeds based on control mode
 | 
			
		||||
    if (isManualInputMode) {
 | 
			
		||||
        xSpeed = manualInputVelX;
 | 
			
		||||
        ySpeed = -manualInputVelY; // Negative because canvas Y axis is inverted
 | 
			
		||||
        turnSpeed = manualInputOmega;
 | 
			
		||||
    } else {
 | 
			
		||||
        xSpeed = parseFloat(vxSlider.value);
 | 
			
		||||
        ySpeed = -parseFloat(vySlider.value);
 | 
			
		||||
        turnSpeed = parseFloat(omegaSlider.value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Update module states before drawing the robot
 | 
			
		||||
    // The drive() method will update the gyroHeading internally
 | 
			
		||||
    robot.drive(xSpeed, ySpeed, turnSpeed, parseFloat(maxSpeedSlider.value));
 | 
			
		||||
    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
 | 
			
		||||
    if (isManualInputMode) {
 | 
			
		||||
        // In manual input mode (keyboard/gamepad), show the current values
 | 
			
		||||
        keyboardMaxSpeedOutput.textContent = `Max: ${keyboardMaxSpeed.value} | Current: ${Math.max(Math.abs(actualVelocity.x), Math.abs(actualVelocity.y)).toFixed(1)}`;
 | 
			
		||||
        keyboardMaxRotationOutput.textContent = `Max: ${keyboardMaxRotation.value} | Current: ${Math.abs(robot.actualTurnSpeed || 0).toFixed(2)}`;
 | 
			
		||||
    } else {
 | 
			
		||||
        // In slider mode, show requested vs actual
 | 
			
		||||
        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 || 0).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
 | 
			
		||||
    // doesn't find the edge of the grid
 | 
			
		||||
    drawGrid(ctx, canvas.width * 2, gridSquareSize, xGridOffset, yGridOffset);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								styles.css
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								styles.css
									
									
									
									
									
								
							@ -238,7 +238,7 @@ tr:hover {
 | 
			
		||||
 | 
			
		||||
.visualization-area {
 | 
			
		||||
  grid-column: 1 / 2;
 | 
			
		||||
  grid-row: 1 / 3;
 | 
			
		||||
  grid-row: 2 / 4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#swerve-canvas {
 | 
			
		||||
@ -252,22 +252,22 @@ tr:hover {
 | 
			
		||||
 | 
			
		||||
.controls-panel {
 | 
			
		||||
  grid-column: 1 / 2;
 | 
			
		||||
  grid-row: 2 / 3;
 | 
			
		||||
  grid-row: 3 / 4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.config-panel {
 | 
			
		||||
  grid-column: 2 / 3;
 | 
			
		||||
  grid-row: 1 / 2;
 | 
			
		||||
  grid-row: 2 / 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.module-states {
 | 
			
		||||
  grid-column: 2 / 3;
 | 
			
		||||
  grid-row: 2 / 3;
 | 
			
		||||
  grid-row: 3 / 4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.documentation {
 | 
			
		||||
  grid-column: 1 / 3;
 | 
			
		||||
  grid-row: 4 / 5;
 | 
			
		||||
  grid-row: 1 / 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fieldset {
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user