updates UI

This commit is contained in:
mike
2026-06-30 02:27:31 +02:00
parent ad9a2ae078
commit 5cea78c406
2 changed files with 230 additions and 131 deletions

View File

@@ -77,6 +77,10 @@ Scenery:
- add a 2nd visible filmstrip to easy move items from one to another strip in the UI
- on the landing page we now just play all images of an group. However if the orbit generaition is complete. I want to circulate those images , (this basically is a new filmstrip feature)) so we set the orbit in a filmstrip by default, but can modify it. Also we want the face to be visible in the landing page and the most prominent (most 3d alike full body with femine descriptors)) image
- still looking for that killer-feminine shot, one image that would show most characteristics of a nude female including eyes.
- scrolling past the Scne - STANDALONE / UPLOADED IMAGES still is unpleasant. I think all images get completly resized and 100x recalculated when moving the scrollbar there.
- introduce bulk select in trash.html
- tab "SAM" checkerboard icon to studio-view overlay
- in the landing page hide SOURCE images
-
## refine

View File

@@ -2011,7 +2011,7 @@
<span id="lbCounter" style="font-size:12px;color:#555"></span>
</div>
<div class="studio-topbar-right">
<button class="btn" id="sidebarToggleTopBtn" onclick="toggleSidebar()" style="font-size:11px;padding:4px 10px" title="Toggle toolbox">Tools</button>
<button class="btn" id="sidebarInnerToggle" onclick="toggleSidebar()" title="Collapse/expand toolbox"></button>
</div>
</div>
@@ -2040,6 +2040,7 @@
<button onclick="switchFilmstripTab('source')" class="fs-tab" id="fsTabSource" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'SOURCE')">Source</button>
<button onclick="switchFilmstripTab('archived')" class="fs-tab" id="fsTabArchived" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'ARCHIVED')">Archived</button>
<button onclick="switchFilmstripTab('video')" class="fs-tab" id="fsTabVideo" ondragover="event.preventDefault()">Videos</button>
<button onclick="switchFilmstripTab('orbit')" class="fs-tab" id="fsTabOrbitFilter" style="display:none" ondragover="event.preventDefault()">Orbits</button>
</div>
<div style="display:flex;gap:6px;align-items:center" id="multiSelectControls">
<button onclick="toggleMultiSelectMode()" id="multiSelectToggleBtn" style="background:#222;border:1px solid #333;color:#888;font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer">Multi-select</button>
@@ -2071,30 +2072,28 @@
<!-- Sidebar toolbox -->
<div id="studioSidebar" class="studio-sidebar">
<button class="sidebar-toggle-btn" id="sidebarInnerToggle" onclick="toggleSidebar()" title="Collapse/expand toolbox"></button>
<div id="sidebarInner" class="sidebar-inner">
<div class="sidebar-tabs">
<button class="sb-tab active" data-tab="info" onclick="switchSidebarTab('info')">Info</button>
<button class="sb-tab" data-tab="generate" onclick="switchSidebarTab('generate')">Generate</button>
<button class="sb-tab" data-tab="faceswap" onclick="switchSidebarTab('faceswap')">Face</button>
<button class="sb-tab" data-tab="scenery" onclick="switchSidebarTab('scenery')">Scene</button>
<button class="sb-tab" data-tab="segment" onclick="switchSidebarTab('segment')">SAM</button>
<button class="sb-tab" data-tab="orbit" onclick="switchSidebarTab('orbit')" id="sbTabOrbit">Orbit</button>
<button class="sb-tab" data-tab="generate" onclick="switchSidebarTab('generate')">Ceate</button>
<button class="sb-tab" data-tab="faceswap" onclick="switchSidebarTab('faceswap')">👤</button>
<button class="sb-tab" data-tab="scenery" onclick="switchSidebarTab('scenery')">🎬</button>
<button class="sb-tab" data-tab="segment" onclick="switchSidebarTab('segment')">α</button>
<button class="sb-tab" data-tab="orbit" onclick="switchSidebarTab('orbit')" id="sbTabOrbit"></button>
<button class="sb-tab" data-tab="designer" onclick="switchSidebarTab('designer')" id="sbTabDesigner">Design</button>
</div>
<div class="sb-panels">
<!-- Info panel: static HTML with existing lb* IDs so updateStudio() just works -->
<div class="sb-panel active" id="sbPanelInfo">
<div class="sb-label">Image name</div>
<input type="text" class="sb-input" id="lbNameInput"
placeholder="Image name…"
onblur="updateName()" onkeydown="if(event.key==='Enter')this.blur()"
style="margin-bottom:8px">
<div id="lbClipDesc" class="sb-status" style="margin-bottom:6px;font-size:11px;color:#555"></div>
<div id="lbGenPromptWrap" style="display:none;margin-bottom:8px">
<div class="sb-label" style="margin-bottom:3px">Generation prompt</div>
<div id="lbGenPrompt" style="font-size:11px;color:#888;line-height:1.4;word-break:break-word"></div>
</div>
<div id="lbSourceRefsWrap" style="display:none;margin-bottom:8px">
<div class="sb-label" style="margin-bottom:3px">Reference Images</div>
<div id="lbSourceRefs" class="sb-source-refs"></div>
</div>
<div id="lbClipDesc" class="sb-status" style="margin-bottom:6px;font-size:11px;color:#555"></div>
<div id="lbFaceBook" style="display:none;margin-bottom:8px">
<div class="sb-label" style="margin-bottom:4px">Face reference</div>
<div style="display:flex;gap:8px;align-items:flex-start">
@@ -2167,16 +2166,11 @@
<div class="sb-actions" style="margin-bottom:8px">
<button class="sb-btn" onclick="tagCurrentImage()" title="Re-run tagger">Re-tag</button>
<button class="sb-btn purple" id="lbUndressBtn" style="display:none" onclick="lbUndress()">Undress</button>
<button class="sb-btn teal" id="lbFaceswapBtn" onclick="lbFaceswap()">Faceswap →</button>
</div>
<div class="sb-actions" style="margin-bottom:8px">
<button class="sb-btn" id="lbExtract" style="display:none" onclick="extractCurrentImage()">Extract</button>
<button class="sb-btn" id="lbNoBgBtn" onclick="lbRemoveBg()">No BG</button>
<button class="sb-btn" id="lbInvertAlphaBtn" onclick="lbInvertAlpha()" title="Invert transparency — fixes when the wrong segment was kept">Invert α</button>
<button class="sb-btn" id="lbCropBtn" style="display:none" onclick="lbAutoCrop()" title="Crop away transparent border">Crop</button>
<button class="sb-btn" id="lbDuplicateBtn" onclick="lbDuplicate()" title="Duplicate image into same group">Duplicate</button>
<button class="sb-btn" id="lbManualCropBtn" onclick="startManualCrop()" title="Drag to crop image">Crop…</button>
<button class="sb-btn" id="lbManualPadBtn" onclick="startManualPad()" title="Pad to expand canvas by padding each side">Pad</button>
<button class="sb-btn" id="lbRotateLeftBtn" onclick="lbRotate(-90)" title="Rotate 90° counter-clockwise">⟲ 90°</button>
<button class="sb-btn" id="lbRotateRightBtn" onclick="lbRotate(90)" title="Rotate 90° clockwise">⟳ 90°</button>
<button class="sb-btn" id="lbPoseBtn" onclick="lbTogglePose()" title="Preview body pose skeleton">Pose</button>
@@ -2186,7 +2180,20 @@
<button class="sb-btn" id="lbArchiveBtn" onclick="lbArchive()" style="color:#f59e0b" title="Move to archive (recoverable)">Archive</button>
<button class="sb-btn danger" id="lbDeleteBtn" onclick="lbDeleteArm()" title="Click once to arm, then again to confirm">Delete</button>
</div>
<div id="lbSourceRefs" class="sb-source-refs" style="display:none"></div>
<div class="sb-sep"></div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
<div class="sb-label" style="margin:0">Image name</div>
<button class="sb-btn" onclick="copyRawFilename()" style="padding:2px 6px;font-size:9px;min-width:auto;display:inline-flex;align-items:center;gap:4px" title="Copy raw filename to clipboard">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>Copy
</button>
</div>
<input type="text" class="sb-input" id="lbNameInput"
placeholder="Image name…"
onblur="updateName()" onkeydown="if(event.key==='Enter')this.blur()"
style="margin-bottom:8px">
</div>
<!-- Other panels: rendered dynamically -->
<div class="sb-panel" id="sbPanelGenerate"></div>
@@ -2346,6 +2353,31 @@
// --- HYDRATION_START ---
const PRELOADED_IMAGES = [
"20260630_021844_view_000_000deg.png",
"20260630_022002_view_000_000deg.png",
"20260630_021951_view_000_000deg.png",
"20260630_021940_view_000_000deg.png",
"20260630_021929_view_000_000deg.png",
"20260630_021906_view_000_000deg.png",
"20260630_021855_view_000_000deg.png",
"20260630_021833_view_000_000deg.png",
"20260630_021821_view_000_000deg.png",
"20260630_021810_view_000_000deg.png",
"20260630_021759_view_000_000deg.png",
"20260630_021748_view_000_000deg.png",
"20260630_021737_view_000_000deg.png",
"20260630_021726_view_000_000deg.png",
"20260630_021715_view_000_000deg.png",
"_turntable/cg_ad73335e/views/view_000_000deg.png",
"20260630_021650_view_000_000deg.png",
"20260630_021342_sc_view_023_345deg.png",
"20260630_021047_sc_view_023_345deg.png",
"20260630_020706_sc_view_023_345deg.png",
"20260630_020632_sc_view_023_345deg.png",
"20260630_020621_sc_view_023_345deg.png",
"20260630_020517_sc_view_023_345deg.png",
"20260630_020313_sc_view_023_345deg.png",
"20260630_020143_sc_view_023_345deg.png",
"20260629_212135_image.png",
"20260629_211901_dup_view_002_030deg.png",
"20260629_211841_dup_view_002_030deg.png",
@@ -2353,7 +2385,6 @@
"20260629_211613_dup_view_002_030deg.png",
"20260629_211543_dup_view_002_030deg.png",
"20260629_205647_dup_view_002_030deg.png",
"20260629_211323_test_upload_image.png",
"20260629_211213_dup_view_002_030deg.png",
"20260629_210320_dup_view_002_030deg.png",
"20260629_210225_dup_view_002_030deg.png",
@@ -5219,7 +5250,6 @@
"_turntable/cg_ad73335e/views/view_005_075deg.png",
"_turntable/cg_ad73335e/views/view_004_060deg.png",
"_turntable/cg_ad73335e/views/view_003_045deg.png",
"_turntable/cg_ad73335e/views/view_000_000deg.png",
"_turntable/cg_61b863a8/views/view_023_345deg.png",
"_turntable/cg_61b863a8/views/view_022_330deg.png",
"_turntable/cg_61b863a8/views/view_021_315deg.png",
@@ -7791,13 +7821,43 @@
let _multiSelectModeActive = false;
const _selectedFilenames = new Set();
function isOrbitFile(fname) {
if (!fname) return false;
return fname.includes('_turntable/') || /deg\.png|deg_|\d+deg/i.test(fname);
}
function getRepresentativeLabel(n) {
if (isOrbitFile(n)) {
const m = n.match(/_(\d+)deg/i) || n.match(/(\d+)deg/i) || n.match(/view_(\d+)/i);
if (m) {
let num = m[1].replace(/^0+/, '');
if (!num) num = '0';
return `Orbit ${num}°`;
}
return 'Orbit';
}
if (n.endsWith('_face.png') || n.includes('_face.')) {
return '👤 Face Crop';
}
if (fileIsSource[n]) {
return '★ Source';
}
if (filePoses[n]) {
return filePoses[n];
}
if (isVideo(n) || fileContentType[n] === 'video') {
return '🎬 Video';
}
return '';
}
function switchFilmstripTab(tab) {
_activeFilmstripTab = tab;
// Highlight tab
['Active', 'Group', 'Hidden', 'Source', 'Archived', 'Video'].forEach(t => {
['Active', 'Group', 'Hidden', 'Source', 'Archived', 'Video', 'OrbitFilter'].forEach(t => {
const el = document.getElementById(`fsTab${t}`);
if (el) el.classList.toggle('active', t.toLowerCase() === tab);
if (el) el.classList.toggle('active', t.toLowerCase().replace('filter', '') === tab);
});
// Reset selection when switching tabs
@@ -7810,12 +7870,13 @@
const data = groupData.get(lbCurrentGid);
if (data) {
if (tab === 'active') {
// Filter out hidden, source, archived, and videos
// Filter out hidden, source, archived, videos, and orbits
const activeNames = [];
const activeUrls = [];
data.names.forEach((name, i) => {
const isVid = isVideo(name) || fileContentType[name] === 'video';
if (!fileHidden[name] && !fileIsSource[name] && !fileArchived[name] && !isVid) {
const isOrb = isOrbitFile(name);
if (!fileHidden[name] && !fileIsSource[name] && !fileArchived[name] && !isVid && !isOrb) {
activeNames.push(name);
activeUrls.push(data.urls[i]);
}
@@ -7847,6 +7908,8 @@
matched.push(fname);
} else if (tab === 'video' && isVid && !isArchived) {
matched.push(fname);
} else if (tab === 'orbit' && isOrbitFile(fname) && !isArchived) {
matched.push(fname);
}
});
@@ -8476,8 +8539,10 @@
if (!rect) return;
const isManual = !!document.getElementById('padBar');
if (!isManual) return;
const getV = (side) => {
const el = document.getElementById(isManual ? 'pad' + side : 'sbPad' + side);
const el = document.getElementById('pad' + side);
return el ? el.value : '0';
};
@@ -8496,7 +8561,6 @@
overlay.className = 'pad-preview-overlay';
// Style based on selected fill mode to make preview professional & intuitive
if (isManual) {
if (fill === 'black') {
overlay.style.background = 'rgba(0, 0, 0, 0.7)';
overlay.style.borderColor = '#444';
@@ -8507,11 +8571,6 @@
overlay.style.background = 'rgba(245, 158, 11, 0.4)';
overlay.style.borderColor = '#f59e0b';
}
} else {
// Sidebar is always transparent background before outpaint
overlay.style.background = 'rgba(245, 158, 11, 0.4)';
overlay.style.borderColor = '#f59e0b';
}
if (side === 'Left' || side === 'Right') {
const w = isPercent ? (rect.width * val / 100) : val;
@@ -8530,6 +8589,9 @@
}
viewer.appendChild(overlay);
});
// Re-render Generate tab button if active
updateSbGenBtn();
}
function closeStudio() {
@@ -8849,9 +8911,11 @@
// Source refs
const refs = fileSourceRefs[fname];
const refsDiv = document.getElementById('lbSourceRefs');
const refsWrap = document.getElementById('lbSourceRefsWrap');
if (refsDiv) {
if (refs && refs.length > 0) {
refsDiv.style.display = 'flex';
if (refsWrap) refsWrap.style.display = 'block';
refsDiv.innerHTML = refs.map(r => {
let srcUrl = '';
let isGallery = true;
@@ -8879,6 +8943,7 @@
}).join('');
} else {
refsDiv.style.display = 'none';
if (refsWrap) refsWrap.style.display = 'none';
}
}
@@ -8901,13 +8966,21 @@
const prefBtn = document.getElementById('lbPreferredBtn');
if (prefBtn) prefBtn.style.display = multi && lbIdx > 0 ? '' : 'none';
// Dynamically show or hide the Orbit tab filter depending on whether orbit files exist in this group
const orbitFilterBtn = document.getElementById('fsTabOrbitFilter');
if (orbitFilterBtn) {
const data = groupData.get(lbCurrentGid);
const hasOrbits = data && data.names.some(isOrbitFile);
orbitFilterBtn.style.display = hasOrbits ? '' : 'none';
}
// Film strip — always visible (override old display:none CSS)
const strip = document.getElementById('lbVariantStrip');
strip.style.display = 'flex';
const sceneryActive = _activeSidebarTab === 'scenery';
strip.innerHTML = lbUrls.map((url, i) => {
const n = lbNames[i];
const pose = filePoses[n] || '';
const pose = getRepresentativeLabel(n);
const act = i === lbIdx ? ' active' : '';
const isSel = _selectedFilenames.has(n);
const selCls = isSel ? ' selected' : '';
@@ -10423,9 +10496,19 @@
if (btn) btn.disabled = false;
}
function copyRawFilename() {
const fname = lbNames[lbIdx];
if (!fname) return;
navigator.clipboard.writeText(fname).then(() => {
showToast('Filename copied to clipboard', 'success');
}).catch(err => {
showToast('Failed to copy: ' + err, 'error');
});
}
async function lbRemoveBg() {
const fname = lbNames[lbIdx];
const btn = document.getElementById('lbNoBgBtn');
const btn = document.getElementById('lbNoBgBtn') || document.getElementById('sbNoBgBtn');
if (btn) { btn.disabled = true; btn.textContent = 'Removing…'; }
showToast('Removing background…');
try {
@@ -12028,9 +12111,8 @@
}
if ((e.key === 'c' || e.key === 'C') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
if (document.getElementById('studio').classList.contains('open')) {
const cb = document.getElementById('sbCheckerboard');
const viewer = document.getElementById('studioViewer');
const cur = viewer ? viewer.classList.contains('show-checker') : (cb ? cb.checked : false);
const cur = viewer ? viewer.classList.contains('show-checker') : false;
toggleCheckerboard(!cur);
return;
}
@@ -12352,12 +12434,6 @@
const savedScrollTop = panel.scrollTop;
// Preserve live input values before re-render
const prevPromptVal = document.getElementById('sbGenPromptInput')?.value ?? '';
_sbPadTop = document.getElementById('sbPadTop')?.value ?? _sbPadTop;
_sbPadRight = document.getElementById('sbPadRight')?.value ?? _sbPadRight;
_sbPadBottom = document.getElementById('sbPadBottom')?.value ?? _sbPadBottom;
_sbPadLeft = document.getElementById('sbPadLeft')?.value ?? _sbPadLeft;
_sbPadOutpaint = document.getElementById('sbPadOutpaint')?.checked ?? _sbPadOutpaint;
_sbPadUniform = document.getElementById('sbPadUniform')?.checked ?? _sbPadUniform;
_fsSelectedPoses.forEach(name => {
const el = document.getElementById('sbPoseEdit_' + CSS.escape(name));
if (el) _sbPoseEdits[name] = el.value;
@@ -12366,32 +12442,40 @@
const donePoses = new Set();
lbNames.forEach(n => { if (filePoses[n]) donePoses.add(filePoses[n]); });
// Camera angles section — absolute positions
// Camera angles section — absolute positions (default collapsed)
const absAngles = CAMERA_ANGLES.filter(a => !a.relative);
const relAngles = CAMERA_ANGLES.filter(a => a.relative);
const ORBIT_NAMES = ['Front', '¾ Left', '¾ Right', 'Side L', 'Side R', 'Back'];
const allOrbitSel = ORBIT_NAMES.every(n => _sbSelectedAngles.has(n));
let html = `<div style="display:flex;align-items:center;gap:6px;margin-bottom:3px">
<span class="sb-label" style="margin:0;flex:1">Camera — absolute</span>
<button class="sb-btn${allOrbitSel?' primary':''}" style="padding:2px 8px;font-size:10px" onclick="sbToggleOrbitAll()" title="Select all 6 orbit angles (front · ¾ · side · back)">${allOrbitSel?'✓ Orbit all':'Orbit all'}</button>
</div><div class="sb-camera-grid" id="sbCameraGrid">`;
let html = `
<details style="margin-bottom:8px">
<summary class="sb-label" style="cursor:pointer;outline:none;display:flex;align-items:center;gap:6px">
<span>Camera — absolute</span>
</summary>
<div style="display:flex;justify-content:flex-end;margin:4px 0">
<button class="sb-btn${allOrbitSel?' primary':''}" style="padding:2px 8px;font-size:10px;margin:0" onclick="sbToggleOrbitAll()" title="Select all 6 orbit angles (front · ¾ · side · back)">${allOrbitSel?'✓ Orbit all':'Orbit all'}</button>
</div>
<div class="sb-camera-grid" id="sbCameraGrid" style="margin-top:4px">`;
html += absAngles.map(a => {
const sel = _sbSelectedAngles.has(a.name) ? ' selected' : '';
const done = donePoses.has(a.name) ? ' done' : '';
const nSafe = a.name.replace(/'/g, "\\'");
return `<button class="sb-angle-btn${done}${sel}" onclick="toggleSbAngle('${nSafe}')" title="${escHtml(a.prompt)}">${a.icon}<br><span style="font-size:9px">${escHtml(a.name)}</span></button>`;
}).join('');
html += '</div>';
html += '</div></details>';
// Relative rotations
html += '<div class="sb-label" style="margin-top:6px">Camera — relative</div><div class="sb-camera-grid" id="sbRelAngleGrid">';
// Relative rotations (default collapsed)
html += `
<details style="margin-bottom:8px">
<summary class="sb-label" style="cursor:pointer;outline:none">Camera — relative</summary>
<div class="sb-camera-grid" id="sbRelAngleGrid" style="margin-top:6px">`;
html += relAngles.map(a => {
const sel = _sbSelectedAngles.has(a.name) ? ' selected' : '';
const done = donePoses.has(a.name) ? ' done' : '';
const nSafe = a.name.replace(/'/g, "\\'");
return `<button class="sb-angle-btn${done}${sel}" onclick="toggleSbAngle('${nSafe}')" title="${escHtml(a.prompt)}">${a.icon}<br><span style="font-size:9px">${escHtml(a.name)}</span></button>`;
}).join('');
html += '</div>';
html += '</div></details>';
// Wireframe pose reference
const wfSel = _sbWireframeRef || '';
@@ -12485,18 +12569,6 @@
});
}
// Add a brand-new pose (persisted to poses.md).
html += `<div class="sb-sep"></div>
<details style="margin-bottom:6px">
<summary style="font-size:11px;color:#888;cursor:pointer">+ New pose</summary>
<input type="text" id="sbNewPoseName" placeholder="Pose name"
style="width:100%;box-sizing:border-box;background:#111;border:1px solid #2a2a2a;border-radius:5px;color:#ccc;font-size:11px;padding:6px;margin:6px 0 4px">
<textarea id="sbNewPoseText" placeholder="Prompt text"
style="width:100%;box-sizing:border-box;background:#111;border:1px solid #2a2a2a;border-radius:5px;color:#ccc;font-size:11px;padding:6px;resize:vertical;min-height:48px"></textarea>
<label style="font-size:10px;color:#777;display:flex;align-items:center;gap:3px;cursor:pointer;margin-top:4px"><input type="checkbox" id="sbNewPoseBeta">beta</label>
<button class="sb-btn primary" style="margin-top:4px;font-size:10px;padding:3px 10px" onclick="addNewPose()">Add pose</button>
</details>`;
// Sticky footer — always visible without scrolling: refs + custom prompt + Generate
html += `<div class="sb-gen-footer">`;
if (_sbRefIndices.length > 0) {
@@ -12526,33 +12598,7 @@
: 'One edit per selected reference.'}</div>`;
}
}
html += `<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:8px">
<span style="font-size:11px;color:#666" title="Expand canvas before generation — gives the model space to outpaint">Pad:</span>
${['Top','Right','Bottom','Left'].map(side => {
const val = side==='Top'?_sbPadTop:side==='Right'?_sbPadRight:side==='Bottom'?_sbPadBottom:_sbPadLeft;
const onInput = `syncPadFields(this)`;
const onBlur = `this.value = formatPadValue(this.value); ${onInput}`;
const label = side === 'Top' ? 'T:' : side === 'Right' ? 'R:' : side === 'Bottom' ? 'B:' : 'L:';
return `<label style="display:flex;align-items:center;gap:2px;font-size:11px;color:#888">${label}
<input type="text" id="sbPad${side}" value="${val}" placeholder="px or %"
style="width:48px;background:#111;border:1px solid #2a2a2a;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right"
oninput="${onInput}" onblur="${onBlur}"
onkeydown="if(event.key === 'Enter') { submitSbGenerate(); event.preventDefault(); }"></label>`;
}).join('')}
<label style="display:flex;align-items:center;gap:2px;font-size:11px;color:#888;cursor:pointer" title="Keep all padding values equal">
<input type="checkbox" id="sbPadUniform" ${_sbPadUniform?'checked':''} onchange="togglePadUniform(this)" style="cursor:pointer">Uniform</label>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('10%', true)">+10%</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('20%', true)">+20%</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('50%', true)">+50%</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('1:1', true)">1:1</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('4:3', true)">4:3</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('3:4', true)">3:4</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('16:9', true)">16:9</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0;background:#3b0764;color:#d8b4fe;border-color:#581c87" onclick="applyPadPreset('0', true)">Reset</button>
<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#888;cursor:pointer" title="Instruct the model to fill in the padded area">
<input type="checkbox" id="sbPadOutpaint" ${(_sbPadOutpaint??false)?'checked':''} onchange="updateSbGenBtn()" style="cursor:pointer">Outpaint</label>
</div>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
html += `<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<span id="sbGenJobStatus" style="flex:1;font-size:11px;color:#888;display:none;min-width:120px">
<span class="sb-spinner"></span><span id="sbGenJobText">Generating…</span>
</span>
@@ -12628,13 +12674,14 @@
if (el) _sbPoseEdits[name] = el.value;
}
// Refetch poses from the backend, then re-render both the Generate tab and batch menu.
// Refetch poses from the backend, then re-render both the Generate tab, Design tab and batch menu.
async function _reloadPosesUI() {
try {
const r = await fetch(`${API}/poses`);
if (r.ok) availablePoses = await r.json();
} catch (e) { /* non-fatal */ }
renderSidebarGenerate();
renderSidebarDesigner();
if (document.getElementById('poseMenu')) renderPoseMenu();
}
@@ -12694,6 +12741,12 @@
});
if (r.ok) {
showToast('Pose added', 'success');
const nEl = document.getElementById('sbNewPoseName');
const tEl = document.getElementById('sbNewPoseText');
const bEl = document.getElementById('sbNewPoseBeta');
if (nEl) nEl.value = '';
if (tEl) tEl.value = '';
if (bEl) bEl.checked = false;
await _reloadPosesUI();
} else {
showToast('Add failed: ' + await r.text(), 'error');
@@ -12764,7 +12817,7 @@
}, 400);
}
async function submitSingleAngle(prompt) {
async function submitSingleAngle(prompt, angleName) {
if (!_fsModelFilename) return;
const bar = document.getElementById('studioAngleBar');
if (bar) bar.style.pointerEvents = 'none';
@@ -12774,7 +12827,7 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filenames: [_fsModelFilename], prompt: [prompt],
poses: [null], seed: -1, max_area: 0, group_id: lbCurrentGid,
poses: [angleName || null], seed: -1, max_area: 0, group_id: lbCurrentGid,
wireframe_ref: _sbWireframeRef || null,
wireframe_time: _sbWireframeTime,
}),
@@ -12811,7 +12864,8 @@
const overlay = CAMERA_ANGLES.slice(0, 6);
bar.innerHTML = overlay.map(a => {
const pSafe = a.prompt.replace(/'/g, "\\'");
return `<button onclick="submitSingleAngle('${pSafe}')" title="${escHtml(a.name)}">${a.icon}</button>`;
const nSafe = a.name.replace(/'/g, "\\'");
return `<button onclick="submitSingleAngle('${pSafe}', '${nSafe}')" title="${escHtml(a.name)}">${a.icon}</button>`;
}).join('') + `
<button onclick="startManualPad()" title="Pad canvas (Expand image)" style="color:#f59e0b">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin:auto;display:block">
@@ -12841,18 +12895,38 @@
<path d="M6 2v14a2 2 0 0 0 2 2h14M18 22V8a2 2 0 0 0-2-2H2"/>
<path d="M10 10l4 4m0-4l-4 4" stroke-width="1.5"/>
</svg>
</button>
<button id="overlayCheckerboardBtn" onclick="toggleCheckerboard(!document.getElementById('studioViewer').classList.contains('show-checker'))" title="Toggle Checkerboard (show transparency)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin:auto;display:block">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<path d="M9 3v18m6-18v18M3 9h18M3 15h18"/>
</svg>
</button>`;
// Synchronize the button's visual state on render
const isChecked = document.getElementById('studioViewer')?.classList.contains('show-checker');
const overlayBtn = document.getElementById('overlayCheckerboardBtn');
if (overlayBtn) {
overlayBtn.style.color = isChecked ? '#10b981' : '#f43f5e';
overlayBtn.style.background = isChecked ? 'rgba(16,185,129,0.1)' : '';
}
}
function updateSbGenBtn() {
const btn = document.getElementById('sbGenBtn');
if (!btn) return;
const hasPrompt = (document.getElementById('sbGenPromptInput')?.value || '').trim().length > 0;
let hasPaddingAction = false;
const padBar = document.getElementById('padBar');
if (padBar) {
const getV = (id) => document.getElementById(id)?.value || '0';
const padValStr = getV('sbPadTop') + getV('sbPadRight') + getV('sbPadBottom') + getV('sbPadLeft');
const padValStr = getV('padTop') + getV('padRight') + getV('padBottom') + getV('padLeft');
const hasPadding = padValStr.replace(/[0%\s]/g, '').length > 0;
const outpaint = document.getElementById('sbPadOutpaint')?.checked;
const hasPaddingAction = hasPadding && outpaint;
const outpaint = document.getElementById('padOutpaint')?.checked;
hasPaddingAction = hasPadding && outpaint;
}
const total = _fsSelectedPoses.size + _sbSelectedAngles.size + (hasPrompt ? 1 : 0) + (hasPaddingAction && !hasPrompt ? 1 : 0);
btn.disabled = total === 0;
btn.textContent = total > 1 ? `Generate (${total})` : 'Generate';
@@ -12986,20 +13060,29 @@
if (!_fsModelFilename) return;
const promptVal = (document.getElementById('sbGenPromptInput')?.value || '').trim();
const hasPoses = _fsSelectedPoses.size > 0 || _sbSelectedAngles.size > 0;
const getV = (id) => document.getElementById(id)?.value || '0';
const padValStr = getV('sbPadTop') + getV('sbPadRight') + getV('sbPadBottom') + getV('sbPadLeft');
const hasPadding = padValStr.replace(/[0%\s]/g, '').length > 0;
const outpaint = document.getElementById('sbPadOutpaint')?.checked;
const hasPaddingAction = hasPadding && outpaint;
if (!hasPoses && !hasPaddingAction) {
// If no poses or padding, we'll run base_prompts
const padBar = document.getElementById('padBar');
let hasPaddingAction = false;
if (padBar) {
const getV = (id) => document.getElementById(id)?.value || '0';
const padValStr = getV('padTop') + getV('padRight') + getV('padBottom') + getV('padLeft');
const hasPadding = padValStr.replace(/[0%\s]/g, '').length > 0;
const outpaint = document.getElementById('padOutpaint')?.checked;
hasPaddingAction = hasPadding && outpaint;
_sbPadTop = document.getElementById('padTop')?.value || '0';
_sbPadRight = document.getElementById('padRight')?.value || '0';
_sbPadBottom = document.getElementById('padBottom')?.value || '0';
_sbPadLeft = document.getElementById('padLeft')?.value || '0';
_sbPadOutpaint = outpaint || false;
} else {
_sbPadTop = '0';
_sbPadRight = '0';
_sbPadBottom = '0';
_sbPadLeft = '0';
_sbPadOutpaint = false;
}
_sbPadTop = document.getElementById('sbPadTop')?.value || '0';
_sbPadRight = document.getElementById('sbPadRight')?.value || '0';
_sbPadBottom = document.getElementById('sbPadBottom')?.value || '0';
_sbPadLeft = document.getElementById('sbPadLeft')?.value || '0';
_sbPadOutpaint = document.getElementById('sbPadOutpaint')?.checked || false;
const prompts = [], poses = [];
_fsSelectedPoses.forEach(name => {
poses.push(name);
@@ -13860,8 +13943,8 @@
const isSelected = _sceneRefs[0] && _sceneRefs[0].kind === 'wireframe' && _sceneRefs[0].name === frame;
const borderStyle = isSelected ? 'border:2px solid #8b5cf6' : 'border:1px solid #27272a';
html += `
<div style="cursor:pointer;position:relative" title="${escHtml(frame)}" onclick="sceneSelectWireframeImage('${fSafe}')">
<img src="${API}/wireframe/${encodeURIComponent(frame)}" loading="lazy" decoding="async" style="width:40px;height:40px;object-fit:cover;border-radius:4px;${borderStyle}" onerror="this.style.opacity=.3">
<div style="cursor:pointer;position:relative;width:40px;height:40px;flex-shrink:0" title="${escHtml(frame)}" onclick="sceneSelectWireframeImage('${fSafe}')">
<img src="${API}/wireframe/${encodeURIComponent(frame)}" width="40" height="40" loading="lazy" decoding="async" style="width:40px;height:40px;object-fit:cover;border-radius:4px;${borderStyle}" onerror="this.style.opacity=.3">
</div>`;
});
@@ -13897,8 +13980,8 @@
const isSelected = _sceneRefs[0] && _sceneRefs[0].kind === 'wireframe' && _sceneRefs[0].name === img;
const borderStyle = isSelected ? 'border:2px solid #2563eb' : 'border:1px solid #333';
html += `
<div style="cursor:pointer;position:relative" title="${escHtml(img)}" onclick="sceneSelectWireframeImage('${imgSafe}')">
<img src="${API}/wireframe/${encodeURIComponent(img)}" loading="lazy" decoding="async" style="width:48px;height:48px;object-fit:cover;border-radius:4px;${borderStyle}" onerror="this.style.opacity=.3">
<div style="cursor:pointer;position:relative;width:48px;height:48px;flex-shrink:0" title="${escHtml(img)}" onclick="sceneSelectWireframeImage('${imgSafe}')">
<img src="${API}/wireframe/${encodeURIComponent(img)}" width="48" height="48" loading="lazy" decoding="async" style="width:48px;height:48px;object-fit:cover;border-radius:4px;${borderStyle}" onerror="this.style.opacity=.3">
</div>`;
});
html += '</div>';
@@ -14006,10 +14089,10 @@
let groupHtml = `<div style="display:flex;flex-wrap:wrap;gap:4px">`;
groupHtml += visibleItems.map(it => {
const url = `${API}/output/` + encodeURIComponent(it.filename);
return `<div style="display:inline-block;cursor:pointer;position:relative;vertical-align:top"
return `<div style="display:inline-block;cursor:pointer;position:relative;vertical-align:top;width:48px;height:48px;flex-shrink:0"
title="Use as background · ${escHtml(it.filename)}${it.group_id?' ('+escHtml(it.group_id)+')':''}"
onclick="sceneLibUseAsBackground(${JSON.stringify(it.filename)})">
<img src="${url}" style="width:48px;height:48px;object-fit:cover;border-radius:4px;border:1px solid #27272a;background:#09090b"
<img src="${url}" width="48" height="48" style="width:48px;height:48px;object-fit:cover;border-radius:4px;border:1px solid #27272a;background:#09090b"
onerror="this.style.opacity=.3">
</div>`;
}).join('');
@@ -14468,15 +14551,11 @@
</div>
<div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap">
<button class="sb-btn" id="sbSam2Btn" onclick="sam2RemoveBg()" title="SAM2 segmentation — preserves hair/glasses/accessories; falls back to rembg if unavailable">Remove BG (SAM2)</button>
<button class="sb-btn" id="sbNoBgBtn" onclick="lbRemoveBg()" title="rembg BG removal, overwrites in-place">No BG</button>
<button class="sb-btn" id="sbRembgBtn" onclick="rembgRemoveBg()" title="rembg (U2Net) — fast background removal without SAM2">Remove BG (rembg)</button>
<button class="sb-btn" id="sbRestoreBgBtn" onclick="restoreBg()">Restore BG</button>
</div>
<div id="sbSegStatus" style="font-size:11px;color:#aaa;min-height:16px;margin-bottom:10px"></div>
<div class="sb-sep"></div>
<label style="display:flex;align-items:center;gap:8px;font-size:12px;cursor:pointer;margin-bottom:6px">
<input type="checkbox" id="sbCheckerboard" onchange="toggleCheckerboard(this.checked)" ${isChecked ? 'checked' : ''}>
<span>Checkerboard (show transparency)</span>
</label>
<div style="font-size:10px;color:#555;line-height:1.4">
SAM2 merges all non-border masks (preserves hair, glasses, sandals).<br>
rembg (U2Net) is faster and works without SAM2 checkpoint.<br>
@@ -14623,8 +14702,11 @@
}
}
const cb = document.getElementById('sbCheckerboard');
if (cb) cb.checked = on;
const overlayBtn = document.getElementById('overlayCheckerboardBtn');
if (overlayBtn) {
overlayBtn.style.color = on ? '#10b981' : '#f43f5e';
overlayBtn.style.background = on ? 'rgba(16,185,129,0.1)' : '';
}
}
// ---- set preferred ----
@@ -14846,6 +14928,19 @@
} else if (_designerResults !== null) {
html += `<div style="font-size:11px;color:#ff6b6b;padding:8px 0">No valid poses were returned by the AI. Try adjusting guidelines.</div>`;
}
// Add a brand-new pose (persisted to poses.md).
html += `<div class="sb-sep"></div>
<details style="margin-bottom:6px">
<summary style="font-size:11px;color:#888;cursor:pointer">+ New pose</summary>
<input type="text" id="sbNewPoseName" placeholder="Pose name"
style="width:100%;box-sizing:border-box;background:#111;border:1px solid #2a2a2a;border-radius:5px;color:#ccc;font-size:11px;padding:6px;margin:6px 0 4px">
<textarea id="sbNewPoseText" placeholder="Prompt text"
style="width:100%;box-sizing:border-box;background:#111;border:1px solid #2a2a2a;border-radius:5px;color:#ccc;font-size:11px;padding:6px;resize:vertical;min-height:48px"></textarea>
<label style="font-size:10px;color:#777;display:flex;align-items:center;gap:3px;cursor:pointer;margin-top:4px"><input type="checkbox" id="sbNewPoseBeta">beta</label>
<button class="sb-btn primary" style="margin-top:4px;font-size:10px;padding:3px 10px" onclick="addNewPose()">Add pose</button>
</details>`;
html += `</div>`;
panel.innerHTML = html;