Compare commits
7 Commits
swerveCode
...
nicerRobot
| Author | SHA1 | Date | |
|---|---|---|---|
|
b766527a97
|
|||
|
9bd249af6f
|
|||
|
f91374ed64
|
|||
|
18ebebdcb7
|
|||
|
94fd41e424
|
|||
|
f1117bf925
|
|||
|
22e48b34d5
|
@ -16,7 +16,7 @@
|
|||||||
<main>
|
<main>
|
||||||
<section class="visualization-canvas">
|
<section class="visualization-canvas">
|
||||||
<h2>Robot Visualization</h2>
|
<h2>Robot Visualization</h2>
|
||||||
<canvas id="swerve-canvas" width="600" height="600"></canvas>
|
<canvas id="swerve-canvas" width="800" height="800"></canvas>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="control-panel">
|
<section class="control-panel">
|
||||||
@ -78,6 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="generate-inputs-btn" type="button">Generate Position Inputs</button>
|
<button id="generate-inputs-btn" type="button">Generate Position Inputs</button>
|
||||||
|
<button id="delete-inputs-btn" type="button">Remove Position Inputs</button>
|
||||||
|
|
||||||
<div id="module-position-inputs" class="position-inputs">
|
<div id="module-position-inputs" class="position-inputs">
|
||||||
<!-- Dynamically generated position inputs will appear here -->
|
<!-- Dynamically generated position inputs will appear here -->
|
||||||
@ -91,6 +92,8 @@
|
|||||||
<div id="current-config-info" class="config-info">
|
<div id="current-config-info" class="config-info">
|
||||||
Current Configuration: <strong id="config-name">4-Wheel Rectangle</strong>
|
Current Configuration: <strong id="config-name">4-Wheel Rectangle</strong>
|
||||||
(<span id="module-count-display">4</span> modules)
|
(<span id="module-count-display">4</span> modules)
|
||||||
|
<br>
|
||||||
|
Gyro Heading: <strong id="gyro-heading-display">0.0°</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="module-grid" id="module-grid">
|
<div class="module-grid" id="module-grid">
|
||||||
<!-- Dynamically generated module data will appear here -->
|
<!-- Dynamically generated module data will appear here -->
|
||||||
@ -107,7 +110,8 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<script type="module" src="vendor/lucio/graham-scan.mjs"></script>
|
||||||
|
<script type="module" src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
250
script.js
250
script.js
@ -2,6 +2,8 @@
|
|||||||
* BEGIN CLASS DECLARATIONS
|
* BEGIN CLASS DECLARATIONS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import GrahamScan from "./vendor/lucio/graham-scan.mjs";
|
||||||
|
|
||||||
// 2D vector class to make some of the math easier
|
// 2D vector class to make some of the math easier
|
||||||
class Vec2D {
|
class Vec2D {
|
||||||
constructor(x, y) {
|
constructor(x, y) {
|
||||||
@ -28,19 +30,27 @@ class SwerveModule {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateState(velocityX, velocityY, turnSpeed) {
|
calculateState(velocityX, velocityY, turnSpeed, heading = 0) {
|
||||||
// Take the requested speed and turn rate of the robot and calculate
|
// Take the requested speed and turn rate of the robot and calculate
|
||||||
// speed and angle of this module to achieve it
|
// speed and angle of this module to achieve it
|
||||||
|
|
||||||
|
// Transform field-relative velocities to robot-relative velocities
|
||||||
|
// by rotating the velocity vector by the negative of the robot's heading
|
||||||
|
const cosHeading = Math.cos(-heading);
|
||||||
|
const sinHeading = Math.sin(-heading);
|
||||||
|
|
||||||
|
const robotVelX = velocityX * cosHeading - velocityY * sinHeading;
|
||||||
|
const robotVelY = velocityX * sinHeading + velocityY * cosHeading;
|
||||||
|
|
||||||
// Calculate rotation contribution (perpendicular to position vector)
|
// Calculate rotation contribution (perpendicular to position vector)
|
||||||
const rotX = -this.position.y * turnSpeed;
|
const rotX = -this.position.y * turnSpeed;
|
||||||
const rotY = this.position.x * turnSpeed;
|
const rotY = this.position.x * turnSpeed;
|
||||||
|
|
||||||
// Combine translation and rotation
|
// Combine translation and rotation (now in robot frame)
|
||||||
this.velocity.x = velocityX + rotX;
|
this.velocity.x = robotVelX + rotX;
|
||||||
this.velocity.y = velocityY + rotY;
|
this.velocity.y = robotVelY + rotY;
|
||||||
|
|
||||||
// Calculate speed and angle
|
// Calculate speed and angle (in robot frame)
|
||||||
this.speed = this.velocity.magnitude();
|
this.speed = this.velocity.magnitude();
|
||||||
this.angle = this.velocity.angle();
|
this.angle = this.velocity.angle();
|
||||||
}
|
}
|
||||||
@ -48,8 +58,14 @@ class SwerveModule {
|
|||||||
|
|
||||||
// Swerve drive class to represent the robot as a whole
|
// Swerve drive class to represent the robot as a whole
|
||||||
class SwerveDrive {
|
class SwerveDrive {
|
||||||
constructor(modulePositionsAndNames) {
|
constructor(modulePositionsAndNames, robotName) {
|
||||||
this.setModules(modulePositionsAndNames);
|
this.setModules(modulePositionsAndNames);
|
||||||
|
this.setName(robotName);
|
||||||
|
this.gyroHeading = 0; // Simulated gyro heading in radians
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(robotName) {
|
||||||
|
this.name = robotName;
|
||||||
}
|
}
|
||||||
|
|
||||||
setModules(modulePositionsAndNames) {
|
setModules(modulePositionsAndNames) {
|
||||||
@ -59,10 +75,23 @@ class SwerveDrive {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
drive(velocityX, velocityY, turnSpeed, maxModuleSpeed) {
|
updateHeading(turnSpeed, deltaTime = 0.01) {
|
||||||
|
// Integrate turn speed to update gyro heading
|
||||||
|
// turnSpeed is in radians/second, deltaTime is the time step
|
||||||
|
this.gyroHeading += turnSpeed * deltaTime;
|
||||||
|
|
||||||
|
// Normalize to -PI to PI range
|
||||||
|
while (this.gyroHeading > Math.PI) this.gyroHeading -= 2 * Math.PI;
|
||||||
|
while (this.gyroHeading < -Math.PI) this.gyroHeading += 2 * Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
drive(velocityX, velocityY, turnSpeed, maxModuleSpeed, deltaTime = 0.01) {
|
||||||
|
// Update gyro heading first
|
||||||
|
this.updateHeading(turnSpeed, deltaTime);
|
||||||
|
|
||||||
// Take in a requested speeds and update every module
|
// Take in a requested speeds and update every module
|
||||||
this.modules.forEach(module =>
|
this.modules.forEach(module =>
|
||||||
module.calculateState(velocityX, velocityY, turnSpeed)
|
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
|
||||||
@ -166,6 +195,7 @@ const maxSpeedOutput = document.getElementById('max-speed-value');
|
|||||||
// Get button elements
|
// Get button elements
|
||||||
const resetBtn = document.getElementById('reset-btn');
|
const resetBtn = document.getElementById('reset-btn');
|
||||||
const generateInputsBtn = document.getElementById('generate-inputs-btn');
|
const generateInputsBtn = document.getElementById('generate-inputs-btn');
|
||||||
|
const clearInputsBtn = document.getElementById('delete-inputs-btn');
|
||||||
const applyCustomBtn = document.getElementById('apply-custom-btn');
|
const applyCustomBtn = document.getElementById('apply-custom-btn');
|
||||||
|
|
||||||
// Preset buttons
|
// Preset buttons
|
||||||
@ -216,35 +246,175 @@ resetBtn.addEventListener('click', (e) => {
|
|||||||
preset2WheelBtn.addEventListener('click', () => {
|
preset2WheelBtn.addEventListener('click', () => {
|
||||||
const positions = PresetConfigs.twoWheel(robotSize);
|
const positions = PresetConfigs.twoWheel(robotSize);
|
||||||
robot.setModules(positions);
|
robot.setModules(positions);
|
||||||
|
robot.setName("2-Wheel Differential");
|
||||||
|
createModuleDisplays(robot);
|
||||||
|
updateModuleDisplays(robot);
|
||||||
});
|
});
|
||||||
|
|
||||||
preset3WheelBtn.addEventListener('click', () => {
|
preset3WheelBtn.addEventListener('click', () => {
|
||||||
const positions = PresetConfigs.threeWheel(robotSize);
|
const positions = PresetConfigs.threeWheel(robotSize);
|
||||||
robot.setModules(positions);
|
robot.setModules(positions);
|
||||||
|
robot.setName("3-Wheel Triangle");
|
||||||
|
createModuleDisplays(robot);
|
||||||
|
updateModuleDisplays(robot);
|
||||||
});
|
});
|
||||||
|
|
||||||
preset4WheelBtn.addEventListener('click', () => {
|
preset4WheelBtn.addEventListener('click', () => {
|
||||||
const positions = PresetConfigs.fourWheelSquare(robotSize);
|
const positions = PresetConfigs.fourWheelSquare(robotSize);
|
||||||
robot.setModules(positions);
|
robot.setModules(positions);
|
||||||
|
robot.setName("4-Wheel Square");
|
||||||
|
createModuleDisplays(robot);
|
||||||
|
updateModuleDisplays(robot);
|
||||||
});
|
});
|
||||||
|
|
||||||
preset4RectBtn.addEventListener('click', () => {
|
preset4RectBtn.addEventListener('click', () => {
|
||||||
const positions = PresetConfigs.fourWheelRectangle(robotSize);
|
const positions = PresetConfigs.fourWheelRectangle(robotSize);
|
||||||
robot.setModules(positions);
|
robot.setModules(positions);
|
||||||
|
robot.setName("4-Wheel Rectangle");
|
||||||
|
createModuleDisplays(robot);
|
||||||
|
updateModuleDisplays(robot);
|
||||||
});
|
});
|
||||||
|
|
||||||
preset6WheelBtn.addEventListener('click', () => {
|
preset6WheelBtn.addEventListener('click', () => {
|
||||||
const positions = PresetConfigs.sixWheel(robotSize);
|
const positions = PresetConfigs.sixWheel(robotSize);
|
||||||
robot.setModules(positions);
|
robot.setModules(positions);
|
||||||
|
robot.setName("6-Wheel Hexagon");
|
||||||
|
createModuleDisplays(robot);
|
||||||
|
updateModuleDisplays(robot);
|
||||||
});
|
});
|
||||||
|
|
||||||
preset8WheelBtn.addEventListener('click', () => {
|
preset8WheelBtn.addEventListener('click', () => {
|
||||||
const positions = PresetConfigs.eightWheel(robotSize);
|
const positions = PresetConfigs.eightWheel(robotSize);
|
||||||
robot.setModules(positions);
|
robot.setModules(positions);
|
||||||
|
robot.setName("8-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.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
generateModuleInputs(count);
|
||||||
|
applyCustomBtn.style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
clearInputsBtn.addEventListener('click', () => {
|
||||||
|
generateModuleInputs(0);
|
||||||
|
applyCustomBtn.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
applyCustomBtn.addEventListener('click', () => {
|
||||||
|
const container = document.getElementById('module-position-inputs');
|
||||||
|
const moduleElements = container.childNodes;
|
||||||
|
|
||||||
|
const customModules = [];
|
||||||
|
for (let i = 0; i < moduleElements.length; i++) {
|
||||||
|
const xInput = document.getElementById(`module-${i}-x`);
|
||||||
|
const yInput = document.getElementById(`module-${i}-y`);
|
||||||
|
const nameInput = document.getElementById(`module-${i}-name`);
|
||||||
|
|
||||||
|
const x = parseFloat(xInput.value);
|
||||||
|
const y = parseFloat(yInput.value);
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
|
||||||
|
customModules.push({ x, y, name });
|
||||||
|
}
|
||||||
|
|
||||||
|
robot.setModules(customModules);
|
||||||
|
robot.setName("Custom Configuration");
|
||||||
|
createModuleDisplays(robot);
|
||||||
|
updateModuleDisplays(robot);
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* END LISTENER CODE
|
* END LISTENER CODE
|
||||||
|
* BEGIN DYNAMIC DOM FUNCTIONS
|
||||||
|
*/
|
||||||
|
|
||||||
|
function generateModuleInputs(count) {
|
||||||
|
const container = document.getElementById('module-position-inputs');
|
||||||
|
container.innerHTML = ''; // Clear existing inputs
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const moduleFieldset = document.createElement('fieldset');
|
||||||
|
moduleFieldset.className = 'module-input-group';
|
||||||
|
moduleFieldset.innerHTML = `
|
||||||
|
<legend>Module ${i + 1}</legend>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="module-${i}-name">Module Name</label>
|
||||||
|
<input type="text" id="module-${i}-name" value="Module ${i + 1}" 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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(moduleFieldset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createModuleDisplays(robot) {
|
||||||
|
const grid = document.getElementById('module-grid');
|
||||||
|
grid.innerHTML = ''; // Delete any pre-existing elements before creating new ones
|
||||||
|
|
||||||
|
const modules = robot.modules;
|
||||||
|
modules.forEach((module, i) => {
|
||||||
|
const article = document.createElement('article');
|
||||||
|
article.className = 'module-display';
|
||||||
|
const name = module.name;
|
||||||
|
|
||||||
|
article.innerHTML = `
|
||||||
|
<h3>${name}</h3>
|
||||||
|
<div class="readout">
|
||||||
|
<span class="label">Angle:</span>
|
||||||
|
<span id="module-${i}-angle" class="value">0.0°</span>
|
||||||
|
</div>
|
||||||
|
<div class="readout">
|
||||||
|
<span class="label">Speed:</span>
|
||||||
|
<span id="module-${i}-speed" class="value">0.00 pixels/s</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
grid.appendChild(article);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateModuleDisplays(robot) {
|
||||||
|
const configName = document.getElementById('config-name');
|
||||||
|
configName.textContent = robot.name;
|
||||||
|
const moduleCount = document.getElementById('module-count-display');
|
||||||
|
moduleCount.textContent = robot.modules.length;
|
||||||
|
|
||||||
|
// Update gyro heading display
|
||||||
|
const gyroHeadingDisplay = document.getElementById('gyro-heading-display');
|
||||||
|
if (gyroHeadingDisplay) {
|
||||||
|
const headingDeg = (robot.gyroHeading * 180 / Math.PI).toFixed(1);
|
||||||
|
gyroHeadingDisplay.textContent = `${headingDeg}°`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modules = robot.modules;
|
||||||
|
modules.forEach((module, i) => {
|
||||||
|
const angleElement = document.getElementById(`module-${i}-angle`);
|
||||||
|
const speedElement = document.getElementById(`module-${i}-speed`);
|
||||||
|
|
||||||
|
if (angleElement && speedElement) {
|
||||||
|
const angleDeg = (module.angle * 180 / Math.PI).toFixed(1);
|
||||||
|
angleElement.textContent = `${angleDeg}°`;
|
||||||
|
speedElement.textContent = `${module.speed.toFixed(2)} pixels/s`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* END DYNAMIC DOM FUNCTIONS
|
||||||
* BEGIN ANIMATION CODE
|
* BEGIN ANIMATION CODE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -255,12 +425,9 @@ const ctx = canvas.getContext('2d');
|
|||||||
// Get CSS variables for use in canvas
|
// Get CSS variables for use in canvas
|
||||||
const rootStyles = getComputedStyle(document.documentElement);
|
const rootStyles = getComputedStyle(document.documentElement);
|
||||||
|
|
||||||
function drawGrid(ctx, sideLength, gridSquareSize, xOffset, yOffset, rotation) {
|
function drawGrid(ctx, sideLength, gridSquareSize, xOffset, yOffset) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
// Apply rotation transform
|
|
||||||
ctx.rotate(-rotation);
|
|
||||||
|
|
||||||
ctx.strokeStyle = rootStyles.getPropertyValue('--grid-color');
|
ctx.strokeStyle = rootStyles.getPropertyValue('--grid-color');
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
const startX = (-sideLength / 2) - xOffset;
|
const startX = (-sideLength / 2) - xOffset;
|
||||||
@ -324,35 +491,54 @@ function drawModule(ctx, module) {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawRobot(ctx, robot) {
|
|
||||||
|
function drawRobot(ctx, robot, heading) {
|
||||||
|
ctx.save(); // Save current state before rotation
|
||||||
|
|
||||||
|
ctx.rotate(heading);
|
||||||
|
|
||||||
ctx.strokeStyle = rootStyles.getPropertyValue('--robot-frame-color')
|
ctx.strokeStyle = rootStyles.getPropertyValue('--robot-frame-color')
|
||||||
ctx.fillStyle = rootStyles.getPropertyValue('--robot-fill-color');
|
ctx.fillStyle = rootStyles.getPropertyValue('--robot-fill-color');
|
||||||
ctx.lineWidth = 4;
|
ctx.lineWidth = 4;
|
||||||
|
|
||||||
const modules = robot.modules.sort((a, b) => Math.atan2(a.position.y, a.position.x) - Math.atan2(b.position.y, b.position.x));
|
let hull = [];
|
||||||
|
// Get the convex hull of the robot if there are more than 3 modules
|
||||||
|
if (robot.modules.length > 3) {
|
||||||
|
const grahamScan = new GrahamScan();
|
||||||
|
grahamScan.setPoints(robot.modules.map((module) => [module.position.x, module.position.y]));
|
||||||
|
hull = grahamScan.getHull();
|
||||||
|
} else {
|
||||||
|
hull = robot.modules.map((module) => [module.position.x, module.position.y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Draw the convex hull as the robot frame
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(modules[0].position.x, modules[0].position.y);
|
ctx.moveTo(hull[0][0], hull[0][1]);
|
||||||
for (let i = 1; i < modules.length; i++) {
|
for (let i = 1; i < hull.length; i++) {
|
||||||
ctx.lineTo(modules[i].position.x, modules[i].position.y);
|
ctx.lineTo(hull[i][0], hull[i][1]);
|
||||||
}
|
}
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
modules.forEach(module => drawModule(ctx, module));
|
// Draw all modules (not just hull modules)
|
||||||
|
robot.modules.forEach(module => drawModule(ctx, module, heading));
|
||||||
|
|
||||||
|
ctx.restore(); // Restore to remove rotation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Initialize Variables
|
// Initialize Variables
|
||||||
const robotSize = 200;
|
const robotSize = 200;
|
||||||
const robot = new SwerveDrive(PresetConfigs.fourWheelSquare(robotSize));
|
const defaultModulePositions = PresetConfigs.fourWheelSquare(robotSize);
|
||||||
|
const robot = new SwerveDrive(defaultModulePositions, "4-Wheel Square");
|
||||||
|
createModuleDisplays(robot);
|
||||||
let xSpeed = 0;
|
let xSpeed = 0;
|
||||||
let ySpeed = 0;
|
let ySpeed = 0;
|
||||||
let turnSpeed = -1;
|
let turnSpeed = -1;
|
||||||
let robotRotation = 0; // Track cumulative robot rotation for grid display
|
|
||||||
|
|
||||||
let gridSquareSize = 25;
|
let gridSquareSize = 50;
|
||||||
let xGridOffset = 0;
|
let xGridOffset = 0;
|
||||||
let yGridOffset = 0;
|
let yGridOffset = 0;
|
||||||
robot.drive(xSpeed, ySpeed, 0, 500);
|
robot.drive(xSpeed, ySpeed, 0, 500);
|
||||||
@ -370,24 +556,20 @@ function animate() {
|
|||||||
|
|
||||||
// Animate the grid with robot movement
|
// Animate the grid with robot movement
|
||||||
let offsetSpeedDivisor = (100 - gridSquareSize <= 0 ? 1 : 100 - gridSquareSize);
|
let offsetSpeedDivisor = (100 - gridSquareSize <= 0 ? 1 : 100 - gridSquareSize);
|
||||||
robotRotation += turnSpeed * 0.01; // Scale factor for reasonable rotation speed
|
|
||||||
|
|
||||||
// Convert robot velocities to world velocities for grid movement
|
|
||||||
const cosRot = Math.cos(robotRotation);
|
|
||||||
const sinRot = Math.sin(robotRotation);
|
|
||||||
const worldVx = xSpeed * cosRot - ySpeed * sinRot;
|
|
||||||
const worldVy = xSpeed * sinRot + ySpeed * cosRot;
|
|
||||||
|
|
||||||
// Update grid offsets based on robot movement
|
// Update grid offsets based on robot movement
|
||||||
xGridOffset = (xGridOffset + (worldVx / offsetSpeedDivisor)) % gridSquareSize;
|
xGridOffset = (xGridOffset + (xSpeed / offsetSpeedDivisor)) % gridSquareSize;
|
||||||
yGridOffset = (yGridOffset + (worldVy / offsetSpeedDivisor)) % gridSquareSize;
|
yGridOffset = (yGridOffset + (ySpeed / offsetSpeedDivisor)) % gridSquareSize;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
// 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, robotRotation);
|
drawGrid(ctx, canvas.width * 2, gridSquareSize, xGridOffset, yGridOffset);
|
||||||
drawRobot(ctx, robot);
|
drawRobot(ctx, robot, robot.gyroHeading);
|
||||||
|
|
||||||
robot.drive(xSpeed, ySpeed, turnSpeed, parseFloat(maxSpeedSlider.value));
|
|
||||||
|
|
||||||
// Do it all over again
|
// Do it all over again
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|||||||
65
styles.css
65
styles.css
@ -296,4 +296,69 @@ button:hover {
|
|||||||
border-color: var(--accent-blue);
|
border-color: var(--accent-blue);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Module States Grid */
|
||||||
|
.module-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: var(--spacing-small);
|
||||||
|
margin-top: var(--spacing-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card {
|
||||||
|
background-color: var(--background-dark);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: var(--spacing-small);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-info {
|
||||||
|
background-color: var(--background-dark);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: var(--spacing-small);
|
||||||
|
margin-bottom: var(--spacing-small);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
margin-bottom: var(--spacing-small);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-inputs {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--spacing-small);
|
||||||
|
background-color: var(--background-dark);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
margin-top: var(--spacing-small);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-display {
|
||||||
|
background-color: var(--background-dark);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: var(--spacing-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.readout {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: calc(var(--spacing-small) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.readout .value {
|
||||||
|
color: var(--text-light);
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
148
vendor/lucio/graham-scan.mjs
vendored
Normal file
148
vendor/lucio/graham-scan.mjs
vendored
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
This module is not by me, it was found at the following github with MIT license:
|
||||||
|
https://github.com/luciopaiva/graham-scan/tree/master
|
||||||
|
=========
|
||||||
|
Copyright 2020 Lucio Paiva
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
=========
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
const X = 0;
|
||||||
|
const Y = 1;
|
||||||
|
const REMOVED = -1;
|
||||||
|
|
||||||
|
export default class GrahamScan {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
/** @type {[Number, Number][]} */
|
||||||
|
this.points = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.points = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getPoints() {
|
||||||
|
return this.points;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPoints(points) {
|
||||||
|
this.points = points.slice(); // copy
|
||||||
|
}
|
||||||
|
|
||||||
|
addPoint(point) {
|
||||||
|
this.points.push(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the smallest convex hull of a given set of points. Runs in O(n log n).
|
||||||
|
*
|
||||||
|
* @return {[Number, Number][]}
|
||||||
|
*/
|
||||||
|
getHull() {
|
||||||
|
const pivot = this.preparePivotPoint();
|
||||||
|
|
||||||
|
let indexes = Array.from(this.points, (point, i) => i);
|
||||||
|
const angles = Array.from(this.points, (point) => this.getAngle(pivot, point));
|
||||||
|
const distances = Array.from(this.points, (point) => this.euclideanDistanceSquared(pivot, point));
|
||||||
|
|
||||||
|
// sort by angle and distance
|
||||||
|
indexes.sort((i, j) => {
|
||||||
|
const angleA = angles[i];
|
||||||
|
const angleB = angles[j];
|
||||||
|
if (angleA === angleB) {
|
||||||
|
const distanceA = distances[i];
|
||||||
|
const distanceB = distances[j];
|
||||||
|
return distanceA - distanceB;
|
||||||
|
}
|
||||||
|
return angleA - angleB;
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove points with repeated angle (but never the pivot, so start from i=1)
|
||||||
|
for (let i = 1; i < indexes.length - 1; i++) {
|
||||||
|
if (angles[indexes[i]] === angles[indexes[i + 1]]) { // next one has same angle and is farther
|
||||||
|
indexes[i] = REMOVED; // remove it logically to avoid O(n) operation to physically remove it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hull = [];
|
||||||
|
for (let i = 0; i < indexes.length; i++) {
|
||||||
|
const index = indexes[i];
|
||||||
|
const point = this.points[index];
|
||||||
|
|
||||||
|
if (index !== REMOVED) {
|
||||||
|
if (hull.length < 3) {
|
||||||
|
hull.push(point);
|
||||||
|
} else {
|
||||||
|
while (this.checkOrientation(hull[hull.length - 2], hull[hull.length - 1], point) > 0) {
|
||||||
|
hull.pop();
|
||||||
|
}
|
||||||
|
hull.push(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hull.length < 3 ? [] : hull;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the orientation of 3 points in the order given.
|
||||||
|
*
|
||||||
|
* It works by comparing the slope of P1->P2 vs P2->P3. If P1->P2 > P2->P3, orientation is clockwise; if
|
||||||
|
* P1->P2 < P2->P3, counter-clockwise. If P1->P2 == P2->P3, points are co-linear.
|
||||||
|
*
|
||||||
|
* @param {[Number, Number]} p1
|
||||||
|
* @param {[Number, Number]} p2
|
||||||
|
* @param {[Number, Number]} p3
|
||||||
|
* @return {Number} positive if orientation is clockwise, negative if counter-clockwise, 0 if co-linear
|
||||||
|
*/
|
||||||
|
checkOrientation(p1, p2, p3) {
|
||||||
|
return (p2[Y] - p1[Y]) * (p3[X] - p2[X]) - (p3[Y] - p2[Y]) * (p2[X] - p1[X]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {[Number, Number]} a
|
||||||
|
* @param {[Number, Number]} b
|
||||||
|
* @return Number
|
||||||
|
*/
|
||||||
|
getAngle(a, b) {
|
||||||
|
return Math.atan2(b[Y] - a[Y], b[X] - a[X]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {[Number, Number]} p1
|
||||||
|
* @param {[Number, Number]} p2
|
||||||
|
* @return {Number}
|
||||||
|
*/
|
||||||
|
euclideanDistanceSquared(p1, p2) {
|
||||||
|
const a = p2[X] - p1[X];
|
||||||
|
const b = p2[Y] - p1[Y];
|
||||||
|
return a * a + b * b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @return {[Number, Number]}
|
||||||
|
*/
|
||||||
|
preparePivotPoint() {
|
||||||
|
let pivot = this.points[0];
|
||||||
|
let pivotIndex = 0;
|
||||||
|
for (let i = 1; i < this.points.length; i++) {
|
||||||
|
const point = this.points[i];
|
||||||
|
if (point[Y] < pivot[Y] || point[Y] === pivot[Y] && point[X] < pivot[X]) {
|
||||||
|
pivot = point;
|
||||||
|
pivotIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pivot;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user