[object Object]

← back to Wallco Ai

studio: add repeat-size slider + fresh room re-renders on each compose

0bbf445f975670aeaffe2ac8a76af24d3f3fff73 · 2026-05-12 12:43:21 -0700 · SteveStudio2

Slider (12-108") in the post-compose payoff frame re-fires the 3 room
renders (bedroom/living/dining) at the chosen patternWidth/Height inches.
Pattern stays seamless at every scale — slider just changes how many
repeats show on the wall.

Each render batch owns an AbortController so a new Compose or slider
drag cancels in-flight room fetches before kicking off the next batch,
which fixes the case where stale room images from a previous design
appeared underneath the freshly-composed pattern. Loading state is
reset on every batch so the visual refresh is immediate.

Default 72" (close to one full-pattern repeat per wall) matches the
'starts at max size' UX Steve asked for.

Files touched

Diff

commit 0bbf445f975670aeaffe2ac8a76af24d3f3fff73
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 12:43:21 2026 -0700

    studio: add repeat-size slider + fresh room re-renders on each compose
    
    Slider (12-108") in the post-compose payoff frame re-fires the 3 room
    renders (bedroom/living/dining) at the chosen patternWidth/Height inches.
    Pattern stays seamless at every scale — slider just changes how many
    repeats show on the wall.
    
    Each render batch owns an AbortController so a new Compose or slider
    drag cancels in-flight room fetches before kicking off the next batch,
    which fixes the case where stale room images from a previous design
    appeared underneath the freshly-composed pattern. Loading state is
    reset on every batch so the visual refresh is immediate.
    
    Default 72" (close to one full-pattern repeat per wall) matches the
    'starts at max size' UX Steve asked for.
---
 server.js | 105 +++++++++++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 80 insertions(+), 25 deletions(-)

diff --git a/server.js b/server.js
index 5acb08c..42e5978 100644
--- a/server.js
+++ b/server.js
@@ -8520,48 +8520,103 @@ document.getElementById('btn-compose').addEventListener('click', async function(
             '</div>'+
           '</div>'+
         '</div>'+
+        // Repeat-size control — pattern stays seamless; user picks how big a single repeat
+        // shows on the wall. Slider min = lots of small repeats; max = one giant pattern.
+        // Default sits at the larger end so the first render shows the design near full-size.
+        '<div style="padding:10px 14px;background:#fff;border-top:1px solid #eee;border-bottom:1px solid #eee">'+
+          '<label style="font-size:11px;color:#444;display:flex;align-items:center;gap:10px;cursor:pointer">'+
+            '<span style="white-space:nowrap;letter-spacing:.04em">Repeat size</span>'+
+            '<input type="range" id="studio-repeat-size" min="12" max="108" value="72" step="6" style="flex:1;accent-color:#0f0e0c">'+
+            '<span id="studio-repeat-val" style="font-family:ui-monospace,Menlo,monospace;color:#0f0e0c;min-width:70px;text-align:right;font-size:11px">72&quot; wide</span>'+
+          '</label>'+
+          '<div style="font-size:10px;color:#999;margin-top:3px">← more repeats (small tile) &nbsp;·&nbsp; one giant pattern →. Pattern stays seamless at every size.</div>'+
+        '</div>'+
         // 3 room settings row — one per category, in parallel.
-        '<div style="padding:14px 14px 12px;background:#fafafa;border-top:1px solid #eee">'+
+        '<div style="padding:14px 14px 12px;background:#fafafa">'+
           '<div style="font-size:10px;color:#999;letter-spacing:.08em;text-transform:uppercase;margin-bottom:8px">See it in a room · 3 categories</div>'+
           '<div id="studio-rooms-grid" style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px">'+
             '<div class="studio-room-cell" data-room="bedroom"     data-label="Bedroom"      style="position:relative;border:1px solid #eee;border-radius:6px;aspect-ratio:4/3;background:#fafafa center/cover no-repeat;display:flex;align-items:center;justify-content:center;color:#999;font-size:11px;overflow:hidden">Bedroom · loading…</div>'+
             '<div class="studio-room-cell" data-room="living_room" data-label="Living Room"  style="position:relative;border:1px solid #eee;border-radius:6px;aspect-ratio:4/3;background:#fafafa center/cover no-repeat;display:flex;align-items:center;justify-content:center;color:#999;font-size:11px;overflow:hidden">Living Room · loading…</div>'+
             '<div class="studio-room-cell" data-room="dining_room" data-label="Dining Room"  style="position:relative;border:1px solid #eee;border-radius:6px;aspect-ratio:4/3;background:#fafafa center/cover no-repeat;display:flex;align-items:center;justify-content:center;color:#999;font-size:11px;overflow:hidden">Dining Room · loading…</div>'+
           '</div>'+
-          '<div style="font-size:9px;color:#bbb;margin:6px 0 0;letter-spacing:.04em">Rendered in parallel · ~10-30s each</div>'+
+          '<div style="font-size:9px;color:#bbb;margin:6px 0 0;letter-spacing:.04em">Rendered in parallel · ~10-30s each · drag the slider to re-render at a different scale</div>'+
         '</div>'+
       '</div>'
     );
     // R0b/T18 — kick off 3 room renders in parallel (one per category).
     // The first room that lands is linked onto this studio_renders row so the
     // personal gallery still shows pattern+room together.
-    var roomCells = document.querySelectorAll('#studio-rooms-grid .studio-room-cell');
+    //
+    // Slider-driven re-render: any change to #studio-repeat-size aborts in-flight
+    // fetches and fires a new batch with the updated patternWidth/Height. Each
+    // payoff frame owns its own AbortController so a fresh Compose can't race
+    // with an old slider drag.
     var roomLinked = false;
-    Array.prototype.forEach.call(roomCells, function(cell){
-      var rt = cell.dataset.room;
-      var label = cell.dataset.label;
-      fetch('/api/room', {
-        method:'POST', headers:{'Content-Type':'application/json'},
-        body: JSON.stringify({ design_id: j.design_id, roomType: rt })
-      }).then(function(r){ return r.json(); }).then(function(rd){
-        if (rd && rd.ok && rd.image_url) {
-          cell.style.backgroundImage = 'url(\\''+rd.image_url+'\\')';
-          cell.innerHTML = '<span style="position:absolute;bottom:6px;left:8px;background:rgba(0,0,0,.6);color:#fff;padding:2px 6px;border-radius:3px;font-size:9px;text-transform:uppercase;letter-spacing:.06em">'+label+' · #'+rd.room_id+'</span>';
-          if (rd.room_id && !roomLinked) {
-            roomLinked = true;
-            fetch('/api/studio-renders/'+j.design_id+'/link-room', {
-              method:'POST', headers:{'Content-Type':'application/json'},
-              body: JSON.stringify({ sid: SID, room_id: rd.room_id })
-            });
-            if (typeof loadMyRenders === 'function') loadMyRenders();
+    var roomAbort = null;
+    function renderStudioRooms() {
+      if (roomAbort) { try { roomAbort.abort(); } catch(_) {} }
+      roomAbort = new AbortController();
+      var signal = roomAbort.signal;
+      var sliderEl = document.getElementById('studio-repeat-size');
+      var inches = sliderEl ? parseInt(sliderEl.value, 10) || 72 : 72;
+      var cells = document.querySelectorAll('#studio-rooms-grid .studio-room-cell');
+      // Reset every cell to the loading state before firing new fetches —
+      // this is what makes the visual refresh feel instant on each slider tick.
+      Array.prototype.forEach.call(cells, function(cell){
+        cell.style.backgroundImage = '';
+        cell.innerHTML = cell.dataset.label + ' · loading…';
+      });
+      Array.prototype.forEach.call(cells, function(cell){
+        var rt = cell.dataset.room;
+        var label = cell.dataset.label;
+        fetch('/api/room', {
+          method:'POST', headers:{'Content-Type':'application/json'},
+          signal: signal,
+          body: JSON.stringify({
+            design_id: j.design_id,
+            roomType: rt,
+            patternWidth: inches,
+            patternHeight: inches
+          })
+        }).then(function(r){ return r.json(); }).then(function(rd){
+          if (signal.aborted) return;
+          if (rd && rd.ok && rd.image_url) {
+            // Cache-bust on the rendered URL so an old image with the same
+            // path (rare, but possible if the server reuses filenames) can't
+            // hang around in the browser cache after a re-render.
+            cell.style.backgroundImage = 'url(\\''+rd.image_url+'?t='+Date.now()+'\\')';
+            cell.innerHTML = '<span style="position:absolute;bottom:6px;left:8px;background:rgba(0,0,0,.6);color:#fff;padding:2px 6px;border-radius:3px;font-size:9px;text-transform:uppercase;letter-spacing:.06em">'+label+' · #'+rd.room_id+'</span>';
+            if (rd.room_id && !roomLinked) {
+              roomLinked = true;
+              fetch('/api/studio-renders/'+j.design_id+'/link-room', {
+                method:'POST', headers:{'Content-Type':'application/json'},
+                body: JSON.stringify({ sid: SID, room_id: rd.room_id })
+              });
+              if (typeof loadMyRenders === 'function') loadMyRenders();
+            }
+          } else {
+            cell.innerHTML = '<span style="color:#c44;font-size:11px">'+label+' failed</span>';
           }
-        } else {
+        }).catch(function(err){
+          if (err && err.name === 'AbortError') return;
           cell.innerHTML = '<span style="color:#c44;font-size:11px">'+label+' failed</span>';
-        }
-      }).catch(function(){
-        cell.innerHTML = '<span style="color:#c44;font-size:11px">'+label+' failed</span>';
+        });
       });
-    });
+    }
+    renderStudioRooms();
+    // Slider wiring — debounce so dragging doesn't fire 60 renders/sec.
+    var sliderEl = document.getElementById('studio-repeat-size');
+    var sliderVal = document.getElementById('studio-repeat-val');
+    var sliderDebounce = null;
+    if (sliderEl && sliderVal) {
+      sliderEl.addEventListener('input', function(){
+        sliderVal.textContent = sliderEl.value + '" wide';
+      });
+      sliderEl.addEventListener('change', function(){
+        if (sliderDebounce) clearTimeout(sliderDebounce);
+        sliderDebounce = setTimeout(renderStudioRooms, 250);
+      });
+    }
     // Delete button — wired by R0d.
     var delBtn = document.getElementById('studio-delete-btn');
     if (delBtn) delBtn.addEventListener('click', function(){

← 38e639c wallco.ai · /compare view choice (specs/grid) now persists i  ·  back to Wallco Ai  ·  test: studio repeat-size slider — 5 cases covering UX shippe 965dff1 →