mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-08 22:48:46 -04:00
Merge verified Odysseus fixes
This commit is contained in:
@@ -136,6 +136,10 @@ export function wireInpaintButtons({
|
||||
const dilatedMask = dilateMask(mergedMask, padPx);
|
||||
const imageB64 = flatCanvas.toDataURL('image/png').split(',')[1];
|
||||
const maskB64 = dilatedMask.toDataURL('image/png').split(',')[1];
|
||||
const baseSnap = document.createElement('canvas');
|
||||
baseSnap.width = state.imgWidth;
|
||||
baseSnap.height = state.imgHeight;
|
||||
baseSnap.getContext('2d').drawImage(flatCanvas, 0, 0);
|
||||
const res = await fetch('/api/image/inpaint', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -180,7 +184,7 @@ export function wireInpaintButtons({
|
||||
maskSnap.width = state.maskCanvas.width;
|
||||
maskSnap.height = state.maskCanvas.height;
|
||||
maskSnap.getContext('2d').drawImage(state.maskCanvas, 0, 0);
|
||||
resultLayer.inpaintSource = { ai: aiSnap, mask: maskSnap, padPx };
|
||||
resultLayer.inpaintSource = { ai: aiSnap, mask: maskSnap, base: baseSnap, padPx };
|
||||
// Apply initial alpha = hard mask (no feather, no edge shift).
|
||||
applyInpaintFeather(resultLayer, 0, 0);
|
||||
state.layers.push(resultLayer);
|
||||
@@ -216,7 +220,9 @@ export function wireInpaintButtons({
|
||||
const eRow = document.getElementById('ge-inpaint-edgestroke-row');
|
||||
const eSlider = document.getElementById('ge-edgestroke-slider');
|
||||
const eLabel = document.getElementById('ge-edgestroke-label');
|
||||
const autoRow = document.getElementById('ge-inpaint-automatch-row');
|
||||
if (eRow) eRow.style.display = '';
|
||||
if (autoRow) autoRow.style.display = '';
|
||||
if (eSlider) {
|
||||
eSlider.max = String(padPx);
|
||||
eSlider.min = String(-padPx);
|
||||
|
||||
Vendored
+35
@@ -102,6 +102,35 @@ export function controlsHTML({ color, brushSize, wandTolerance }) {
|
||||
</div>
|
||||
<p style="font-size:9px;opacity:0.4;margin:4px 0 0;">Click a region to select similar pixels. Shift+click to add, Alt+click to subtract. Esc to clear.</p>
|
||||
</div>
|
||||
<div class="ge-sam-section" id="ge-sam-section" style="display:none;">
|
||||
<div class="ge-section-title ge-section-title-with-help"><span>SAM</span><span class="ge-section-help" tabindex="0" role="img" aria-label="SAM selection help" title="Click an object for visual SAM selection, or type a neutral object label and use Find. The text is only used to locate a region before SAM creates the mask.">?</span></div>
|
||||
<div class="ge-control-row" style="display:flex;gap:4px;margin-bottom:4px;" title="How the next SAM selection combines with the current selection. Shift / Alt held during a click override this for one click.">
|
||||
<button type="button" class="ge-btn ge-btn-sm ge-wand-mode-btn active" data-wand-mode="replace" title="Replace selection">New</button>
|
||||
<button type="button" class="ge-btn ge-btn-sm ge-wand-mode-btn" data-wand-mode="add" title="Add to selection">+ Add</button>
|
||||
<button type="button" class="ge-btn ge-btn-sm ge-wand-mode-btn" data-wand-mode="subtract" title="Subtract from selection">− Subtract</button>
|
||||
</div>
|
||||
<div class="ge-control-row" style="display:flex;gap:6px;align-items:center;min-width:0;">
|
||||
<input type="text" class="ge-inpaint-prompt" id="ge-sam-query" placeholder="Object to select..." style="flex:1 1 auto;min-width:0;" />
|
||||
<button class="ge-btn ge-btn-sm ge-btn-ai" id="ge-sam-find" style="height:28px;display:inline-flex;align-items:center;gap:5px;" title="Find object and create a SAM mask">
|
||||
<span class="ge-btn-ai-mark" aria-hidden="true">✦</span>
|
||||
Find
|
||||
</button>
|
||||
</div>
|
||||
<div class="ge-control-row ge-actions" style="margin-top:4px;flex-wrap:wrap;">
|
||||
<button class="ge-btn ge-btn-sm ge-mask-vis-btn visible" id="ge-sam-vis" title="Hide selection overlay" aria-label="Toggle selection overlay">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</button>
|
||||
<button class="ge-btn ge-btn-sm ge-btn-iconlabel" id="ge-sam-clear" title="Clear the selection">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg>
|
||||
Clear
|
||||
</button>
|
||||
<button class="ge-btn ge-btn-sm ge-btn-iconlabel" id="ge-sam-mask" title="Add selection to the inpaint mask">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.06 11.9l8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08"/><path d="M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1.08 1.1 2.49 2.02 4 2.02 2.2 0 4-1.8 4-4.04a3.01 3.01 0 0 0-3-3.02z"/></svg>
|
||||
To Mask
|
||||
</button>
|
||||
</div>
|
||||
<p style="font-size:9px;opacity:0.4;margin:4px 0 0;">Click an object, or type a neutral object label. Shift adds, Alt subtracts.</p>
|
||||
</div>
|
||||
<div class="ge-inpaint-section" id="ge-inpaint-section" style="display:none;">
|
||||
<div class="ge-inpaint-popover-head" data-inpaint-drag>
|
||||
<div class="ge-section-title ge-section-title-with-help ge-inpaint-popover-title"><span>INPAINT</span><span class="ge-section-help" tabindex="0" role="img" aria-label="How inpaint works" title="Brush the area you want the AI to redraw — the red preview marks the mask region. Use Paint to add, Erase to subtract (or hold Ctrl+Alt to flip for one stroke). Generate fills with what your prompt describes; Remove fills with the surrounding background.">?</span></div>
|
||||
@@ -189,6 +218,12 @@ export function controlsHTML({ color, brushSize, wandTolerance }) {
|
||||
<label>Edge stroke <span id="ge-edgestroke-label">0px</span></label>
|
||||
<input type="range" id="ge-edgestroke-slider" min="-80" max="80" value="0" title="Expand (+) or contract (−) the inpaint layer's edge before feathering. Uses the AI buffer generated around your brush." />
|
||||
</div>
|
||||
<div class="ge-control-row ge-actions" id="ge-inpaint-automatch-row" style="display:none;margin-top:6px;">
|
||||
<button class="ge-btn ge-btn-sm ge-btn-iconlabel ge-btn-ai" id="ge-inpaint-automatch" style="width:100%;justify-content:center;" title="Match the latest inpaint result to the surrounding colour and lighting using an adjustment layer.">
|
||||
<span class="ge-btn-ai-mark" aria-hidden="true">✦</span>
|
||||
Auto match color
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ge-eraser-section" id="ge-clone-section" style="display:none;">
|
||||
<div class="ge-section-title ge-section-title-with-help"><span>Clone</span><span class="ge-section-help" tabindex="0" role="img" aria-label="How clone works" title="Alt-click (desktop) or double-tap (mobile) somewhere on the canvas to set the sample source. Then drag elsewhere to clone those pixels onto the active layer. The source point moves with your brush so the offset stays constant. Size / Opacity / Flow / Softness come from the Brush panel.">?</span></div>
|
||||
|
||||
@@ -27,6 +27,7 @@ export function buildToolbar({ currentTool, onSelectTool, onClearSelection }) {
|
||||
{ id: 'clone', label: 'Clone', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="9" r="3"/><path d="M9 12l-3 4h12l-3-4"/><path d="M4 20h16"/></svg>', key: 'K' },
|
||||
{ id: 'lasso', label: 'Lasso', icon: '⟡', key: 'L' },
|
||||
{ id: 'wand', label: 'Wand', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4V2"/><path d="M15 16v-2"/><path d="M8 9h2"/><path d="M20 9h2"/><path d="M17.8 11.8L19 13"/><path d="M15 9h0"/><path d="M17.8 6.2L19 5"/><path d="M3 21l9-9"/><path d="M12.2 6.2L11 5"/></svg>', key: 'W' },
|
||||
{ id: 'sam', label: 'SAM', ai: true, icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7c3-3 13-3 16 0"/><path d="M4 17c3 3 13 3 16 0"/><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3"/></svg>' },
|
||||
{ sep: true },
|
||||
{ id: 'inpaint', label: 'Inpaint', ai: true, icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.06 11.9l8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08"/><path d="M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1.08 1.1 2.49 2.02 4 2.02 2.2 0 4-1.8 4-4.04a3.01 3.01 0 0 0-3-3.02z"/></svg>', key: 'M' },
|
||||
{ id: 'rembg', ai: true, label: 'Bg Remove', icon: '✄' },
|
||||
@@ -54,8 +55,9 @@ export function buildToolbar({ currentTool, onSelectTool, onClearSelection }) {
|
||||
// Selection-clear badge — rendered only for tools that can hold a
|
||||
// selection (lasso, wand). Inpaint masks are first-class sub-layers
|
||||
// now so they get their own delete-X in the layer panel.
|
||||
const clearBadge = (t.id === 'lasso' || t.id === 'wand')
|
||||
? '<span class="ge-tool-clear" title="Clear selection" data-clear-tool="' + t.id + '">' +
|
||||
const clearTitle = t.id === 'sam' ? 'Open SAM prompt' : 'Clear selection';
|
||||
const clearBadge = (t.id === 'lasso' || t.id === 'wand' || t.id === 'sam')
|
||||
? '<span class="ge-tool-clear" title="' + clearTitle + '" data-clear-tool="' + t.id + '">' +
|
||||
'<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg>' +
|
||||
'</span>'
|
||||
: '';
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
* openFxPopup: (layer: object, anchor: HTMLElement) => void,
|
||||
* editAdjLayer: (layer: object, adj: object, anchor: HTMLElement) => void,
|
||||
* createLayer: (name: string, w: number, h: number) => object,
|
||||
* renderLayer?: (layer: object) => HTMLCanvasElement,
|
||||
* lassoToMask: () => void,
|
||||
* wandToMask: () => void,
|
||||
* getActiveMaskLayer: () => object | null,
|
||||
@@ -54,7 +55,7 @@ export function createLayerPanelRenderer(deps) {
|
||||
const {
|
||||
composite, saveState, showLayerThumb, hideLayerThumb,
|
||||
loadLayerAlphaAsSelection, openFxPopup, editAdjLayer,
|
||||
createLayer, lassoToMask, wandToMask, getActiveMaskLayer,
|
||||
createLayer, renderLayer, lassoToMask, wandToMask, getActiveMaskLayer,
|
||||
syncFxPanelToActiveLayerIfPresent,
|
||||
dragSortModule, uiModule,
|
||||
} = deps;
|
||||
@@ -336,7 +337,7 @@ export function createLayerPanelRenderer(deps) {
|
||||
mergeDownBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
saveState(`Merge "${layer.name}" down`);
|
||||
mergeLayerDownAtIndex(i);
|
||||
mergeLayerDownAtIndex(i, renderLayer);
|
||||
composite();
|
||||
render();
|
||||
uiModule.showToast('Layer merged down');
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
* @param {{
|
||||
* composite: () => void,
|
||||
* applyInpaintFeather: (layer: object, featherPx: number, edgeShiftPx: number) => void,
|
||||
* autoMatchInpaint: () => void,
|
||||
* syncToolClearIndicators: () => void,
|
||||
* attachColorPicker: (el: HTMLInputElement) => void,
|
||||
* uiModule: object,
|
||||
@@ -37,7 +38,7 @@ const EYE_OPEN_SM = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
const EYE_OFF_SM = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><line x1="8" y1="16" x2="16" y2="8"/><line x1="8" y1="8" x2="16" y2="16"/></svg>';
|
||||
|
||||
export function wireInpaintControls({
|
||||
composite, applyInpaintFeather, syncToolClearIndicators,
|
||||
composite, applyInpaintFeather, autoMatchInpaint, syncToolClearIndicators,
|
||||
attachColorPicker, uiModule,
|
||||
}) {
|
||||
// ── Feather + Strength preview swatches ──
|
||||
@@ -93,6 +94,9 @@ export function wireInpaintControls({
|
||||
document.getElementById('ge-strength-label').textContent = (e.target.value / 100).toFixed(2);
|
||||
syncStrengthPreview(parseInt(e.target.value, 10));
|
||||
});
|
||||
document.getElementById('ge-inpaint-automatch')?.addEventListener('click', () => {
|
||||
if (typeof autoMatchInpaint === 'function') autoMatchInpaint();
|
||||
});
|
||||
syncFeatherPreview(0);
|
||||
syncStrengthPreview(75);
|
||||
|
||||
|
||||
@@ -15,32 +15,60 @@
|
||||
* createLayer: (name, w, h) => object,
|
||||
* renderLayerPanel: () => void,
|
||||
* composite: () => void,
|
||||
* renderLayer?: (layer) => HTMLCanvasElement,
|
||||
* uiModule: object,
|
||||
* }} deps
|
||||
*/
|
||||
import { state } from './state.js';
|
||||
|
||||
export function mergeLayerDownAtIndex(idx) {
|
||||
function _renderSource(layer, renderLayer) {
|
||||
if (!layer) return null;
|
||||
try {
|
||||
return typeof renderLayer === 'function' ? (renderLayer(layer) || layer.canvas) : layer.canvas;
|
||||
} catch {
|
||||
return layer.canvas;
|
||||
}
|
||||
}
|
||||
|
||||
function _clearBakedAdjustments(layer) {
|
||||
if (!layer) return;
|
||||
layer.adjLayers = [];
|
||||
layer._adjFinal = null;
|
||||
layer._adjFinalKey = '';
|
||||
layer._adjCache = null;
|
||||
layer._adjCacheKey = '';
|
||||
}
|
||||
|
||||
export function mergeLayerDownAtIndex(idx, renderLayer = null) {
|
||||
if (idx < 1 || idx >= state.layers.length) return null;
|
||||
const upper = state.layers[idx];
|
||||
const lower = state.layers[idx - 1];
|
||||
const upperOff = state.layerOffsets.get(upper.id) || { x: 0, y: 0 };
|
||||
const lowerOff = state.layerOffsets.get(lower.id) || { x: 0, y: 0 };
|
||||
lower.ctx.save();
|
||||
lower.ctx.globalAlpha = upper.opacity;
|
||||
lower.ctx.drawImage(
|
||||
upper.canvas,
|
||||
upperOff.x - lowerOff.x,
|
||||
upperOff.y - lowerOff.y,
|
||||
);
|
||||
lower.ctx.restore();
|
||||
const lowerSource = _renderSource(lower, renderLayer);
|
||||
const upperSource = _renderSource(upper, renderLayer);
|
||||
const merged = document.createElement('canvas');
|
||||
merged.width = state.imgWidth;
|
||||
merged.height = state.imgHeight;
|
||||
const mctx = merged.getContext('2d');
|
||||
mctx.globalAlpha = lower.opacity;
|
||||
mctx.drawImage(lowerSource, lowerOff.x, lowerOff.y);
|
||||
mctx.globalAlpha = upper.opacity;
|
||||
mctx.drawImage(upperSource, upperOff.x, upperOff.y);
|
||||
mctx.globalAlpha = 1;
|
||||
lower.canvas = merged;
|
||||
lower.ctx = lower.canvas.getContext('2d');
|
||||
lower.opacity = 1;
|
||||
lower.visible = true;
|
||||
state.layerOffsets.set(lower.id, { x: 0, y: 0 });
|
||||
_clearBakedAdjustments(lower);
|
||||
state.layers.splice(idx, 1);
|
||||
state.layerOffsets.delete(upper.id);
|
||||
state.activeLayerId = lower.id;
|
||||
return lower;
|
||||
}
|
||||
|
||||
export function wireMergeButtons({ saveState, createLayer, renderLayerPanel, composite, uiModule }) {
|
||||
export function wireMergeButtons({ saveState, createLayer, renderLayerPanel, composite, renderLayer, uiModule }) {
|
||||
// Flatten Copy.
|
||||
document.getElementById('ge-flatten')?.addEventListener('click', () => {
|
||||
if (state.layers.length < 2) return;
|
||||
@@ -51,9 +79,10 @@ export function wireMergeButtons({ saveState, createLayer, renderLayerPanel, com
|
||||
if (!l.visible) continue;
|
||||
const off = state.layerOffsets.get(l.id) || { x: 0, y: 0 };
|
||||
ctx.globalAlpha = l.opacity;
|
||||
ctx.drawImage(l.canvas, off.x, off.y);
|
||||
ctx.drawImage(_renderSource(l, renderLayer), off.x, off.y);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
_clearBakedAdjustments(merged);
|
||||
state.layers.push(merged);
|
||||
state.activeLayerId = merged.id;
|
||||
renderLayerPanel();
|
||||
@@ -70,14 +99,23 @@ export function wireMergeButtons({ saveState, createLayer, renderLayerPanel, com
|
||||
}
|
||||
saveState('Merge all');
|
||||
const base = visibleLayers[0];
|
||||
const baseCtx = base.ctx;
|
||||
for (let i = 1; i < visibleLayers.length; i++) {
|
||||
const merged = document.createElement('canvas');
|
||||
merged.width = state.imgWidth;
|
||||
merged.height = state.imgHeight;
|
||||
const baseCtx = merged.getContext('2d');
|
||||
for (let i = 0; i < visibleLayers.length; i++) {
|
||||
const l = visibleLayers[i];
|
||||
const off = state.layerOffsets.get(l.id) || { x: 0, y: 0 };
|
||||
baseCtx.globalAlpha = l.opacity;
|
||||
baseCtx.drawImage(l.canvas, off.x, off.y);
|
||||
baseCtx.drawImage(_renderSource(l, renderLayer), off.x, off.y);
|
||||
baseCtx.globalAlpha = 1;
|
||||
}
|
||||
base.canvas = merged;
|
||||
base.ctx = base.canvas.getContext('2d');
|
||||
base.opacity = 1;
|
||||
base.visible = true;
|
||||
state.layerOffsets.set(base.id, { x: 0, y: 0 });
|
||||
_clearBakedAdjustments(base);
|
||||
// Free offset entries for the discarded layers; keep base.
|
||||
for (const l of state.layers) {
|
||||
if (l === base) continue;
|
||||
@@ -95,7 +133,7 @@ export function wireMergeButtons({ saveState, createLayer, renderLayerPanel, com
|
||||
const idx = state.layers.findIndex(l => l.id === state.activeLayerId);
|
||||
if (idx < 1) return; // can't merge the bottom layer
|
||||
saveState('Merge down');
|
||||
mergeLayerDownAtIndex(idx);
|
||||
mergeLayerDownAtIndex(idx, renderLayer);
|
||||
renderLayerPanel();
|
||||
composite();
|
||||
uiModule.showToast('Layer merged down');
|
||||
|
||||
@@ -71,10 +71,15 @@ export function wireTopbar(deps) {
|
||||
// original IDs so the standalone handlers below wire to them
|
||||
// unchanged.
|
||||
{
|
||||
const saveBtn = document.getElementById('ge-save-menu-btn');
|
||||
const saveMenu = document.getElementById('ge-save-menu');
|
||||
const editorRoot = document.getElementById('gallery-editor-container') || document;
|
||||
const saveBtn = editorRoot.querySelector('#ge-save-menu-btn');
|
||||
const saveWrap = saveBtn?.closest('.ge-save-wrap');
|
||||
const saveMenu = saveWrap?.querySelector('#ge-save-menu');
|
||||
if (saveBtn && saveMenu) {
|
||||
const saveTopbar = saveBtn.closest('.ge-topbar');
|
||||
document.querySelectorAll('body > #ge-save-menu').forEach((menu) => {
|
||||
if (menu !== saveMenu) menu.remove();
|
||||
});
|
||||
// Reparent the menu to <body>. Without this, the menu inherits
|
||||
// the gallery modal's containing block (the modal applies a
|
||||
// `transform: scale(...)` for its enter animation — and any
|
||||
@@ -105,7 +110,7 @@ export function wireTopbar(deps) {
|
||||
saveMenu.addEventListener('click', () => { setSaveMenuOpen(false); });
|
||||
window.addEventListener('resize', () => { if (!saveMenu.hidden) positionSaveMenu(); });
|
||||
registerDocClickAway((e) => {
|
||||
if (!saveMenu.hidden && !saveMenu.contains(e.target) && e.target !== saveBtn) {
|
||||
if (!saveMenu.hidden && !saveMenu.contains(e.target) && !saveBtn.contains(e.target)) {
|
||||
setSaveMenuOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user