Browse Source

fix: light-mode preview contrast + resolve board SD paths to host catalog

- Light-mode pattern previews deepen to #7A5A2E (same sand hue, 6:1
  contrast on the paper card) — thin lines were washing out at #A87F45.
  Dark mode unchanged.
- _from_sd_path now re-finds the playing pattern in the host catalog
  (path suffix, then unique basename — the reverse of
  make_sd_path_resolver) when the board-reported SD path doesn't exist
  locally, so Now Playing previews, play history, and favorites work for
  custom patterns that upload to the SD patterns/ root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tuanchris 5 days ago
parent
commit
3912fcb84d

+ 4 - 3
frontend/src/index.css

@@ -167,10 +167,11 @@ body {
    to the sand accent — the golden hue the touch app renders its previews in.
    Foreground-tinted lines averaged out to near-white/gray at grid size; the
    primary tokens keep them reading as sand under warm light. Chains are
-   numerically solved to land exactly on the primary tokens:
-   #A87F45 (deep sand, day) / #D9B98A (dune sand, night). */
+   numerically solved to land exactly on the targets: #7A5A2E (deep bronze,
+   day — primary's hue driven to 6:1 contrast so thin lines hold up on the
+   paper card) / #D9B98A (dune sand, night). */
 .pattern-preview {
-  filter: brightness(0) saturate(100%) invert(32%) sepia(100%) saturate(349%) hue-rotate(356deg) brightness(134%) contrast(61%);
+  filter: brightness(0) saturate(100%) invert(52%) sepia(43%) saturate(369%) hue-rotate(355deg) brightness(69%) contrast(123%);
 }
 
 .dark .pattern-preview {

+ 30 - 4
modules/core/execution.py

@@ -136,17 +136,43 @@ def _state(st: Optional[dict]) -> str:
     return ((st or {}).get("state") or "").split(":", 1)[0]
 
 
+_sd_path_cache: dict = {}
+
+
 def _from_sd_path(sd_path: str) -> Optional[str]:
     """Map a board SD path ('/patterns/x.thr', '/sd/patterns/x.thr') to the
-    host-relative path ('./patterns/x.thr') the frontend/history expect."""
+    host-relative path ('./patterns/x.thr') the frontend/history expect.
+
+    The board's SD layout can differ from the host catalog (a host
+    'custom_patterns/x.thr' uploads to SD 'patterns/x.thr'), so when the
+    literal mapping doesn't exist locally, re-find the pattern in the host
+    catalog by path suffix, then unique basename — the reverse of
+    make_sd_path_resolver's matching. Otherwise previews/history for a
+    playing custom pattern point at a path the host doesn't have."""
     if not sd_path:
         return None
+    cached = _sd_path_cache.get(sd_path)
+    if cached:
+        return cached
     p = sd_path.replace("\\", "/")
     if p.startswith("/sd/"):
         p = p[3:]
-    if not p.startswith("/"):
-        p = "/" + p
-    return "." + p
+    p = p.lstrip("/")
+    result = "./" + p
+    if not os.path.exists(result):
+        from modules.core.pattern_manager import list_theta_rho_files
+        rel = p[len("patterns/"):] if p.startswith("patterns/") else p
+        catalog = list_theta_rho_files()
+        hits = [r for r in catalog if r == rel or r.endswith("/" + rel)]
+        if not hits:
+            base = rel.rsplit("/", 1)[-1]
+            hits = [r for r in catalog if r.rsplit("/", 1)[-1] == base]
+        if len(hits) == 1:
+            result = "./patterns/" + hits[0]
+    if len(_sd_path_cache) > 512:
+        _sd_path_cache.clear()
+    _sd_path_cache[sd_path] = result
+    return result
 
 
 # ---------------------------------------------------------------------------

File diff suppressed because it is too large
+ 0 - 0
static/dist/assets/index-Djx5fY-0.css


+ 0 - 0
static/dist/assets/index-BCKILkcX.js → static/dist/assets/index-ZKmcHv9J.js


+ 2 - 2
static/dist/index.html

@@ -58,8 +58,8 @@
           .catch(function() {});
       })();
     </script>
-    <script type="module" crossorigin src="/assets/index-BCKILkcX.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-BQCSo6mO.css">
+    <script type="module" crossorigin src="/assets/index-ZKmcHv9J.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-Djx5fY-0.css">
   <script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
   <body>
     <div id="root"></div>

File diff suppressed because it is too large
+ 0 - 0
static/dist/sw.js


+ 13 - 0
tests/unit/test_execution.py

@@ -113,6 +113,19 @@ class TestMapping:
         assert _from_sd_path("/sd/patterns/x.thr") == "./patterns/x.thr"
         assert _from_sd_path("") is None
 
+    def test_sd_path_refinds_relocated_pattern(self, tmp_path, monkeypatch):
+        # A host custom pattern uploads to SD as 'patterns/<basename>'; the
+        # board reports that SD path, and the mapping must re-find the host
+        # copy (custom_patterns/...) rather than a path that doesn't exist.
+        from modules.core import execution, pattern_manager
+        (tmp_path / "custom_patterns").mkdir()
+        (tmp_path / "custom_patterns" / "capybara.thr").write_text("0 0\n")
+        monkeypatch.setattr(pattern_manager, "THETA_RHO_DIR", str(tmp_path))
+        monkeypatch.setattr(execution, "_sd_path_cache", {})
+        assert _from_sd_path("/sd/patterns/capybara.thr") == "./patterns/custom_patterns/capybara.thr"
+        # Unknown-everywhere paths keep the literal mapping as fallback
+        assert _from_sd_path("/sd/patterns/nope.thr") == "./patterns/nope.thr"
+
 
 class TestTranslateStatus:
     def test_offline(self):

Some files were not shown because too many files changed in this diff