← 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" wide</span>'+
+ '</label>'+
+ '<div style="font-size:10px;color:#999;margin-top:3px">← more repeats (small tile) · 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 →