소스 검색

add custom clear, remove clear sideway from adaptive

tuanchris 5 달 전
부모
커밋
ffcb25b73f
5개의 변경된 파일451개의 추가작업 그리고 3개의 파일을 삭제
  1. 41 0
      main.py
  2. 36 1
      modules/core/pattern_manager.py
  3. 6 0
      modules/core/state.py
  4. 252 2
      static/js/settings.js
  5. 116 0
      templates/settings.html

+ 41 - 0
main.py

@@ -784,6 +784,47 @@ async def skip_pattern():
     state.skip_requested = True
     return {"success": True}
 
+@app.get("/api/custom_clear_patterns")
+async def get_custom_clear_patterns():
+    """Get the currently configured custom clear patterns."""
+    return {
+        "success": True,
+        "custom_clear_from_in": state.custom_clear_from_in,
+        "custom_clear_from_out": state.custom_clear_from_out
+    }
+
+@app.post("/api/custom_clear_patterns")
+async def set_custom_clear_patterns(request: dict):
+    """Set custom clear patterns for clear_from_in and clear_from_out."""
+    try:
+        # Validate that the patterns exist if they're provided
+        if "custom_clear_from_in" in request and request["custom_clear_from_in"]:
+            pattern_path = os.path.join(pattern_manager.THETA_RHO_DIR, request["custom_clear_from_in"])
+            if not os.path.exists(pattern_path):
+                raise HTTPException(status_code=400, detail=f"Pattern file not found: {request['custom_clear_from_in']}")
+            state.custom_clear_from_in = request["custom_clear_from_in"]
+        elif "custom_clear_from_in" in request:
+            state.custom_clear_from_in = None
+            
+        if "custom_clear_from_out" in request and request["custom_clear_from_out"]:
+            pattern_path = os.path.join(pattern_manager.THETA_RHO_DIR, request["custom_clear_from_out"])
+            if not os.path.exists(pattern_path):
+                raise HTTPException(status_code=400, detail=f"Pattern file not found: {request['custom_clear_from_out']}")
+            state.custom_clear_from_out = request["custom_clear_from_out"]
+        elif "custom_clear_from_out" in request:
+            state.custom_clear_from_out = None
+        
+        state.save()
+        logger.info(f"Custom clear patterns updated - in: {state.custom_clear_from_in}, out: {state.custom_clear_from_out}")
+        return {
+            "success": True,
+            "custom_clear_from_in": state.custom_clear_from_in,
+            "custom_clear_from_out": state.custom_clear_from_out
+        }
+    except Exception as e:
+        logger.error(f"Failed to set custom clear patterns: {str(e)}")
+        raise HTTPException(status_code=500, detail=str(e))
+
 @app.post("/preview_thr_batch")
 async def preview_thr_batch(request: dict):
     start = time.time()

+ 36 - 1
modules/core/pattern_manager.py

@@ -156,6 +156,41 @@ def get_clear_pattern_file(clear_pattern_mode, path=None):
     # Get patterns for current table type, fallback to standard patterns if type not found
     table_patterns = clear_patterns.get(state.table_type, clear_patterns['dune_weaver'])
     
+    # Check for custom patterns first
+    if state.custom_clear_from_out and clear_pattern_mode in ['clear_from_out', 'adaptive']:
+        if clear_pattern_mode == 'adaptive':
+            # For adaptive mode, check if we should use custom pattern
+            if path:
+                coordinates = parse_theta_rho_file(path)
+                if coordinates and coordinates[0][1] < 0.5:
+                    # Use custom clear_from_out if set
+                    custom_path = os.path.join('./patterns', state.custom_clear_from_out)
+                    if os.path.exists(custom_path):
+                        logger.debug(f"Using custom clear_from_out: {custom_path}")
+                        return custom_path
+        elif clear_pattern_mode == 'clear_from_out':
+            custom_path = os.path.join('./patterns', state.custom_clear_from_out)
+            if os.path.exists(custom_path):
+                logger.debug(f"Using custom clear_from_out: {custom_path}")
+                return custom_path
+    
+    if state.custom_clear_from_in and clear_pattern_mode in ['clear_from_in', 'adaptive']:
+        if clear_pattern_mode == 'adaptive':
+            # For adaptive mode, check if we should use custom pattern
+            if path:
+                coordinates = parse_theta_rho_file(path)
+                if coordinates and coordinates[0][1] >= 0.5:
+                    # Use custom clear_from_in if set
+                    custom_path = os.path.join('./patterns', state.custom_clear_from_in)
+                    if os.path.exists(custom_path):
+                        logger.debug(f"Using custom clear_from_in: {custom_path}")
+                        return custom_path
+        elif clear_pattern_mode == 'clear_from_in':
+            custom_path = os.path.join('./patterns', state.custom_clear_from_in)
+            if os.path.exists(custom_path):
+                logger.debug(f"Using custom clear_from_in: {custom_path}")
+                return custom_path
+    
     logger.debug(f"Clear pattern mode: {clear_pattern_mode} for table type: {state.table_type}")
     
     if clear_pattern_mode == "random":
@@ -175,7 +210,7 @@ def get_clear_pattern_file(clear_pattern_mode, path=None):
         if first_rho < 0.5:
             return table_patterns['clear_from_out']
         else:
-            return random.choice([table_patterns['clear_from_in'], table_patterns['clear_sideway']])
+            return table_patterns['clear_from_in']
     else:
         if clear_pattern_mode not in table_patterns:
             return False

+ 6 - 0
modules/core/state.py

@@ -46,6 +46,8 @@ class AppState:
         self._playlist_mode = "loop"
         self._pause_time = 0
         self._clear_pattern = "none"
+        self.custom_clear_from_in = None  # Custom clear from center pattern
+        self.custom_clear_from_out = None  # Custom clear from perimeter pattern
         self.load()
 
     @property
@@ -157,6 +159,8 @@ class AppState:
             "playlist_mode": self._playlist_mode,
             "pause_time": self._pause_time,
             "clear_pattern": self._clear_pattern,
+            "custom_clear_from_in": self.custom_clear_from_in,
+            "custom_clear_from_out": self.custom_clear_from_out,
             "port": self.port,
             "wled_ip": self.wled_ip,
         }
@@ -183,6 +187,8 @@ class AppState:
         self._playlist_mode = data.get("playlist_mode", "loop")
         self._pause_time = data.get("pause_time", 0)
         self._clear_pattern = data.get("clear_pattern", "none")
+        self.custom_clear_from_in = data.get("custom_clear_from_in", None)
+        self.custom_clear_from_out = data.get("custom_clear_from_out", None)
         self.port = data.get("port", None)
         self.wled_ip = data.get('wled_ip', None)
 

+ 252 - 2
static/js/settings.js

@@ -168,8 +168,14 @@ document.addEventListener('DOMContentLoaded', async () => {
         fetch('/api/version').then(response => response.json()).catch(() => ({ current: '1.0.0', latest: '1.0.0', update_available: false })),
         
         // Load available serial ports
-        fetch('/list_serial_ports').then(response => response.json()).catch(() => [])
-    ]).then(([statusData, wledData, updateData, ports]) => {
+        fetch('/list_serial_ports').then(response => response.json()).catch(() => []),
+        
+        // Load available pattern files for clear pattern selection
+        fetch('/list_theta_rho_files').then(response => response.json()).catch(() => []),
+        
+        // Load current custom clear patterns
+        fetch('/api/custom_clear_patterns').then(response => response.json()).catch(() => ({ custom_clear_from_in: null, custom_clear_from_out: null }))
+    ]).then(([statusData, wledData, updateData, ports, patterns, clearPatterns]) => {
         // Update connection status
         setCachedConnectionStatus(statusData);
         updateConnectionUI(statusData);
@@ -239,6 +245,29 @@ document.addEventListener('DOMContentLoaded', async () => {
                 portSelect.value = ports[0];
             }
         }
+        
+        // Initialize autocomplete for clear patterns
+        const clearFromInInput = document.getElementById('customClearFromInInput');
+        const clearFromOutInput = document.getElementById('customClearFromOutInput');
+        
+        if (clearFromInInput && clearFromOutInput && patterns && Array.isArray(patterns)) {
+            // Store patterns globally for autocomplete
+            window.availablePatterns = patterns;
+            
+            // Set current values if they exist
+            if (clearPatterns && clearPatterns.custom_clear_from_in) {
+                clearFromInInput.value = clearPatterns.custom_clear_from_in;
+            }
+            if (clearPatterns && clearPatterns.custom_clear_from_out) {
+                clearFromOutInput.value = clearPatterns.custom_clear_from_out;
+            }
+            
+            // Initialize autocomplete for both inputs
+            initializeAutocomplete('customClearFromInInput', 'clearFromInSuggestions', 'clearFromInClear', patterns);
+            initializeAutocomplete('customClearFromOutInput', 'clearFromOutSuggestions', 'clearFromOutClear', patterns);
+            
+            console.log('Autocomplete initialized with', patterns.length, 'patterns');
+        }
     }).catch(error => {
         logMessage(`Error initializing settings page: ${error.message}`, LOG_TYPE.ERROR);
     });
@@ -378,6 +407,53 @@ function setupEventListeners() {
             }
         });
     }
+    
+    // Save custom clear patterns button
+    const saveClearPatterns = document.getElementById('saveClearPatterns');
+    if (saveClearPatterns) {
+        saveClearPatterns.addEventListener('click', async () => {
+            const clearFromInInput = document.getElementById('customClearFromInInput');
+            const clearFromOutInput = document.getElementById('customClearFromOutInput');
+            
+            if (!clearFromInInput || !clearFromOutInput) {
+                return;
+            }
+            
+            // Validate that the entered patterns exist (if not empty)
+            const inValue = clearFromInInput.value.trim();
+            const outValue = clearFromOutInput.value.trim();
+            
+            if (inValue && window.availablePatterns && !window.availablePatterns.includes(inValue)) {
+                showStatusMessage(`Pattern not found: ${inValue}`, 'error');
+                return;
+            }
+            
+            if (outValue && window.availablePatterns && !window.availablePatterns.includes(outValue)) {
+                showStatusMessage(`Pattern not found: ${outValue}`, 'error');
+                return;
+            }
+            
+            try {
+                const response = await fetch('/api/custom_clear_patterns', {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify({
+                        custom_clear_from_in: inValue || null,
+                        custom_clear_from_out: outValue || null
+                    })
+                });
+                
+                if (response.ok) {
+                    showStatusMessage('Clear patterns saved successfully', 'success');
+                } else {
+                    const error = await response.json();
+                    throw new Error(error.detail || 'Failed to save clear patterns');
+                }
+            } catch (error) {
+                showStatusMessage(`Failed to save clear patterns: ${error.message}`, 'error');
+            }
+        });
+    }
 }
 
 // Button click handlers
@@ -548,4 +624,178 @@ function showUpdateInstructionsModal(data) {
             document.body.removeChild(modal);
         }
     });
+}
+
+// Autocomplete functionality
+function initializeAutocomplete(inputId, suggestionsId, clearButtonId, patterns) {
+    const input = document.getElementById(inputId);
+    const suggestionsDiv = document.getElementById(suggestionsId);
+    const clearButton = document.getElementById(clearButtonId);
+    let selectedIndex = -1;
+    
+    if (!input || !suggestionsDiv) return;
+    
+    // Function to update clear button visibility
+    function updateClearButton() {
+        if (clearButton) {
+            if (input.value.trim()) {
+                clearButton.classList.remove('hidden');
+            } else {
+                clearButton.classList.add('hidden');
+            }
+        }
+    }
+    
+    // Format pattern name for display
+    function formatPatternName(pattern) {
+        return pattern.replace('.thr', '').replace(/_/g, ' ');
+    }
+    
+    // Filter patterns based on input
+    function filterPatterns(searchTerm) {
+        if (!searchTerm) return patterns.slice(0, 20); // Show first 20 when empty
+        
+        const term = searchTerm.toLowerCase();
+        return patterns.filter(pattern => {
+            const name = pattern.toLowerCase();
+            return name.includes(term);
+        }).sort((a, b) => {
+            // Prioritize patterns that start with the search term
+            const aStarts = a.toLowerCase().startsWith(term);
+            const bStarts = b.toLowerCase().startsWith(term);
+            if (aStarts && !bStarts) return -1;
+            if (!aStarts && bStarts) return 1;
+            return a.localeCompare(b);
+        }).slice(0, 20); // Limit to 20 results
+    }
+    
+    // Highlight matching text
+    function highlightMatch(text, searchTerm) {
+        if (!searchTerm) return text;
+        
+        const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
+        return text.replace(regex, '<mark>$1</mark>');
+    }
+    
+    // Show suggestions
+    function showSuggestions(searchTerm) {
+        const filtered = filterPatterns(searchTerm);
+        
+        if (filtered.length === 0 && searchTerm) {
+            suggestionsDiv.innerHTML = '<div class="suggestion-item" style="cursor: default; color: #9ca3af;">No patterns found</div>';
+            suggestionsDiv.classList.remove('hidden');
+            return;
+        }
+        
+        suggestionsDiv.innerHTML = filtered.map((pattern, index) => {
+            const displayName = formatPatternName(pattern);
+            const highlighted = highlightMatch(displayName, searchTerm);
+            return `<div class="suggestion-item" data-value="${pattern}" data-index="${index}">${highlighted}</div>`;
+        }).join('');
+        
+        suggestionsDiv.classList.remove('hidden');
+        selectedIndex = -1;
+    }
+    
+    // Hide suggestions
+    function hideSuggestions() {
+        setTimeout(() => {
+            suggestionsDiv.classList.add('hidden');
+            selectedIndex = -1;
+        }, 200);
+    }
+    
+    // Select suggestion
+    function selectSuggestion(value) {
+        input.value = value;
+        hideSuggestions();
+        updateClearButton();
+    }
+    
+    // Handle keyboard navigation
+    function handleKeyboard(e) {
+        const items = suggestionsDiv.querySelectorAll('.suggestion-item[data-value]');
+        
+        if (e.key === 'ArrowDown') {
+            e.preventDefault();
+            selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
+            updateSelection(items);
+        } else if (e.key === 'ArrowUp') {
+            e.preventDefault();
+            selectedIndex = Math.max(selectedIndex - 1, -1);
+            updateSelection(items);
+        } else if (e.key === 'Enter') {
+            e.preventDefault();
+            if (selectedIndex >= 0 && items[selectedIndex]) {
+                selectSuggestion(items[selectedIndex].dataset.value);
+            } else if (items.length === 1) {
+                selectSuggestion(items[0].dataset.value);
+            }
+        } else if (e.key === 'Escape') {
+            hideSuggestions();
+        }
+    }
+    
+    // Update visual selection
+    function updateSelection(items) {
+        items.forEach((item, index) => {
+            if (index === selectedIndex) {
+                item.classList.add('selected');
+                item.scrollIntoView({ block: 'nearest' });
+            } else {
+                item.classList.remove('selected');
+            }
+        });
+    }
+    
+    // Event listeners
+    input.addEventListener('input', (e) => {
+        const value = e.target.value.trim();
+        updateClearButton();
+        if (value.length > 0 || e.target === document.activeElement) {
+            showSuggestions(value);
+        } else {
+            hideSuggestions();
+        }
+    });
+    
+    input.addEventListener('focus', () => {
+        const value = input.value.trim();
+        showSuggestions(value);
+    });
+    
+    input.addEventListener('blur', hideSuggestions);
+    
+    input.addEventListener('keydown', handleKeyboard);
+    
+    // Click handler for suggestions
+    suggestionsDiv.addEventListener('click', (e) => {
+        const item = e.target.closest('.suggestion-item[data-value]');
+        if (item) {
+            selectSuggestion(item.dataset.value);
+        }
+    });
+    
+    // Mouse hover handler
+    suggestionsDiv.addEventListener('mouseover', (e) => {
+        const item = e.target.closest('.suggestion-item[data-value]');
+        if (item) {
+            selectedIndex = parseInt(item.dataset.index);
+            const items = suggestionsDiv.querySelectorAll('.suggestion-item[data-value]');
+            updateSelection(items);
+        }
+    });
+    
+    // Clear button handler
+    if (clearButton) {
+        clearButton.addEventListener('click', () => {
+            input.value = '';
+            updateClearButton();
+            hideSuggestions();
+            input.focus();
+        });
+    }
+    
+    // Initialize clear button visibility
+    updateClearButton();
 } 

+ 116 - 0
templates/settings.html

@@ -72,6 +72,46 @@ endblock %}
 .dark .text-gray-400 {
   color: #9ca3af;
 }
+
+/* Autocomplete suggestions dark mode */
+.dark #clearFromInSuggestions,
+.dark #clearFromOutSuggestions {
+  background-color: #262626;
+  border-color: #404040;
+}
+.dark .suggestion-item {
+  color: #e5e5e5;
+}
+.dark .suggestion-item:hover {
+  background-color: #404040;
+}
+.dark .suggestion-item.selected {
+  background-color: #0c7ff2;
+  color: white;
+}
+
+/* Light mode autocomplete styles */
+.suggestion-item {
+  padding: 8px 12px;
+  cursor: pointer;
+  color: #1f2937;
+  transition: background-color 0.15s;
+}
+.suggestion-item:hover {
+  background-color: #f3f4f6;
+}
+.suggestion-item.selected {
+  background-color: #0c7ff2;
+  color: white;
+}
+.suggestion-item mark {
+  background-color: #fef3c7;
+  font-weight: 600;
+}
+.dark .suggestion-item mark {
+  background-color: #92400e;
+  color: #fef3c7;
+}
 {% endblock %}
 
 {% block content %}
@@ -146,6 +186,82 @@ endblock %}
       </div>
     </div>
   </section>
+  <section class="bg-white rounded-xl shadow-sm overflow-hidden">
+    <h2
+      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+    >
+      Clear Pattern Configuration
+    </h2>
+    <div class="px-6 py-5 space-y-6">
+      <p class="text-sm text-slate-600">
+        Customize the clear patterns used when transitioning between patterns. These patterns are used to clear the sand before drawing a new pattern.
+      </p>
+      
+      <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
+        <div class="flex flex-col gap-1.5">
+          <label for="customClearFromInInput" class="text-slate-700 text-sm font-medium leading-normal">Clear From Center Pattern</label>
+          <div class="relative">
+            <input
+              id="customClearFromInInput"
+              type="text"
+              class="form-input w-full resize-none overflow-hidden rounded-lg text-slate-900 focus:outline-0 focus:ring-2 focus:ring-sky-500 border border-slate-300 bg-white focus:border-sky-500 h-10 placeholder:text-slate-400 px-4 pr-10 text-base font-normal leading-normal transition-colors"
+              placeholder="Type to search patterns or leave empty for default"
+              autocomplete="off"
+            />
+            <button
+              type="button"
+              id="clearFromInClear"
+              class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hidden"
+              aria-label="Clear selection"
+              title="Clear selection"
+            >
+              <span class="material-icons text-xl">close</span>
+            </button>
+            <div id="clearFromInSuggestions" class="absolute z-10 w-full mt-1 bg-white border border-slate-300 rounded-lg shadow-lg max-h-60 overflow-y-auto hidden"></div>
+          </div>
+          <p class="text-xs text-slate-500 mt-1">
+            Pattern to use when clearing from the center outward.
+          </p>
+        </div>
+
+        <div class="flex flex-col gap-1.5">
+          <label for="customClearFromOutInput" class="text-slate-700 text-sm font-medium leading-normal">Clear From Perimeter Pattern</label>
+          <div class="relative">
+            <input
+              id="customClearFromOutInput"
+              type="text"
+              class="form-input w-full resize-none overflow-hidden rounded-lg text-slate-900 focus:outline-0 focus:ring-2 focus:ring-sky-500 border border-slate-300 bg-white focus:border-sky-500 h-10 placeholder:text-slate-400 px-4 pr-10 text-base font-normal leading-normal transition-colors"
+              placeholder="Type to search patterns or leave empty for default"
+              autocomplete="off"
+            />
+            <button
+              type="button"
+              id="clearFromOutClear"
+              class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hidden"
+              aria-label="Clear selection"
+              title="Clear selection"
+            >
+              <span class="material-icons text-xl">close</span>
+            </button>
+            <div id="clearFromOutSuggestions" class="absolute z-10 w-full mt-1 bg-white border border-slate-300 rounded-lg shadow-lg max-h-60 overflow-y-auto hidden"></div>
+          </div>
+          <p class="text-xs text-slate-500 mt-1">
+            Pattern to use when clearing from the perimeter inward.
+          </p>
+        </div>
+      </div>
+
+      <div class="flex justify-end">
+        <button
+          id="saveClearPatterns"
+          class="flex items-center justify-center gap-2 min-w-[140px] cursor-pointer rounded-lg h-10 px-4 bg-sky-600 hover:bg-sky-700 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors"
+        >
+          <span class="material-icons text-lg">save</span>
+          <span class="truncate">Save Clear Patterns</span>
+        </button>
+      </div>
+    </div>
+  </section>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
       class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"