updates UI
This commit is contained in:
@@ -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,19 +8561,13 @@
|
||||
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';
|
||||
} else if (fill === 'white') {
|
||||
overlay.style.background = 'rgba(255, 255, 255, 0.7)';
|
||||
overlay.style.borderColor = '#ccc';
|
||||
} else {
|
||||
overlay.style.background = 'rgba(245, 158, 11, 0.4)';
|
||||
overlay.style.borderColor = '#f59e0b';
|
||||
}
|
||||
if (fill === 'black') {
|
||||
overlay.style.background = 'rgba(0, 0, 0, 0.7)';
|
||||
overlay.style.borderColor = '#444';
|
||||
} else if (fill === 'white') {
|
||||
overlay.style.background = 'rgba(255, 255, 255, 0.7)';
|
||||
overlay.style.borderColor = '#ccc';
|
||||
} else {
|
||||
// Sidebar is always transparent background before outpaint
|
||||
overlay.style.background = 'rgba(245, 158, 11, 0.4)';
|
||||
overlay.style.borderColor = '#f59e0b';
|
||||
}
|
||||
@@ -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;
|
||||
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;
|
||||
|
||||
let hasPaddingAction = false;
|
||||
const padBar = document.getElementById('padBar');
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user