base.js 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040
  1. // Player status bar functionality - Updated to fix logMessage errors
  2. // Pattern files cache for improved performance
  3. let patternFilesCache = null;
  4. let patternFilesCacheTimestamp = null;
  5. const PATTERN_CACHE_EXPIRY = 30000; // 30 seconds cache
  6. // Function to get cached pattern files or fetch fresh data
  7. async function getCachedPatternFiles(forceRefresh = false) {
  8. const now = Date.now();
  9. // Check if cache is valid and not forced to refresh
  10. if (!forceRefresh && patternFilesCache && patternFilesCacheTimestamp &&
  11. (now - patternFilesCacheTimestamp) < PATTERN_CACHE_EXPIRY) {
  12. return patternFilesCache;
  13. }
  14. try {
  15. const response = await fetch('/list_theta_rho_files');
  16. if (!response.ok) {
  17. throw new Error(`Failed to fetch pattern files: ${response.status}`);
  18. }
  19. const files = await response.json();
  20. patternFilesCache = files;
  21. patternFilesCacheTimestamp = now;
  22. return files;
  23. } catch (error) {
  24. console.error('Error fetching pattern files:', error);
  25. // Return cached data if available, even if expired
  26. return patternFilesCache || [];
  27. }
  28. }
  29. // Function to invalidate pattern files cache
  30. function invalidatePatternFilesCache() {
  31. patternFilesCache = null;
  32. patternFilesCacheTimestamp = null;
  33. }
  34. // Helper function to normalize file paths for cross-platform compatibility
  35. function normalizeFilePath(filePath) {
  36. if (!filePath) return '';
  37. // First normalize path separators
  38. let normalized = filePath.replace(/\\/g, '/');
  39. // Remove only the patterns directory prefix, not patterns within the path
  40. if (normalized.startsWith('./patterns/')) {
  41. normalized = normalized.substring(11);
  42. } else if (normalized.startsWith('patterns/')) {
  43. normalized = normalized.substring(9);
  44. }
  45. return normalized;
  46. }
  47. let ws = null;
  48. let reconnectAttempts = 0;
  49. const maxReconnectAttempts = 5;
  50. const reconnectDelay = 3000; // 3 seconds
  51. let isEditingSpeed = false; // Track if user is editing speed
  52. let playerPreviewData = null; // Store the current pattern's preview data for modal
  53. let playerPreviewCtx = null; // Store the canvas context for modal preview
  54. let playerAnimationId = null; // Store animation frame ID for modal
  55. let lastProgress = 0; // Last known progress from backend
  56. let targetProgress = 0; // Target progress to animate towards
  57. let animationStartTime = 0; // Start time of current animation
  58. let animationDuration = 1000; // Duration of interpolation in ms
  59. let smoothAnimationStartTime = 0; // Start time for smooth coordinate animation
  60. let smoothAnimationActive = false; // Whether smooth animation is running
  61. let modalAnimationId = null; // Store animation frame ID for modal
  62. let modalLastProgress = 0; // Last known progress for modal
  63. let modalTargetProgress = 0; // Target progress for modal
  64. let modalAnimationStartTime = 0; // Start time for modal animation
  65. let userDismissedModal = false; // Track if user has manually dismissed the modal
  66. // Function to set modal visibility
  67. function setModalVisibility(show, userAction = false) {
  68. const modal = document.getElementById('playerPreviewModal');
  69. if (!modal) return;
  70. if (show) {
  71. modal.classList.remove('hidden');
  72. } else {
  73. modal.classList.add('hidden');
  74. }
  75. if (userAction) {
  76. userDismissedModal = !show;
  77. }
  78. }
  79. let currentPreviewFile = null; // Track the current file for preview data
  80. function connectWebSocket() {
  81. if (ws) {
  82. ws.close();
  83. }
  84. ws = new WebSocket(`ws://${window.location.host}/ws/status`);
  85. ws.onopen = function() {
  86. console.log("WebSocket connection established");
  87. reconnectAttempts = 0;
  88. };
  89. ws.onclose = function() {
  90. console.log("WebSocket connection closed");
  91. if (reconnectAttempts < maxReconnectAttempts) {
  92. reconnectAttempts++;
  93. setTimeout(connectWebSocket, reconnectDelay);
  94. }
  95. };
  96. ws.onerror = function(error) {
  97. console.error("WebSocket error:", error);
  98. };
  99. ws.onmessage = function(event) {
  100. try {
  101. const data = JSON.parse(event.data);
  102. if (data.type === 'status_update') {
  103. // Update modal status with the full data
  104. syncModalControls(data.data);
  105. // Update speed input field on table control page if it exists
  106. if (data.data && data.data.speed) {
  107. const currentSpeedDisplay = document.getElementById('currentSpeedDisplay');
  108. if (currentSpeedDisplay) {
  109. currentSpeedDisplay.textContent = `${data.data.speed} mm/s`;
  110. }
  111. }
  112. // Update connection status dot using 'connection_status' or fallback to 'connected'
  113. if (data.data.hasOwnProperty('connection_status')) {
  114. updateConnectionStatus(data.data.connection_status);
  115. }
  116. // Check if current file has changed and reload preview data if needed
  117. if (data.data.current_file) {
  118. const newFile = normalizeFilePath(data.data.current_file);
  119. if (newFile !== currentPreviewFile) {
  120. currentPreviewFile = newFile;
  121. loadPlayerPreviewData(data.data.current_file);
  122. }
  123. } else {
  124. currentPreviewFile = null;
  125. playerPreviewData = null;
  126. }
  127. // Update progress for modal animation with smooth interpolation
  128. if (playerPreviewData && data.data.progress && data.data.progress.percentage !== null) {
  129. const newProgress = data.data.progress.percentage / 100;
  130. targetProgress = newProgress;
  131. // Update modal if open with smooth animation
  132. const modal = document.getElementById('playerPreviewModal');
  133. if (modal && !modal.classList.contains('hidden')) {
  134. updateModalPreviewSmooth(newProgress);
  135. }
  136. }
  137. // Reset userDismissedModal flag if no pattern is playing
  138. if (!data.data.current_file || !data.data.is_running) {
  139. userDismissedModal = false;
  140. }
  141. }
  142. } catch (error) {
  143. console.error("Error processing WebSocket message:", error);
  144. }
  145. };
  146. }
  147. function updateConnectionStatus(isConnected) {
  148. const statusDot = document.getElementById("connectionStatusDot");
  149. if (statusDot) {
  150. // Update dot color
  151. statusDot.className = `inline-block size-2 rounded-full ml-2 align-middle ${
  152. isConnected ? "bg-green-500" : "bg-red-500"
  153. }`;
  154. }
  155. }
  156. // Setup player preview with expand button
  157. function setupPlayerPreview() {
  158. const previewContainer = document.getElementById('player-pattern-preview');
  159. if (!previewContainer) return;
  160. // Get current background image URL
  161. const currentBgImage = previewContainer.style.backgroundImage;
  162. // Clear container
  163. previewContainer.innerHTML = '';
  164. previewContainer.style.backgroundImage = '';
  165. // Create preview image container
  166. const imageContainer = document.createElement('div');
  167. imageContainer.className = 'relative aspect-square rounded-full overflow-hidden w-full h-full';
  168. // Create image element
  169. const img = document.createElement('img');
  170. img.className = 'w-full h-full object-cover';
  171. // img.alt = 'Pattern Preview';
  172. // Extract URL from background-image CSS
  173. img.src = currentBgImage.replace(/^url\(['"](.+)['"]\)$/, '$1');
  174. // Add expand button overlay
  175. const expandOverlay = document.createElement('div');
  176. expandOverlay.className = 'absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity duration-200 cursor-pointer z-20 bg-black bg-opacity-20 hover:bg-opacity-30';
  177. expandOverlay.innerHTML = '<div class="bg-white rounded-full p-3 shadow-lg flex items-center justify-center w-12 h-12"><span class="material-icons text-xl text-gray-800">fullscreen</span></div>';
  178. // Add click handler for expand button
  179. expandOverlay.addEventListener('click', (e) => {
  180. e.stopPropagation();
  181. openPlayerPreviewModal();
  182. });
  183. // Add image and overlay to image container
  184. imageContainer.appendChild(img);
  185. imageContainer.appendChild(expandOverlay);
  186. // Add image container to preview container
  187. previewContainer.appendChild(imageContainer);
  188. }
  189. // Open player preview modal
  190. async function openPlayerPreviewModal() {
  191. try {
  192. const modal = document.getElementById('playerPreviewModal');
  193. const title = document.getElementById('playerPreviewTitle');
  194. const canvas = document.getElementById('playerPreviewCanvas');
  195. const ctx = canvas.getContext('2d');
  196. const toggleBtn = document.getElementById('toggle-preview-modal-btn');
  197. // Set static title
  198. title.textContent = 'Live Pattern Preview';
  199. // Show modal and update toggle button
  200. modal.classList.remove('hidden');
  201. // Setup canvas
  202. setupPlayerPreviewCanvas(ctx);
  203. // Draw initial state
  204. drawPlayerPreview(ctx, targetProgress);
  205. } catch (error) {
  206. console.error(`Error opening player preview modal: ${error.message}`);
  207. showStatusMessage('Failed to load pattern for animation', 'error');
  208. }
  209. }
  210. // Setup player preview canvas for modal
  211. function setupPlayerPreviewCanvas(ctx) {
  212. const canvas = ctx.canvas;
  213. const container = canvas.parentElement; // This is the div with max-w and max-h constraints
  214. const modal = document.getElementById('playerPreviewModal');
  215. if (!container || !modal) return;
  216. // Calculate available viewport space directly
  217. const viewportWidth = window.innerWidth;
  218. const viewportHeight = window.innerHeight;
  219. // Calculate maximum canvas size based on viewport and fixed estimates
  220. // Modal uses max-w-5xl (1024px) but we want to be responsive to actual viewport
  221. const modalMaxWidth = Math.min(1024, viewportWidth * 0.9); // Account for modal padding
  222. const modalMaxHeight = viewportHeight * 0.95; // max-h-[95vh]
  223. // Reserve space for modal header (~80px) and controls (~200px) and padding
  224. const reservedSpace = 320; // Header + controls + padding
  225. const availableModalHeight = modalMaxHeight - reservedSpace;
  226. // Calculate canvas constraints (stay within original 800px max, but be responsive)
  227. const maxCanvasSize = Math.min(800, modalMaxWidth - 64, availableModalHeight); // 64px for canvas area padding
  228. // Ensure minimum size
  229. const finalSize = Math.max(200, maxCanvasSize);
  230. // Update container to exact size (override CSS constraints)
  231. container.style.width = `${finalSize}px`;
  232. container.style.height = `${finalSize}px`;
  233. container.style.maxWidth = `${finalSize}px`;
  234. container.style.maxHeight = `${finalSize}px`;
  235. container.style.minWidth = `${finalSize}px`;
  236. container.style.minHeight = `${finalSize}px`;
  237. // Set the internal canvas size for high-DPI rendering
  238. const pixelRatio = (window.devicePixelRatio || 1) * 2;
  239. canvas.width = finalSize * pixelRatio;
  240. canvas.height = finalSize * pixelRatio;
  241. // Set the display size (canvas fills its container)
  242. canvas.style.width = '100%';
  243. canvas.style.height = '100%';
  244. console.log('Canvas resized:', {
  245. viewport: `${viewportWidth}x${viewportHeight}`,
  246. modalMaxWidth,
  247. availableModalHeight,
  248. finalSize: finalSize
  249. });
  250. }
  251. // Get interpolated coordinate at specific progress
  252. function getInterpolatedCoordinate(progress) {
  253. if (!playerPreviewData || playerPreviewData.length === 0) return null;
  254. const totalPoints = playerPreviewData.length;
  255. const exactIndex = progress * (totalPoints - 1);
  256. const index = Math.floor(exactIndex);
  257. const fraction = exactIndex - index;
  258. // Ensure we don't go out of bounds
  259. if (index >= totalPoints - 1) {
  260. return playerPreviewData[totalPoints - 1];
  261. }
  262. if (index < 0) {
  263. return playerPreviewData[0];
  264. }
  265. // Get the two coordinates to interpolate between
  266. const [theta1, rho1] = playerPreviewData[index];
  267. const [theta2, rho2] = playerPreviewData[index + 1];
  268. // Interpolate theta (handle angle wrapping)
  269. let deltaTheta = theta2 - theta1;
  270. if (deltaTheta > Math.PI) deltaTheta -= 2 * Math.PI;
  271. if (deltaTheta < -Math.PI) deltaTheta += 2 * Math.PI;
  272. const interpolatedTheta = theta1 + deltaTheta * fraction;
  273. const interpolatedRho = rho1 + (rho2 - rho1) * fraction;
  274. return [interpolatedTheta, interpolatedRho];
  275. }
  276. // Draw player preview for modal
  277. function drawPlayerPreview(ctx, progress) {
  278. if (!ctx || !playerPreviewData || playerPreviewData.length === 0) return;
  279. const canvas = ctx.canvas;
  280. const pixelRatio = (window.devicePixelRatio || 1) * 2;
  281. const containerSize = canvas.width / pixelRatio;
  282. const center = containerSize / 2;
  283. const scale = (containerSize / 2) - 30;
  284. ctx.save();
  285. // Clear canvas for fresh drawing
  286. ctx.clearRect(0, 0, canvas.width, canvas.height);
  287. // Create circular clipping path
  288. ctx.beginPath();
  289. ctx.arc(canvas.width/2, canvas.height/2, canvas.width/2, 0, Math.PI * 2);
  290. ctx.clip();
  291. // Setup coordinate system for drawing
  292. ctx.scale(pixelRatio, pixelRatio);
  293. // Calculate how many points to draw
  294. const totalPoints = playerPreviewData.length;
  295. const pointsToDraw = Math.floor(totalPoints * progress);
  296. if (pointsToDraw < 2) {
  297. ctx.restore();
  298. return;
  299. }
  300. // Draw the pattern
  301. ctx.beginPath();
  302. ctx.strokeStyle = '#808080';
  303. ctx.lineWidth = 1;
  304. ctx.lineCap = 'round';
  305. ctx.lineJoin = 'round';
  306. // Enable high quality rendering
  307. ctx.imageSmoothingEnabled = true;
  308. ctx.imageSmoothingQuality = 'high';
  309. // Draw pattern lines up to the last complete segment
  310. for (let i = 0; i < pointsToDraw - 1; i++) {
  311. const [theta1, rho1] = playerPreviewData[i];
  312. const [theta2, rho2] = playerPreviewData[i + 1];
  313. const x1 = center + rho1 * scale * Math.cos(theta1);
  314. const y1 = center + rho1 * scale * Math.sin(theta1);
  315. const x2 = center + rho2 * scale * Math.cos(theta2);
  316. const y2 = center + rho2 * scale * Math.sin(theta2);
  317. if (i === 0) {
  318. ctx.moveTo(x1, y1);
  319. }
  320. ctx.lineTo(x2, y2);
  321. }
  322. // Draw the final partial segment to the interpolated position
  323. if (pointsToDraw > 0) {
  324. const interpolatedCoord = getInterpolatedCoordinate(progress);
  325. if (interpolatedCoord && pointsToDraw > 1) {
  326. // Get the last complete coordinate
  327. const [lastTheta, lastRho] = playerPreviewData[pointsToDraw - 1];
  328. const lastX = center + lastRho * scale * Math.cos(lastTheta);
  329. const lastY = center + lastRho * scale * Math.sin(lastTheta);
  330. // Draw line to interpolated position
  331. const [interpTheta, interpRho] = interpolatedCoord;
  332. const interpX = center + interpRho * scale * Math.cos(interpTheta);
  333. const interpY = center + interpRho * scale * Math.sin(interpTheta);
  334. ctx.lineTo(interpX, interpY);
  335. }
  336. }
  337. ctx.stroke();
  338. // Draw current position dot with interpolated position
  339. if (pointsToDraw > 0) {
  340. const interpolatedCoord = getInterpolatedCoordinate(progress);
  341. if (interpolatedCoord) {
  342. const [theta, rho] = interpolatedCoord;
  343. const x = center + rho * scale * Math.cos(theta);
  344. const y = center + rho * scale * Math.sin(theta);
  345. // Draw white border
  346. ctx.beginPath();
  347. ctx.fillStyle = '#ffffff';
  348. ctx.arc(x, y, 7.5, 0, Math.PI * 2);
  349. ctx.fill();
  350. // Draw red dot
  351. ctx.beginPath();
  352. ctx.fillStyle = '#ff0000';
  353. ctx.arc(x, y, 6, 0, Math.PI * 2);
  354. ctx.fill();
  355. }
  356. }
  357. ctx.restore();
  358. }
  359. // Load pattern coordinates for player preview
  360. async function loadPlayerPreviewData(pattern) {
  361. try {
  362. const response = await fetch('/get_theta_rho_coordinates', {
  363. method: 'POST',
  364. headers: { 'Content-Type': 'application/json' },
  365. body: JSON.stringify({ file_name: pattern })
  366. });
  367. if (!response.ok) {
  368. throw new Error(`HTTP error! status: ${response.status}`);
  369. }
  370. const data = await response.json();
  371. if (data.error) {
  372. throw new Error(data.error);
  373. }
  374. playerPreviewData = data.coordinates;
  375. // Store the filename for comparison
  376. playerPreviewData.fileName = normalizeFilePath(pattern);
  377. } catch (error) {
  378. console.error(`Error loading player preview data: ${error.message}`);
  379. playerPreviewData = null;
  380. }
  381. }
  382. // Ultra-smooth animation function for modal
  383. function animateModalPreview() {
  384. const modal = document.getElementById('playerPreviewModal');
  385. if (!modal || modal.classList.contains('hidden')) return;
  386. const canvas = document.getElementById('playerPreviewCanvas');
  387. const ctx = canvas.getContext('2d');
  388. if (!ctx || !playerPreviewData) return;
  389. const currentTime = Date.now();
  390. const elapsed = currentTime - modalAnimationStartTime;
  391. const totalDuration = animationDuration;
  392. // Calculate smooth progress with easing
  393. const rawProgress = Math.min(elapsed / totalDuration, 1);
  394. const easeProgress = rawProgress < 0.5
  395. ? 2 * rawProgress * rawProgress
  396. : 1 - Math.pow(-2 * rawProgress + 2, 2) / 2;
  397. // Interpolate between last and target progress
  398. const currentProgress = modalLastProgress + (modalTargetProgress - modalLastProgress) * easeProgress;
  399. // Draw the pattern up to current progress
  400. drawPlayerPreview(ctx, currentProgress);
  401. // Continue animation if not at target
  402. if (rawProgress < 1) {
  403. modalAnimationId = requestAnimationFrame(animateModalPreview);
  404. }
  405. }
  406. // Update modal preview with smooth animation
  407. function updateModalPreviewSmooth(newProgress) {
  408. if (newProgress === modalTargetProgress) return; // No change needed
  409. // Stop any existing animation
  410. if (modalAnimationId) {
  411. cancelAnimationFrame(modalAnimationId);
  412. }
  413. // Update animation parameters
  414. modalLastProgress = modalTargetProgress;
  415. modalTargetProgress = newProgress;
  416. modalAnimationStartTime = Date.now();
  417. // Start smooth animation
  418. animateModalPreview();
  419. }
  420. // Setup player preview modal events
  421. function setupPlayerPreviewModalEvents() {
  422. const modal = document.getElementById('playerPreviewModal');
  423. const closeBtn = document.getElementById('closePlayerPreview');
  424. const toggleBtn = document.getElementById('toggle-preview-modal-btn');
  425. if (!modal || !closeBtn || !toggleBtn) return;
  426. // Remove any existing event listeners to prevent conflicts
  427. const newToggleBtn = toggleBtn.cloneNode(true);
  428. toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
  429. // Toggle button click handler
  430. newToggleBtn.addEventListener('click', () => {
  431. const isHidden = modal.classList.contains('hidden');
  432. if (isHidden) {
  433. openPlayerPreviewModal();
  434. } else {
  435. modal.classList.add('hidden');
  436. }
  437. });
  438. // Close modal when clicking close button
  439. closeBtn.addEventListener('click', () => {
  440. setModalVisibility(false, true);
  441. });
  442. // Close modal when clicking outside
  443. modal.addEventListener('click', (e) => {
  444. if (e.target === modal) {
  445. setModalVisibility(false, true);
  446. }
  447. });
  448. // Close modal with Escape key
  449. document.addEventListener('keydown', (e) => {
  450. if (e.key === 'Escape' && !modal.classList.contains('hidden')) {
  451. setModalVisibility(false, true);
  452. }
  453. });
  454. // Setup modal control buttons
  455. setupModalControls();
  456. }
  457. // Handle pause/resume toggle
  458. async function togglePauseResume() {
  459. const pauseButton = document.getElementById('modal-pause-button');
  460. if (!pauseButton) return;
  461. try {
  462. const pauseIcon = pauseButton.querySelector('.material-icons');
  463. const isCurrentlyPaused = pauseIcon.textContent === 'play_arrow';
  464. const endpoint = isCurrentlyPaused ? '/resume_execution' : '/pause_execution';
  465. const response = await fetch(endpoint, { method: 'POST' });
  466. if (!response.ok) throw new Error(`Failed to ${isCurrentlyPaused ? 'resume' : 'pause'}`);
  467. } catch (error) {
  468. console.error('Error toggling pause:', error);
  469. showStatusMessage('Failed to pause/resume pattern', 'error');
  470. }
  471. }
  472. // Setup modal controls
  473. function setupModalControls() {
  474. const pauseButton = document.getElementById('modal-pause-button');
  475. const skipButton = document.getElementById('modal-skip-button');
  476. const stopButton = document.getElementById('modal-stop-button');
  477. const speedDisplay = document.getElementById('modal-speed-display');
  478. const speedInput = document.getElementById('modal-speed-input');
  479. if (!pauseButton || !skipButton || !stopButton || !speedDisplay || !speedInput) return;
  480. // Pause button click handler
  481. pauseButton.addEventListener('click', togglePauseResume);
  482. // Skip button click handler
  483. skipButton.addEventListener('click', async () => {
  484. try {
  485. const response = await fetch('/skip_pattern', { method: 'POST' });
  486. if (!response.ok) throw new Error('Failed to skip pattern');
  487. } catch (error) {
  488. console.error('Error skipping pattern:', error);
  489. showStatusMessage('Failed to skip pattern', 'error');
  490. }
  491. });
  492. // Stop button click handler
  493. stopButton.addEventListener('click', async () => {
  494. try {
  495. const response = await fetch('/stop_execution', { method: 'POST' });
  496. if (!response.ok) throw new Error('Failed to stop pattern');
  497. else {
  498. // Hide modal when stopping
  499. const modal = document.getElementById('playerPreviewModal');
  500. if (modal) modal.classList.add('hidden');
  501. }
  502. } catch (error) {
  503. console.error('Error stopping pattern:', error);
  504. showStatusMessage('Failed to stop pattern', 'error');
  505. }
  506. });
  507. // Speed display click to edit
  508. speedDisplay.addEventListener('click', () => {
  509. isEditingSpeed = true;
  510. speedDisplay.classList.add('hidden');
  511. speedInput.value = speedDisplay.textContent;
  512. speedInput.classList.remove('hidden');
  513. speedInput.focus();
  514. speedInput.select();
  515. });
  516. // Speed input handlers
  517. speedInput.addEventListener('keydown', (e) => {
  518. if (e.key === 'Enter') {
  519. e.preventDefault();
  520. exitModalSpeedEditMode(true);
  521. } else if (e.key === 'Escape') {
  522. e.preventDefault();
  523. exitModalSpeedEditMode(false);
  524. }
  525. });
  526. speedInput.addEventListener('blur', () => {
  527. exitModalSpeedEditMode(true);
  528. });
  529. }
  530. // Exit modal speed edit mode
  531. async function exitModalSpeedEditMode(save = false) {
  532. const speedDisplay = document.getElementById('modal-speed-display');
  533. const speedInput = document.getElementById('modal-speed-input');
  534. if (!speedDisplay || !speedInput) return;
  535. isEditingSpeed = false;
  536. speedInput.classList.add('hidden');
  537. speedDisplay.classList.remove('hidden');
  538. if (save) {
  539. const speed = parseInt(speedInput.value);
  540. if (!isNaN(speed) && speed >= 1 && speed <= 5000) {
  541. try {
  542. const response = await fetch('/set_speed', {
  543. method: 'POST',
  544. headers: { 'Content-Type': 'application/json' },
  545. body: JSON.stringify({ speed: speed })
  546. });
  547. const data = await response.json();
  548. if (data.success) {
  549. speedDisplay.textContent = speed;
  550. showStatusMessage(`Speed set to ${speed} mm/s`, 'success');
  551. } else {
  552. throw new Error(data.detail || 'Failed to set speed');
  553. }
  554. } catch (error) {
  555. console.error('Error setting speed:', error);
  556. showStatusMessage('Failed to set speed', 'error');
  557. }
  558. } else {
  559. showStatusMessage('Please enter a valid speed (1-5000)', 'error');
  560. }
  561. }
  562. }
  563. // Helper function to clean up pattern names
  564. function getCleanPatternName(filePath) {
  565. if (!filePath) return '';
  566. const fileName = normalizeFilePath(filePath);
  567. return fileName.split('/').pop().replace('.thr', '');
  568. }
  569. // Sync modal controls with player status
  570. function syncModalControls(status) {
  571. // Pattern name - clean up to show only filename
  572. const modalPatternName = document.getElementById('modal-pattern-name');
  573. if (modalPatternName && status.current_file) {
  574. modalPatternName.textContent = getCleanPatternName(status.current_file);
  575. }
  576. // Pattern preview image
  577. const modalPatternPreviewImg = document.getElementById('modal-pattern-preview-img');
  578. if (modalPatternPreviewImg && status.current_file) {
  579. const encodedFilename = normalizeFilePath(status.current_file).replace(/[\\/]/g, '--');
  580. const previewUrl = `/preview/${encodedFilename}`;
  581. modalPatternPreviewImg.src = previewUrl;
  582. }
  583. // ETA
  584. const modalEta = document.getElementById('modal-eta');
  585. if (modalEta) {
  586. if (status.progress && status.progress.remaining_time !== null) {
  587. const minutes = Math.floor(status.progress.remaining_time / 60);
  588. const seconds = Math.floor(status.progress.remaining_time % 60);
  589. modalEta.textContent = `ETA: ${minutes}:${seconds.toString().padStart(2, '0')}`;
  590. } else {
  591. modalEta.textContent = 'ETA: calculating...';
  592. }
  593. }
  594. // Progress bar
  595. const modalProgressBar = document.getElementById('modal-progress-bar');
  596. if (modalProgressBar) {
  597. if (status.progress && status.progress.percentage !== null) {
  598. modalProgressBar.style.width = `${status.progress.percentage}%`;
  599. } else {
  600. modalProgressBar.style.width = '0%';
  601. }
  602. }
  603. // Next pattern - clean up to show only filename
  604. const modalNextPattern = document.getElementById('modal-next-pattern');
  605. if (modalNextPattern) {
  606. if (status.playlist && status.playlist.next_file) {
  607. modalNextPattern.textContent = getCleanPatternName(status.playlist.next_file);
  608. } else {
  609. modalNextPattern.textContent = 'None';
  610. }
  611. }
  612. // Pause button
  613. const modalPauseBtn = document.getElementById('modal-pause-button');
  614. if (modalPauseBtn) {
  615. const pauseIcon = modalPauseBtn.querySelector('.material-icons');
  616. if (status.is_paused) {
  617. pauseIcon.textContent = 'play_arrow';
  618. } else {
  619. pauseIcon.textContent = 'pause';
  620. }
  621. }
  622. // Skip button visibility
  623. const modalSkipBtn = document.getElementById('modal-skip-button');
  624. if (modalSkipBtn) {
  625. if (status.playlist && status.playlist.next_file) {
  626. modalSkipBtn.classList.remove('invisible');
  627. } else {
  628. modalSkipBtn.classList.add('invisible');
  629. }
  630. }
  631. // Speed display
  632. const modalSpeedDisplay = document.getElementById('modal-speed-display');
  633. if (modalSpeedDisplay && status.speed && !isEditingSpeed) {
  634. modalSpeedDisplay.textContent = status.speed;
  635. }
  636. }
  637. // Toggle modal visibility
  638. function togglePreviewModal() {
  639. const modal = document.getElementById('playerPreviewModal');
  640. const toggleBtn = document.getElementById('toggle-preview-modal-btn');
  641. if (!modal || !toggleBtn) return;
  642. const isHidden = modal.classList.contains('hidden');
  643. if (isHidden) {
  644. openPlayerPreviewModal();
  645. } else {
  646. setModalVisibility(false, true);
  647. toggleBtn.classList.remove('active-tab');
  648. toggleBtn.classList.add('inactive-tab');
  649. }
  650. }
  651. // Button event handlers
  652. document.addEventListener('DOMContentLoaded', async () => {
  653. try {
  654. // Initialize WebSocket connection
  655. initializeWebSocket();
  656. // Setup player preview modal events
  657. setupPlayerPreviewModalEvents();
  658. console.log('Player initialized successfully');
  659. } catch (error) {
  660. console.error(`Error during initialization: ${error.message}`);
  661. }
  662. });
  663. // Initialize WebSocket connection
  664. function initializeWebSocket() {
  665. connectWebSocket();
  666. }
  667. // Add resize handler for responsive canvas with debouncing
  668. let resizeTimeout;
  669. window.addEventListener('resize', () => {
  670. const canvas = document.getElementById('playerPreviewCanvas');
  671. const modal = document.getElementById('playerPreviewModal');
  672. if (canvas && modal && !modal.classList.contains('hidden')) {
  673. // Clear previous timeout
  674. clearTimeout(resizeTimeout);
  675. // Debounce resize calls to avoid excessive updates
  676. resizeTimeout = setTimeout(() => {
  677. const ctx = canvas.getContext('2d');
  678. setupPlayerPreviewCanvas(ctx);
  679. drawPlayerPreview(ctx, targetProgress);
  680. }, 16); // ~60fps update rate
  681. }
  682. });
  683. // Handle file changes and reload preview data
  684. function handleFileChange(newFile) {
  685. if (newFile !== currentPreviewFile) {
  686. currentPreviewFile = newFile;
  687. if (newFile) {
  688. loadPlayerPreviewData(`./patterns/${newFile}`);
  689. } else {
  690. playerPreviewData = null;
  691. }
  692. }
  693. }
  694. // Cache All Previews Prompt functionality
  695. let cacheAllInProgress = false;
  696. function shouldShowCacheAllPrompt() {
  697. // Check if we've already shown the prompt
  698. const promptShown = localStorage.getItem('cacheAllPromptShown');
  699. console.log('shouldShowCacheAllPrompt - promptShown:', promptShown);
  700. return !promptShown;
  701. }
  702. function showCacheAllPrompt(forceShow = false) {
  703. console.log('showCacheAllPrompt called, forceShow:', forceShow);
  704. if (!forceShow && !shouldShowCacheAllPrompt()) {
  705. console.log('Cache all prompt already shown, skipping');
  706. return;
  707. }
  708. const modal = document.getElementById('cacheAllPromptModal');
  709. if (modal) {
  710. console.log('Showing cache all prompt modal');
  711. modal.classList.remove('hidden');
  712. // Store whether this was forced (manually triggered)
  713. modal.dataset.manuallyTriggered = forceShow.toString();
  714. } else {
  715. console.log('Cache all prompt modal not found');
  716. }
  717. }
  718. function hideCacheAllPrompt() {
  719. const modal = document.getElementById('cacheAllPromptModal');
  720. if (modal) {
  721. modal.classList.add('hidden');
  722. }
  723. }
  724. function markCacheAllPromptAsShown() {
  725. localStorage.setItem('cacheAllPromptShown', 'true');
  726. }
  727. function initializeCacheAllPrompt() {
  728. const modal = document.getElementById('cacheAllPromptModal');
  729. const skipBtn = document.getElementById('skipCacheAllBtn');
  730. const startBtn = document.getElementById('startCacheAllBtn');
  731. const closeBtn = document.getElementById('closeCacheAllBtn');
  732. if (!modal || !skipBtn || !startBtn || !closeBtn) {
  733. return;
  734. }
  735. // Skip button handler
  736. skipBtn.addEventListener('click', () => {
  737. const wasManuallyTriggered = modal.dataset.manuallyTriggered === 'true';
  738. hideCacheAllPrompt();
  739. // Only mark as shown if it was automatically shown (not manually triggered)
  740. if (!wasManuallyTriggered) {
  741. markCacheAllPromptAsShown();
  742. }
  743. });
  744. // Close button handler (after completion)
  745. closeBtn.addEventListener('click', () => {
  746. const wasManuallyTriggered = modal.dataset.manuallyTriggered === 'true';
  747. hideCacheAllPrompt();
  748. // Always mark as shown after successful completion
  749. if (!wasManuallyTriggered) {
  750. markCacheAllPromptAsShown();
  751. }
  752. });
  753. // Start caching button handler
  754. startBtn.addEventListener('click', async () => {
  755. if (cacheAllInProgress) {
  756. return;
  757. }
  758. cacheAllInProgress = true;
  759. // Hide buttons and show progress
  760. document.getElementById('cacheAllButtons').classList.add('hidden');
  761. document.getElementById('cacheAllProgress').classList.remove('hidden');
  762. try {
  763. await startCacheAllProcess();
  764. // Show completion message
  765. document.getElementById('cacheAllProgress').classList.add('hidden');
  766. document.getElementById('cacheAllComplete').classList.remove('hidden');
  767. } catch (error) {
  768. console.error('Error caching all previews:', error);
  769. // Show error and reset
  770. document.getElementById('cacheAllProgressText').textContent = 'Error occurred during caching';
  771. setTimeout(() => {
  772. hideCacheAllPrompt();
  773. markCacheAllPromptAsShown();
  774. }, 3000);
  775. } finally {
  776. cacheAllInProgress = false;
  777. }
  778. });
  779. }
  780. async function startCacheAllProcess() {
  781. try {
  782. // Get list of patterns using cached function
  783. const patterns = await getCachedPatternFiles();
  784. if (!patterns || patterns.length === 0) {
  785. throw new Error('No patterns found');
  786. }
  787. const progressBar = document.getElementById('cacheAllProgressBar');
  788. const progressText = document.getElementById('cacheAllProgressText');
  789. const progressPercentage = document.getElementById('cacheAllProgressPercentage');
  790. let completed = 0;
  791. const batchSize = 5; // Process in small batches to avoid overwhelming the server
  792. for (let i = 0; i < patterns.length; i += batchSize) {
  793. const batch = patterns.slice(i, i + batchSize);
  794. // Update progress text
  795. progressText.textContent = `Caching previews... (${Math.min(i + batchSize, patterns.length)}/${patterns.length})`;
  796. // Process batch
  797. const batchPromises = batch.map(async (pattern) => {
  798. try {
  799. const previewResponse = await fetch('/preview_thr', {
  800. method: 'POST',
  801. headers: {
  802. 'Content-Type': 'application/json',
  803. },
  804. body: JSON.stringify({ file_name: pattern })
  805. });
  806. if (previewResponse.ok) {
  807. const data = await previewResponse.json();
  808. if (data.preview_url) {
  809. // Pre-load the image to cache it
  810. return new Promise((resolve) => {
  811. const img = new Image();
  812. img.onload = () => resolve();
  813. img.onerror = () => resolve(); // Continue even if image fails
  814. img.src = data.preview_url;
  815. });
  816. }
  817. }
  818. return Promise.resolve();
  819. } catch (error) {
  820. console.warn(`Failed to cache preview for ${pattern}:`, error);
  821. return Promise.resolve(); // Continue with other patterns
  822. }
  823. });
  824. await Promise.all(batchPromises);
  825. completed += batch.length;
  826. // Update progress bar
  827. const progress = Math.round((completed / patterns.length) * 100);
  828. progressBar.style.width = `${progress}%`;
  829. progressPercentage.textContent = `${progress}%`;
  830. // Small delay between batches to prevent overwhelming the server
  831. if (i + batchSize < patterns.length) {
  832. await new Promise(resolve => setTimeout(resolve, 100));
  833. }
  834. }
  835. progressText.textContent = `Completed! Cached ${patterns.length} previews.`;
  836. } catch (error) {
  837. throw error;
  838. }
  839. }
  840. // Function to be called after initial cache generation completes
  841. function onInitialCacheComplete() {
  842. console.log('onInitialCacheComplete called');
  843. // Show the cache all prompt after a short delay
  844. setTimeout(() => {
  845. console.log('Triggering cache all prompt after delay');
  846. showCacheAllPrompt();
  847. }, 1000);
  848. }
  849. // Initialize on DOM load
  850. document.addEventListener('DOMContentLoaded', () => {
  851. initializeCacheAllPrompt();
  852. });
  853. // Make functions available globally for debugging
  854. window.onInitialCacheComplete = onInitialCacheComplete;
  855. window.showCacheAllPrompt = showCacheAllPrompt;
  856. window.testCacheAllPrompt = function() {
  857. console.log('Manual test trigger');
  858. // Clear localStorage for testing
  859. localStorage.removeItem('cacheAllPromptShown');
  860. showCacheAllPrompt();
  861. };