1
0
Эх сурвалжийг харах

Improve preview (#57)

* fix various arm length for dwp

* reduce default speed

* improve preview

* fix preview text

* fix preview canvas while playing

* cache preview

* add server cache for svg

* remove safe guard that would cost long running pattern to crash DW
Tuan Nguyen 9 сар өмнө
parent
commit
969aea88f5

+ 1 - 0
.gitignore

@@ -6,3 +6,4 @@ __pycache__/
 .idea
 *.json
 .venv/
+patterns/cached_svg/

+ 71 - 16
app.py

@@ -1,5 +1,5 @@
 from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks, WebSocket, WebSocketDisconnect
-from fastapi.responses import JSONResponse, FileResponse
+from fastapi.responses import JSONResponse, FileResponse, Response
 from fastapi.staticfiles import StaticFiles
 from fastapi.templating import Jinja2Templates
 from pydantic import BaseModel
@@ -19,6 +19,9 @@ import sys
 import asyncio
 from contextlib import asynccontextmanager
 from modules.led.led_controller import LEDController, effect_idle
+import math
+from modules.core.svg_cache_manager import generate_all_svg_previews, get_cache_path, generate_svg_preview
+
 # Configure logging
 logging.basicConfig(
     level=logging.INFO,
@@ -47,6 +50,13 @@ async def lifespan(app: FastAPI):
         mqtt_handler = mqtt.init_mqtt()
     except Exception as e:
         logger.warning(f"Failed to initialize MQTT: {str(e)}")
+    
+    # Generate SVG previews for all patterns
+    try:
+        logger.info("Starting SVG cache generation...")
+        await generate_all_svg_previews()
+    except Exception as e:
+        logger.warning(f"Failed to generate SVG cache: {str(e)}")
 
     yield  # This separates startup from shutdown code
 
@@ -193,20 +203,20 @@ async def list_theta_rho_files():
 
 @app.post("/upload_theta_rho")
 async def upload_theta_rho(file: UploadFile = File(...)):
-    custom_patterns_dir = os.path.join(pattern_manager.THETA_RHO_DIR, 'custom_patterns')
-    os.makedirs(custom_patterns_dir, exist_ok=True)
-    logger.debug(f'Ensuring custom patterns directory exists: {custom_patterns_dir}')
-
-    if file:
-        file_path = os.path.join(custom_patterns_dir, file.filename)
-        contents = await file.read()
+    """Upload a theta-rho file."""
+    try:
+        # Save the file
+        file_path = os.path.join(pattern_manager.THETA_RHO_DIR, "custom_patterns", file.filename)
         with open(file_path, "wb") as f:
-            f.write(contents)
-        logger.info(f'Successfully uploaded theta-rho file: {file.filename}')
-        return {"success": True}
-    
-    logger.warning('Upload theta-rho request received without file')
-    return {"success": False}
+            f.write(await file.read())
+        
+        # Generate SVG preview for the new file
+        await generate_svg_preview(os.path.join("custom_patterns", file.filename))
+        
+        return {"success": True, "message": f"File {file.filename} uploaded successfully"}
+    except Exception as e:
+        logger.error(f"Error uploading file: {str(e)}")
+        raise HTTPException(status_code=500, detail=str(e))
 
 class ThetaRhoRequest(BaseModel):
     file_name: str
@@ -352,8 +362,54 @@ async def preview_thr(request: DeleteFileRequest):
         raise HTTPException(status_code=404, detail="File not found")
 
     try:
+        # Check if cached SVG exists
+        cache_path = get_cache_path(request.file_name)
+        if os.path.exists(cache_path):
+            with open(cache_path, 'r', encoding='utf-8') as f:
+                svg_content = f.read()
+                
+            # Parse coordinates for first and last points
+            coordinates = pattern_manager.parse_theta_rho_file(file_path)
+            first_coord = coordinates[0] if coordinates else None
+            last_coord = coordinates[-1] if coordinates else None
+            
+            return {
+                "svg": svg_content,
+                "first_coordinate": first_coord,
+                "last_coordinate": last_coord
+            }
+        
+        # If not cached, generate SVG as before
         coordinates = pattern_manager.parse_theta_rho_file(file_path)
-        return {"success": True, "coordinates": coordinates}
+        
+        # Convert polar coordinates to SVG path
+        svg_path = []
+        for i, (theta, rho) in enumerate(coordinates):
+            x = 100 - rho * 90 * math.cos(theta)
+            y = 100 - rho * 90 * math.sin(theta)
+            
+            if i == 0:
+                svg_path.append(f"M {x:.2f} {y:.2f}")
+            else:
+                svg_path.append(f"L {x:.2f} {y:.2f}")
+        
+        svg = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="100%" height="100%" viewBox="0 0 200 200" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
+    <path d="{' '.join(svg_path)}" 
+          fill="none" 
+          stroke="currentColor" 
+          stroke-width="0.5"/>
+</svg>'''
+        
+        # Cache the SVG for future use
+        with open(cache_path, 'w', encoding='utf-8') as f:
+            f.write(svg)
+        
+        return {
+            "svg": svg,
+            "first_coordinate": coordinates[0] if coordinates else None,
+            "last_coordinate": coordinates[-1] if coordinates else None
+        }
     except Exception as e:
         logger.error(f"Failed to generate preview for {request.file_name}: {str(e)}")
         raise HTTPException(status_code=500, detail=str(e))
@@ -564,6 +620,5 @@ def entrypoint():
     logger.info("Starting FastAPI server on port 8080...")
     uvicorn.run(app, host="0.0.0.0", port=8080, workers=1)  # Set workers to 1 to avoid multiple signal handlers
 
-
 if __name__ == "__main__":
     entrypoint()

+ 4 - 8
modules/connection/connection_manager.py

@@ -224,15 +224,14 @@ def send_grbl_coordinates(x, y, speed=600, timeout=2, home=False):
     
     # Track overall attempt time
     overall_start_time = time.time()
-    max_total_attempt_time = 120 # Maximum total seconds to try
     
-    while time.time() - overall_start_time < max_total_attempt_time:
+    while True:
         try:
             gcode = f"$J=G91 G21 Y{y} F{speed}" if home else f"G1 X{x} Y{y} F{speed}"
             state.conn.send(gcode + "\n")
             logger.debug(f"Sent command: {gcode}")
             start_time = time.time()
-            while time.time() - start_time < timeout and time.time() - overall_start_time < max_total_attempt_time:
+            while True:
                 response = state.conn.readline()
                 logger.debug(f"Response: {response}")
                 if response.lower() == "ok":
@@ -251,10 +250,7 @@ def send_grbl_coordinates(x, y, speed=600, timeout=2, home=False):
                 state.is_connected = False
                 logger.info("Connection marked as disconnected due to device error")
                 return False
-            
-        # Check if we've exceeded our overall timeout
-        if time.time() - overall_start_time >= max_total_attempt_time:
-            break
+
             
         logger.warning(f"No 'ok' received for X{x} Y{y}, speed {speed}. Retrying...")
         time.sleep(0.1)
@@ -362,7 +358,7 @@ def get_machine_steps(timeout=10):
     if settings_complete:
         if y_steps_per_mm == 180:
             state.table_type = 'dune_weaver_mini'
-        elif y_steps_per_mm == 320:
+        elif y_steps_per_mm >= 320:
             state.table_type = 'dune_weaver_pro'
         elif y_steps_per_mm == 287:
             state.table_type = 'dune_weaver'

+ 30 - 0
modules/core/preview.py

@@ -0,0 +1,30 @@
+"""Preview module for generating SVG previews of patterns."""
+import os
+import math
+from modules.core.pattern_manager import parse_theta_rho_file, THETA_RHO_DIR
+
+async def generate_preview_svg(pattern_file):
+    """Generate an SVG preview for a pattern file."""
+    file_path = os.path.join(THETA_RHO_DIR, pattern_file)
+    coordinates = parse_theta_rho_file(file_path)
+    
+    # Convert polar coordinates to SVG path
+    svg_path = []
+    for i, (theta, rho) in enumerate(coordinates):
+        x = 100 - rho * 90 * math.cos(theta)
+        y = 100 - rho * 90 * math.sin(theta)
+        
+        if i == 0:
+            svg_path.append(f"M {x:.2f} {y:.2f}")
+        else:
+            svg_path.append(f"L {x:.2f} {y:.2f}")
+    
+    svg = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="100%" height="100%" viewBox="0 0 200 200" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
+    <path d="{' '.join(svg_path)}" 
+          fill="none" 
+          stroke="currentColor" 
+          stroke-width="0.5"/>
+</svg>'''
+    
+    return svg 

+ 1 - 1
modules/core/state.py

@@ -11,7 +11,7 @@ class AppState:
         # Private variables for properties
         self._current_playing_file = None
         self._pause_requested = False
-        self._speed = 150
+        self._speed = 130
         self._current_playlist = None
         self._current_playlist_name = None  # New variable for playlist name
         

+ 73 - 0
modules/core/svg_cache_manager.py

@@ -0,0 +1,73 @@
+"""SVG Cache Manager for pre-generating and managing SVG previews."""
+import os
+import json
+import asyncio
+import logging
+from pathlib import Path
+from modules.core.pattern_manager import list_theta_rho_files, THETA_RHO_DIR
+
+logger = logging.getLogger(__name__)
+
+# Constants
+CACHE_DIR = os.path.join(THETA_RHO_DIR, "cached_svg")
+
+def ensure_cache_dir():
+    """Ensure the cache directory exists."""
+    Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
+
+def get_cache_path(pattern_file):
+    """Get the cache path for a pattern file."""
+    # Convert the pattern file path to a safe filename
+    safe_name = pattern_file.replace('/', '_').replace('\\', '_')
+    return os.path.join(CACHE_DIR, f"{safe_name}.svg")
+
+def needs_cache(pattern_file):
+    """Check if a pattern file needs its cache generated."""
+    cache_path = get_cache_path(pattern_file)
+    return not os.path.exists(cache_path)
+
+async def generate_svg_preview(pattern_file):
+    """Generate SVG preview for a single pattern file."""
+    from modules.core.preview import generate_preview_svg
+    try:
+        # Generate the SVG
+        svg_content = await generate_preview_svg(pattern_file)
+        
+        # Save to cache
+        cache_path = get_cache_path(pattern_file)
+        with open(cache_path, 'w', encoding='utf-8') as f:
+            f.write(svg_content)
+        
+        return True
+    except Exception as e:
+        # Only log the error message, not the full SVG content
+        logger.error(f"Failed to generate SVG for {pattern_file}")
+        return False
+
+async def generate_all_svg_previews():
+    """Generate SVG previews for all pattern files."""
+    ensure_cache_dir()
+    
+    # Get all pattern files and filter for .thr files only
+    pattern_files = [f for f in list_theta_rho_files() if f.endswith('.thr')]
+    
+    # Filter out patterns that already have cache
+    patterns_to_cache = [f for f in pattern_files if needs_cache(f)]
+    total_files = len(patterns_to_cache)
+    
+    if total_files == 0:
+        logger.info("All patterns are already cached")
+        return
+        
+    logger.info(f"Generating SVG cache for {total_files} uncached .thr patterns...")
+    
+    # Process files concurrently in batches to avoid overwhelming the system
+    batch_size = 5
+    successful = 0
+    for i in range(0, total_files, batch_size):
+        batch = patterns_to_cache[i:i + batch_size]
+        tasks = [generate_svg_preview(file) for file in batch]
+        results = await asyncio.gather(*tasks)
+        successful += sum(1 for r in results if r)
+    
+    logger.info(f"SVG cache generation completed: {successful}/{total_files} patterns cached") 

+ 57 - 6
static/css/style.css

@@ -866,13 +866,13 @@ button#debug_button.active {
 
 /* Preview Canvas */
 #patternPreviewCanvas {
-    width: 100%;
-    max-width: 300px;
-    aspect-ratio: 1/1;
+    width: 100px;
+    height: 100px;
     border: 1px solid var(--border-primary);
     background: var(--theme-secondary);
     border-radius: 100%;
     padding: 15px;
+    box-sizing: border-box;
 }
 
 #pattern-preview {
@@ -986,12 +986,13 @@ body.playing #currently-playing-container:not(.open) #progress-container {
 
 
 #currentlyPlayingCanvas {
-    width: 100px;
-    aspect-ratio: 1/1;
+    width: 50px;
+    height: 50px;
     border: 1px solid var(--border-primary);
     background: var(--theme-secondary);
     border-radius: 100%;
-    padding: 10px;
+    padding: 5px;
+    box-sizing: border-box;
 }
 
 #currently-playing-details {
@@ -1550,3 +1551,53 @@ input[type="number"]:focus {
         display: flex;
     }
 }
+
+#pattern-preview-container .svg-container {
+    width: 300px;
+    height: 300px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+#pattern-preview-container .svg-container svg {
+    width: 100%;
+    height: 100%;
+    max-width: 100%;
+    max-height: 100%;
+}
+
+#pattern-preview-container .coordinate-display {
+    margin-top: 10px;
+    text-align: center;
+}
+
+#pattern-preview-container.fullscreen .svg-container {
+    width: initial;
+    max-width: calc(100vw - 30px);
+}
+
+#currently-playing-preview {
+    width: 100px;
+    height: 100px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin: 0 auto;
+    box-sizing: border-box;
+}
+
+#currently-playing-preview .svg-container {
+    width: 100px;
+    height: 100px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+#currently-playing-preview .svg-container svg {
+    width: 100%;
+    height: 100%;
+    max-width: 100%;
+    max-height: 100%;
+}

+ 119 - 55
static/js/main.js

@@ -145,6 +145,15 @@ async function selectFile(file, listItem) {
     }
 
     logMessage(`Selected file: ${file}`);
+    
+    // Show the preview container
+    const previewContainer = document.getElementById('pattern-preview-container');
+    if (previewContainer) {
+        previewContainer.classList.remove('hidden');
+        previewContainer.classList.add('visible');
+    }
+    
+    // Update the preview
     await previewPattern(file);
 
     // Populate the playlist dropdown after selecting a pattern
@@ -270,7 +279,7 @@ async function runThetaRho() {
                 currentlyPlayingFile.textContent = selectedFile.replace('./patterns/', '');
             }
             // Show initial preview
-            previewPattern(selectedFile.replace('./patterns/', ''), 'currently-playing-container');
+            updateCurrentlyPlayingPattern(selectedFile.replace('./patterns/', ''));
             logMessage(`Pattern running: ${selectedFile}`, LOG_TYPE.SUCCESS);
         } else {
             if (response.status === 409) {
@@ -374,66 +383,130 @@ async function removeCustomPattern(fileName) {
     }
 }
 
-// Preview a Theta-Rho file
-async function previewPattern(fileName, containerId = 'pattern-preview-container') {
+// SVG Cache
+const svgCache = new Map();
+
+// Function to get SVG from cache or fetch it
+async function getSVG(fileName) {
+    // Check if SVG is in cache
+    if (svgCache.has(fileName)) {
+        return svgCache.get(fileName);
+    }
+
+    // If not in cache, fetch it
     try {
-        logMessage(`Fetching data to preview file: ${fileName}...`);
         const response = await fetch('/preview_thr', {
             method: 'POST',
             headers: { 'Content-Type': 'application/json' },
             body: JSON.stringify({ file_name: fileName })
         });
 
-        const result = await response.json();
-        if (result.success) {
-            // Mirror the theta values in the coordinates
-            const coordinates = result.coordinates.map(coord => [
-                (coord[0] < Math.PI) ? 
-                    Math.PI - coord[0] : // For first half
-                    3 * Math.PI - coord[0], // For second half
-                coord[1]
-            ]);
-
-            // Render the pattern in the specified container
-            const canvasId = containerId === 'currently-playing-container'
-                ? 'currentlyPlayingCanvas'
-                : 'patternPreviewCanvas';
-            renderPattern(coordinates, canvasId);
-
-            // Update coordinate display
-            const firstCoordElement = document.getElementById('first_coordinate');
-            const lastCoordElement = document.getElementById('last_coordinate');
-
-            if (firstCoordElement) {
-                const firstCoord = coordinates[0];
-                firstCoordElement.textContent = `First Coordinate: θ=${firstCoord[0].toFixed(2)}, ρ=${firstCoord[1].toFixed(2)}`;
-            } else {
-                logMessage('First coordinate element not found.', LOG_TYPE.WARNING);
-            }
+        if (!response.ok) {
+            throw new Error(`HTTP error! status: ${response.status}`);
+        }
 
-            if (lastCoordElement) {
-                const lastCoord = coordinates[coordinates.length - 1];
-                lastCoordElement.textContent = `Last Coordinate: θ=${lastCoord[0].toFixed(2)}, ρ=${lastCoord[1].toFixed(2)}`;
-            } else {
-                logMessage('Last coordinate element not found.', LOG_TYPE.WARNING);
-            }
+        const data = await response.json();
+        
+        // Store in cache
+        svgCache.set(fileName, data);
+        return data;
+    } catch (error) {
+        logMessage(`Error fetching SVG: ${error.message}`, LOG_TYPE.ERROR);
+        throw error;
+    }
+}
 
-            // Show the preview container
-            const previewContainer = document.getElementById(containerId);
-            if (previewContainer) {
-                previewContainer.classList.remove('hidden');
-                previewContainer.classList.add('visible');
-            } else {
-                logMessage(`Preview container not found: ${containerId}`, LOG_TYPE.ERROR);
-            }
+// Function to clear SVG cache
+function clearSVGCache() {
+    svgCache.clear();
+}
+
+// Update previewPattern function to use cache
+async function previewPattern(fileName, containerId = 'pattern-preview-container') {
+    try {
+        logMessage(`Fetching data to preview file: ${fileName}...`);
+        const data = await getSVG(fileName);
+        
+        // Get the preview container
+        const previewContainer = document.getElementById(containerId);
+        if (!previewContainer) {
+            logMessage(`Preview container not found: ${containerId}`, LOG_TYPE.ERROR);
+            return;
+        }
+
+        // Get the pattern preview section
+        const patternPreview = previewContainer.querySelector('#pattern-preview');
+        if (!patternPreview) {
+            logMessage('Pattern preview section not found', LOG_TYPE.ERROR);
+            return;
+        }
+
+        // Clear existing content
+        patternPreview.innerHTML = '';
+
+        // Create SVG container
+        const svgContainer = document.createElement('div');
+        svgContainer.className = 'svg-container';
+        svgContainer.innerHTML = data.svg;
+
+        // Create coordinate display
+        const coordDisplay = document.createElement('div');
+        coordDisplay.className = 'coordinate-display';
+
+        // Update coordinate text
+        if (data.first_coordinate && data.last_coordinate) {
+            coordDisplay.innerHTML = `
+                <div>First Coordinate: θ=${data.first_coordinate[0].toFixed(2)}, ρ=${data.first_coordinate[1].toFixed(2)}</div>
+                <div>Last Coordinate: θ=${data.last_coordinate[0].toFixed(2)}, ρ=${data.last_coordinate[1].toFixed(2)}</div>
+            `;
         } else {
-            logMessage(`Failed to fetch preview for file: ${fileName}`, LOG_TYPE.WARNING);
+            coordDisplay.innerHTML = 'No coordinates available';
         }
+
+        // Add elements to preview
+        patternPreview.appendChild(svgContainer);
+        patternPreview.appendChild(coordDisplay);
+
     } catch (error) {
         logMessage(`Error previewing pattern: ${error.message}`, LOG_TYPE.ERROR);
     }
 }
 
+// Update updateCurrentlyPlayingPattern function to use cache
+async function updateCurrentlyPlayingPattern(fileName) {
+    try {
+        const data = await getSVG(fileName);
+        
+        // Get the currently playing container
+        const container = document.getElementById('currently-playing-container');
+        if (!container) {
+            logMessage('Currently playing container not found', LOG_TYPE.ERROR);
+            return;
+        }
+
+        // Get the currently playing preview div
+        const previewDiv = container.querySelector('#currently-playing-preview');
+        if (!previewDiv) {
+            logMessage('Currently playing preview div not found', LOG_TYPE.ERROR);
+            return;
+        }
+
+        // Clear existing content
+        previewDiv.innerHTML = '';
+
+        // Create SVG container
+        const svgContainer = document.createElement('div');
+        svgContainer.className = 'svg-container';
+        svgContainer.innerHTML = data.svg;
+
+        // Add SVG to the preview div
+        previewDiv.appendChild(svgContainer);
+
+    } catch (error) {
+        logMessage(`Error updating currently playing pattern: ${error.message}`, LOG_TYPE.ERROR);
+    }
+}
+
 // Render the pattern on a canvas
 function renderPattern(coordinates, canvasId) {
     const canvas = document.getElementById(canvasId);
@@ -1950,15 +2023,6 @@ function updateCurrentlyPlayingUI(status) {
     if (status.current_file && status.is_running) {
         document.body.classList.add('playing');
         container.style.display = 'flex';
-        
-        // Hide the preview container when a pattern is playing
-        const previewContainer = document.getElementById('pattern-preview-container');
-        if (previewContainer) {
-            // Clear any selected file highlights
-            document.querySelectorAll('#theta_rho_files .file-item').forEach(item => {
-                item.classList.remove('selected');
-            });
-        }
     } else {
         document.body.classList.remove('playing');
         container.style.display = 'none';
@@ -1993,7 +2057,7 @@ function updateCurrentlyPlayingUI(status) {
     if (status.current_file && lastPlayedFile !== status.current_file) {
         lastPlayedFile = status.current_file;
         const cleanFileName = status.current_file.replace('./patterns/', '');
-        previewPattern(cleanFileName, 'currently-playing-container');
+        updateCurrentlyPlayingPattern(cleanFileName);
     }
 
     // Update progress information