|
| 1 | +class DynamicShootingWidget { |
| 2 | + constructor(divIdPrefix) { |
| 3 | + this.divIdPrefix = divIdPrefix; |
| 4 | + |
| 5 | + this.containerDiv = document.getElementById(divIdPrefix + "_container"); |
| 6 | + this.visualizationDrawDiv = document.getElementById(divIdPrefix + "_viz"); |
| 7 | + this.controlDrawDiv = document.getElementById(divIdPrefix + "_ctrls"); |
| 8 | + |
| 9 | + if (!this.containerDiv || !this.visualizationDrawDiv || !this.controlDrawDiv) { |
| 10 | + console.error('[DynamicShootingWidget] Missing required DOM elements!'); |
| 11 | + return; |
| 12 | + } |
| 13 | + |
| 14 | + // Initialize visualization |
| 15 | + this.visualization = new DynamicShootingVisualization(this.visualizationDrawDiv); |
| 16 | + |
| 17 | + // Default values |
| 18 | + this.robotVelocityX = 0.0; |
| 19 | + this.robotVelocityY = 0.0; |
| 20 | + this.numIterations = 10; // Number of iterations to compute and display |
| 21 | + this.currentIteration = 1; // Which iteration to highlight/display (1 to numIterations) |
| 22 | + this.projectileSpeed = 3.5; // m/s |
| 23 | + this.convergenceTolerance = 0.1; // meters |
| 24 | + |
| 25 | + // Set initial values on visualization |
| 26 | + this.visualization.setNumIterations(this.numIterations); |
| 27 | + this.visualization.setProjectileSpeed(this.projectileSpeed); |
| 28 | + this.visualization.setConvergenceTolerance(this.convergenceTolerance); |
| 29 | + |
| 30 | + // Build control table |
| 31 | + this.buildControlTable(divIdPrefix); |
| 32 | + |
| 33 | + // Make container square immediately |
| 34 | + if (this.containerDiv) { |
| 35 | + const width = this.containerDiv.offsetWidth || this.containerDiv.clientWidth; |
| 36 | + if (width > 0) { |
| 37 | + this.containerDiv.style.height = width + "px"; |
| 38 | + } |
| 39 | + } |
| 40 | + |
| 41 | + // Initial update - synchronous |
| 42 | + this.update(); |
| 43 | + } |
| 44 | + |
| 45 | + buildControlTable(divIdPrefix) { |
| 46 | + // Clear the old control table and use the control div for our control bar |
| 47 | + this.controlDrawDiv.innerHTML = ""; // Clear existing content |
| 48 | + this.controlDrawDiv.style.display = "flex"; // Use flex layout |
| 49 | + this.controlDrawDiv.style.alignItems = "center"; |
| 50 | + this.controlDrawDiv.style.justifyContent = "center"; |
| 51 | + this.controlDrawDiv.style.gap = "10px"; |
| 52 | + this.controlDrawDiv.style.padding = "5px"; |
| 53 | + this.controlDrawDiv.style.backgroundColor = "#f5f5f5"; |
| 54 | + this.controlDrawDiv.style.borderTop = "1px solid #ddd"; |
| 55 | + this.controlDrawDiv.style.width = "100%"; // Full width |
| 56 | + this.controlDrawDiv.style.flexShrink = "0"; // Don't shrink |
| 57 | + this.controlDrawDiv.style.flexBasis = "auto"; // Natural height |
| 58 | + this.controlDrawDiv.style.order = "2"; // Ensure it appears after visualization |
| 59 | + this.controlDrawDiv.style.position = "relative"; // Proper positioning |
| 60 | + this.controlDrawDiv.style.zIndex = "10"; // Ensure it's above canvas elements |
| 61 | + this.controlDrawDiv.style.boxSizing = "border-box"; // Include padding in size |
| 62 | + |
| 63 | + // Create a tiny control bar - we'll add elements directly to controlDrawDiv |
| 64 | + const controlBar = this.controlDrawDiv; |
| 65 | + |
| 66 | + // Iteration label |
| 67 | + const iterationLabel = document.createElement("label"); |
| 68 | + iterationLabel.innerHTML = "Iterations:"; |
| 69 | + iterationLabel.style.cssText = "font-size: 12px; margin-right: 5px;"; |
| 70 | + controlBar.appendChild(iterationLabel); |
| 71 | + |
| 72 | + // Previous button |
| 73 | + const prevButton = document.createElement("button"); |
| 74 | + prevButton.innerHTML = "◀"; |
| 75 | + prevButton.style.cssText = "padding: 2px 8px; font-size: 12px; cursor: pointer;"; |
| 76 | + prevButton.onclick = function() { |
| 77 | + if (this.currentIteration > 1) { |
| 78 | + this.currentIteration--; |
| 79 | + if (this.iterationInput) { |
| 80 | + this.iterationInput.value = this.currentIteration; |
| 81 | + } |
| 82 | + this.update(); |
| 83 | + } |
| 84 | + }.bind(this); |
| 85 | + controlBar.appendChild(prevButton); |
| 86 | + |
| 87 | + // Iteration number input (for which iteration to display) |
| 88 | + const input = document.createElement("INPUT"); |
| 89 | + input.setAttribute("type", "number"); |
| 90 | + input.setAttribute("min", "1"); |
| 91 | + input.setAttribute("max", this.numIterations.toString()); |
| 92 | + input.setAttribute("step", "1"); |
| 93 | + input.setAttribute("value", "1"); |
| 94 | + input.setAttribute("id", divIdPrefix + "_iteration"); |
| 95 | + input.style.cssText = "width: 50px; padding: 2px; font-size: 12px; text-align: center;"; |
| 96 | + |
| 97 | + // Store reference to input for syncing |
| 98 | + this.iterationInput = input; |
| 99 | + |
| 100 | + input.onchange = function (event) { |
| 101 | + let val = parseInt(event.target.value); |
| 102 | + if (isNaN(val) || val < 1) val = 1; |
| 103 | + if (val > this.numIterations) val = this.numIterations; |
| 104 | + this.currentIteration = val; |
| 105 | + event.target.value = val; |
| 106 | + this.update(); |
| 107 | + }.bind(this); |
| 108 | + |
| 109 | + input.oninput = function (event) { |
| 110 | + let val = parseInt(event.target.value); |
| 111 | + if (!isNaN(val) && val >= 1 && val <= this.numIterations) { |
| 112 | + this.currentIteration = val; |
| 113 | + this.update(); |
| 114 | + } |
| 115 | + }.bind(this); |
| 116 | + |
| 117 | + controlBar.appendChild(input); |
| 118 | + |
| 119 | + // Next button |
| 120 | + const nextButton = document.createElement("button"); |
| 121 | + nextButton.innerHTML = "▶"; |
| 122 | + nextButton.style.cssText = "padding: 2px 8px; font-size: 12px; cursor: pointer;"; |
| 123 | + nextButton.onclick = function() { |
| 124 | + if (this.currentIteration < this.numIterations) { |
| 125 | + this.currentIteration++; |
| 126 | + if (this.iterationInput) { |
| 127 | + this.iterationInput.value = this.currentIteration; |
| 128 | + } |
| 129 | + this.update(); |
| 130 | + } |
| 131 | + }.bind(this); |
| 132 | + controlBar.appendChild(nextButton); |
| 133 | + |
| 134 | + // Add separator |
| 135 | + const separator = document.createElement("span"); |
| 136 | + separator.innerHTML = "|"; |
| 137 | + separator.style.cssText = "margin: 0 10px; color: #999; font-size: 12px;"; |
| 138 | + controlBar.appendChild(separator); |
| 139 | + |
| 140 | + // Projectile speed label |
| 141 | + const speedLabel = document.createElement("label"); |
| 142 | + speedLabel.innerHTML = "Projectile Speed (m/s):"; |
| 143 | + speedLabel.style.cssText = "font-size: 12px; margin-right: 5px;"; |
| 144 | + controlBar.appendChild(speedLabel); |
| 145 | + |
| 146 | + // Projectile speed slider |
| 147 | + const speedSlider = document.createElement("INPUT"); |
| 148 | + speedSlider.setAttribute("type", "range"); |
| 149 | + speedSlider.setAttribute("min", "0.1"); |
| 150 | + speedSlider.setAttribute("max", "20.0"); |
| 151 | + speedSlider.setAttribute("step", "0.1"); |
| 152 | + speedSlider.setAttribute("value", this.projectileSpeed.toString()); |
| 153 | + speedSlider.setAttribute("id", divIdPrefix + "_projectile_speed"); |
| 154 | + speedSlider.style.cssText = "width: 120px; height: 20px; margin: 0 5px; cursor: pointer;"; |
| 155 | + |
| 156 | + // Value display |
| 157 | + const speedValueDisplay = document.createElement("span"); |
| 158 | + speedValueDisplay.innerHTML = this.projectileSpeed.toFixed(1); |
| 159 | + speedValueDisplay.style.cssText = "font-size: 12px; min-width: 35px; text-align: center; display: inline-block;"; |
| 160 | + |
| 161 | + // Store references |
| 162 | + this.speedSlider = speedSlider; |
| 163 | + this.speedValueDisplay = speedValueDisplay; |
| 164 | + |
| 165 | + speedSlider.oninput = function (event) { |
| 166 | + let val = parseFloat(event.target.value); |
| 167 | + if (!isNaN(val) && val >= 0.1 && val <= 20.0) { |
| 168 | + this.projectileSpeed = val; |
| 169 | + this.speedValueDisplay.innerHTML = val.toFixed(1); |
| 170 | + this.update(); |
| 171 | + } |
| 172 | + }.bind(this); |
| 173 | + |
| 174 | + speedSlider.onchange = function (event) { |
| 175 | + let val = parseFloat(event.target.value); |
| 176 | + if (isNaN(val) || val < 0.1) val = 0.1; |
| 177 | + if (val > 20.0) val = 20.0; |
| 178 | + this.projectileSpeed = val; |
| 179 | + this.speedValueDisplay.innerHTML = val.toFixed(1); |
| 180 | + this.update(); |
| 181 | + }.bind(this); |
| 182 | + |
| 183 | + controlBar.appendChild(speedSlider); |
| 184 | + controlBar.appendChild(speedValueDisplay); |
| 185 | + |
| 186 | + // Add separator |
| 187 | + const separator2 = document.createElement("span"); |
| 188 | + separator2.innerHTML = "|"; |
| 189 | + separator2.style.cssText = "margin: 0 10px; color: #999; font-size: 12px;"; |
| 190 | + controlBar.appendChild(separator2); |
| 191 | + |
| 192 | + // Tolerance label |
| 193 | + const toleranceLabel = document.createElement("label"); |
| 194 | + toleranceLabel.innerHTML = "Tolerance (m):"; |
| 195 | + toleranceLabel.style.cssText = "font-size: 12px; margin-right: 5px;"; |
| 196 | + controlBar.appendChild(toleranceLabel); |
| 197 | + |
| 198 | + // Tolerance slider |
| 199 | + const toleranceSlider = document.createElement("INPUT"); |
| 200 | + toleranceSlider.setAttribute("type", "range"); |
| 201 | + toleranceSlider.setAttribute("min", "0.01"); |
| 202 | + toleranceSlider.setAttribute("max", "1.0"); |
| 203 | + toleranceSlider.setAttribute("step", "0.01"); |
| 204 | + toleranceSlider.setAttribute("value", this.convergenceTolerance.toString()); |
| 205 | + toleranceSlider.setAttribute("id", divIdPrefix + "_tolerance"); |
| 206 | + toleranceSlider.style.cssText = "width: 120px; height: 20px; margin: 0 5px; cursor: pointer;"; |
| 207 | + |
| 208 | + // Value display |
| 209 | + const toleranceValueDisplay = document.createElement("span"); |
| 210 | + toleranceValueDisplay.innerHTML = this.convergenceTolerance.toFixed(2); |
| 211 | + toleranceValueDisplay.style.cssText = "font-size: 12px; min-width: 40px; text-align: center; display: inline-block;"; |
| 212 | + |
| 213 | + // Store references |
| 214 | + this.toleranceSlider = toleranceSlider; |
| 215 | + this.toleranceValueDisplay = toleranceValueDisplay; |
| 216 | + |
| 217 | + toleranceSlider.oninput = function (event) { |
| 218 | + let val = parseFloat(event.target.value); |
| 219 | + if (!isNaN(val) && val >= 0.01 && val <= 1.0) { |
| 220 | + this.convergenceTolerance = val; |
| 221 | + this.toleranceValueDisplay.innerHTML = val.toFixed(2); |
| 222 | + this.update(); |
| 223 | + } |
| 224 | + }.bind(this); |
| 225 | + |
| 226 | + toleranceSlider.onchange = function (event) { |
| 227 | + let val = parseFloat(event.target.value); |
| 228 | + if (isNaN(val) || val < 0.01) val = 0.01; |
| 229 | + if (val > 1.0) val = 1.0; |
| 230 | + this.convergenceTolerance = val; |
| 231 | + this.toleranceValueDisplay.innerHTML = val.toFixed(2); |
| 232 | + this.update(); |
| 233 | + }.bind(this); |
| 234 | + |
| 235 | + controlBar.appendChild(toleranceSlider); |
| 236 | + controlBar.appendChild(toleranceValueDisplay); |
| 237 | + |
| 238 | + // Control bar is already in the right place (controlDrawDiv is in the flex-grid) |
| 239 | + // Make sure the visualization div doesn't expand into the control area |
| 240 | + if (this.visualizationDrawDiv.parentNode) { |
| 241 | + // Ensure the parent flex-grid uses column layout if needed |
| 242 | + const flexGrid = this.visualizationDrawDiv.parentNode; |
| 243 | + if (flexGrid.classList && flexGrid.classList.contains('flex-grid')) { |
| 244 | + // Make flex-grid use column layout to stack vertically |
| 245 | + flexGrid.style.flexDirection = "column"; |
| 246 | + flexGrid.style.alignItems = "stretch"; // Stretch children to full width |
| 247 | + flexGrid.style.height = "100%"; // Fill container height |
| 248 | + flexGrid.style.width = "100%"; // Fill container width |
| 249 | + flexGrid.style.margin = "0"; // Remove default margin |
| 250 | + flexGrid.style.padding = "0"; // Remove padding |
| 251 | + flexGrid.style.boxSizing = "border-box"; // Include borders in size |
| 252 | + |
| 253 | + // Override the .col class constraints - let it grow to fill available space |
| 254 | + this.visualizationDrawDiv.style.minHeight = "0"; // Remove min-height constraint |
| 255 | + this.visualizationDrawDiv.style.maxHeight = "none"; // Remove max-height constraint |
| 256 | + this.visualizationDrawDiv.style.height = "auto"; // Let it grow |
| 257 | + this.visualizationDrawDiv.style.flex = "1 1 auto"; // Grow to fill available space |
| 258 | + this.visualizationDrawDiv.style.overflow = "hidden"; // Prevent overflow |
| 259 | + this.visualizationDrawDiv.style.position = "relative"; // Ensure proper positioning |
| 260 | + this.visualizationDrawDiv.style.order = "1"; // Ensure it appears before controls |
| 261 | + this.visualizationDrawDiv.style.width = "100%"; // Full width |
| 262 | + this.visualizationDrawDiv.style.boxSizing = "border-box"; // Include padding in size |
| 263 | + |
| 264 | + // Ensure control div is properly positioned |
| 265 | + this.controlDrawDiv.style.flex = "0 0 auto"; // Don't grow/shrink |
| 266 | + this.controlDrawDiv.style.order = "2"; // Ensure it appears after visualization |
| 267 | + } |
| 268 | + } |
| 269 | + |
| 270 | + // Make the container div square based on its width |
| 271 | + // This ensures the bounding box is square and the visualization can fill it |
| 272 | + if (this.containerDiv) { |
| 273 | + this.containerDiv.style.overflow = "hidden"; // Prevent container overflow |
| 274 | + this.containerDiv.style.width = "100%"; // Full width |
| 275 | + |
| 276 | + // Make container square: set height to match width |
| 277 | + const makeSquare = () => { |
| 278 | + const width = this.containerDiv.offsetWidth || this.containerDiv.clientWidth; |
| 279 | + if (width > 0) { |
| 280 | + this.containerDiv.style.height = width + "px"; |
| 281 | + if (this.visualization) { |
| 282 | + this.visualization.updateSize(); |
| 283 | + this.update(); |
| 284 | + } |
| 285 | + } |
| 286 | + }; |
| 287 | + |
| 288 | + // Update on resize |
| 289 | + window.addEventListener("resize", makeSquare); |
| 290 | + } |
| 291 | + |
| 292 | + // Set up velocity callback for drag-to-update |
| 293 | + this.visualization.setVelocityCallback((velX, velY) => { |
| 294 | + this.robotVelocityX = velX; |
| 295 | + this.robotVelocityY = velY; |
| 296 | + this.update(); |
| 297 | + }); |
| 298 | + } |
| 299 | + |
| 300 | + update() { |
| 301 | + // Update visualization with current values |
| 302 | + this.visualization.setRobotVelocity(this.robotVelocityX, this.robotVelocityY); |
| 303 | + this.visualization.setCurrentIteration(this.currentIteration); |
| 304 | + this.visualization.setNumIterations(this.numIterations); |
| 305 | + this.visualization.setProjectileSpeed(this.projectileSpeed); |
| 306 | + this.visualization.setConvergenceTolerance(this.convergenceTolerance); |
| 307 | + this.visualization.update(); |
| 308 | + |
| 309 | + // Sync iteration input field |
| 310 | + if (this.iterationInput) { |
| 311 | + this.iterationInput.value = this.currentIteration; |
| 312 | + } |
| 313 | + |
| 314 | + // Sync speed slider and value display |
| 315 | + if (this.speedSlider) { |
| 316 | + this.speedSlider.value = this.projectileSpeed.toString(); |
| 317 | + } |
| 318 | + if (this.speedValueDisplay) { |
| 319 | + this.speedValueDisplay.innerHTML = this.projectileSpeed.toFixed(1); |
| 320 | + } |
| 321 | + |
| 322 | + // Sync tolerance slider and value display |
| 323 | + if (this.toleranceSlider) { |
| 324 | + this.toleranceSlider.value = this.convergenceTolerance.toString(); |
| 325 | + } |
| 326 | + if (this.toleranceValueDisplay) { |
| 327 | + this.toleranceValueDisplay.innerHTML = this.convergenceTolerance.toFixed(2); |
| 328 | + } |
| 329 | + } |
| 330 | +} |
0 commit comments