Squash Odysseus development history

This commit is contained in:
pewdiepie-archdaemon
2026-09-11 06:04:19 +00:00
parent e5c99a5eee
commit 6ee6502010
2050 changed files with 538359 additions and 57745 deletions
+172 -36
View File
@@ -21,71 +21,99 @@
* saveState: (label?: string) => void,
* composite: () => void,
* fitZoom: () => void,
* renderLayerPanel: () => void,
* promptCanvasSize: (opts: object) => Promise<{w, h} | null>,
* doFillSelection: () => void,
* rotateAllLayers: (deg: number) => void,
* flipAllLayers: (axis: 'h' | 'v') => void,
* applyGaussianBlur: () => void,
* applyZoomBlur: () => void,
* addRetainedGaussianBlur: () => void,
* addRetainedEffect: (type: string) => void,
* uiModule: object,
* }} deps
*
* @returns {{
* applyResize: (newW: number, newH: number) => void,
* resizeCustomPrompt: () => Promise<void>,
* applyCanvasResize: (newW: number, newH: number) => void,
* applyImageResize: (newW: number, newH: number) => void,
* canvasSizePrompt: () => Promise<void>,
* imageSizePrompt: () => Promise<void>,
* }}
*/
import { state } from './state.js';
import { resizeCanvasDocument, resizeImageDocument } from './document-geometry.js';
export function wireTopbarMenus({
closeOtherTopbarMenus, registerDocClickAway,
saveState, composite, fitZoom,
saveState, composite, fitZoom, renderLayerPanel,
promptCanvasSize, doFillSelection,
rotateAllLayers, flipAllLayers,
applyGaussianBlur, applyZoomBlur,
applyGaussianBlur, applyZoomBlur, addRetainedGaussianBlur, addRetainedEffect,
selectAll, deselectSelection, reselectSelection, invertSelection,
transformSelection, refineSelection,
saveNamedSelection, loadNamedSelection, deleteNamedSelection,
uiModule,
}) {
// ── Resize canvas ──
// Extracted so both the popup presets and the Ctrl+Shift+T shortcut
// can call it.
function applyResize(newW, newH) {
function finishGeometryChange(newW, newH, message) {
const sizeLabel = document.getElementById('ge-canvas-size');
if (sizeLabel) sizeLabel.textContent = `${newW}×${newH}`;
fitZoom();
composite();
renderLayerPanel?.();
uiModule.showToast(message);
}
// Canvas Size changes document bounds without resampling layer pixels.
function applyCanvasResize(newW, newH, options = {}) {
if (!newW || !newH || newW < 1 || newH < 1) {
uiModule.showToast('Invalid size');
return;
}
saveState('Resize canvas');
// Only resize the main canvas — layers keep their original size.
// Content outside the new bounds is clipped during composite, not
// destroyed.
if (state.maskCanvas) {
const tmpMask = document.createElement('canvas');
tmpMask.width = state.maskCanvas.width;
tmpMask.height = state.maskCanvas.height;
tmpMask.getContext('2d').drawImage(state.maskCanvas, 0, 0);
state.maskCanvas.width = newW;
state.maskCanvas.height = newH;
state.maskCtx.drawImage(tmpMask, 0, 0);
}
state.imgWidth = newW;
state.imgHeight = newH;
state.mainCanvas.width = newW;
state.mainCanvas.height = newH;
const sizeLabel = document.getElementById('ge-canvas-size');
if (sizeLabel) sizeLabel.textContent = `${newW}×${newH}`;
fitZoom();
composite();
uiModule.showToast(`Canvas resized to ${newW}×${newH}`);
const result = resizeCanvasDocument(state, newW, newH, options);
finishGeometryChange(result.width, result.height, `Canvas resized to ${result.width}×${result.height}`);
}
async function resizeCustomPrompt() {
function applyImageResize(newW, newH, options = {}) {
if (!newW || !newH || newW < 1 || newH < 1) {
uiModule.showToast('Invalid size');
return;
}
saveState('Resize image');
const result = resizeImageDocument(state, newW, newH, {
smoothingQuality: options.interpolation || 'high',
});
finishGeometryChange(result.width, result.height, `Image resized to ${result.width}×${result.height}`);
}
async function canvasSizePrompt() {
const result = await promptCanvasSize({
title: 'Canvas size',
okLabel: 'Apply',
initialW: state.imgWidth,
initialH: state.imgHeight,
showUnits: true,
showInterpolation: true,
keepProportions: true,
showAnchor: true,
});
if (!result) return;
applyResize(result.w, result.h);
applyCanvasResize(result.w, result.h, result);
}
async function imageSizePrompt() {
const result = await promptCanvasSize({
title: 'Image size',
okLabel: 'Resample',
initialW: state.imgWidth,
initialH: state.imgHeight,
showUnits: true,
showInterpolation: true,
keepProportions: true,
showPresets: false,
});
if (!result) return;
applyImageResize(result.w, result.h, result);
}
// ── Image menu ──
@@ -104,7 +132,8 @@ export function wireTopbarMenus({
if (!item || item.disabled) return;
menu.hidden = true;
const action = item.dataset.imageAction;
if (action === 'resize') resizeCustomPrompt();
if (action === 'canvas-size') canvasSizePrompt();
else if (action === 'image-size') imageSizePrompt();
else if (action === 'selection') document.getElementById('ge-edge-menu-btn')?.click();
else if (action === 'fill') doFillSelection();
else if (action === 'rotate-90') rotateAllLayers(90);
@@ -118,6 +147,96 @@ export function wireTopbarMenus({
}
}
// ── Select menu ──
const renderSelectionMenu = () => {
const hasSelection = !!state.wandMask || (state.lassoPoints?.length >= 3 && !state.lassoActive);
const menu = document.getElementById('ge-selection-menu');
if (!menu) return;
menu.querySelector('[data-selection-action="deselect"]')?.toggleAttribute('disabled', !hasSelection);
menu.querySelector('[data-selection-action="invert"]')?.toggleAttribute('disabled', !hasSelection);
menu.querySelector('[data-selection-action="transform"]')?.toggleAttribute('disabled', !hasSelection);
menu.querySelector('[data-selection-action="refine"]')?.toggleAttribute('disabled', !hasSelection);
menu.querySelector('[data-selection-action="save"]')?.toggleAttribute('disabled', !hasSelection);
menu.querySelector('[data-selection-action="reselect"]')?.toggleAttribute('disabled', !state.lastSelection?.canvas);
const list = document.getElementById('ge-saved-selection-list');
if (!list) return;
list.innerHTML = '';
for (const saved of state.savedSelections || []) {
const row = document.createElement('div');
row.className = 'ge-saved-selection-row';
const load = document.createElement('button');
load.type = 'button';
load.className = 'ge-saved-selection-load';
load.dataset.selectionAction = 'load';
load.dataset.selectionId = saved.id;
load.textContent = saved.name;
load.title = `Load ${saved.name}`;
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'ge-saved-selection-delete';
remove.dataset.selectionAction = 'delete';
remove.dataset.selectionId = saved.id;
remove.textContent = '×';
remove.title = `Delete ${saved.name}`;
remove.setAttribute('aria-label', `Delete ${saved.name}`);
row.append(load, remove);
list.appendChild(row);
}
if (!list.children.length) {
const empty = document.createElement('div');
empty.className = 'ge-saved-selection-empty';
empty.textContent = 'None saved';
list.appendChild(empty);
}
};
{
const btn = document.getElementById('ge-selection-menu-btn');
const menu = document.getElementById('ge-selection-menu');
const nameInput = document.getElementById('ge-selection-name');
if (btn && menu) {
btn.addEventListener('click', event => {
event.stopPropagation();
const willOpen = menu.hidden;
if (willOpen) {
closeOtherTopbarMenus('ge-selection-menu');
renderSelectionMenu();
}
menu.hidden = !menu.hidden;
});
menu.addEventListener('click', event => {
const item = event.target.closest('[data-selection-action]');
if (!item || item.disabled) return;
const action = item.dataset.selectionAction;
let handled = false;
if (action === 'all') handled = selectAll?.();
else if (action === 'deselect') handled = deselectSelection?.();
else if (action === 'reselect') handled = reselectSelection?.();
else if (action === 'invert') handled = invertSelection?.();
else if (action === 'transform') handled = transformSelection?.();
else if (action === 'refine') handled = refineSelection?.();
else if (action === 'save') {
handled = saveNamedSelection?.(nameInput?.value);
if (handled && nameInput) nameInput.value = '';
} else if (action === 'load') handled = loadNamedSelection?.(item.dataset.selectionId);
else if (action === 'delete') handled = deleteNamedSelection?.(item.dataset.selectionId);
if (handled) renderSelectionMenu();
if (handled && !['save', 'delete'].includes(action)) menu.hidden = true;
});
nameInput?.addEventListener('keydown', event => {
if (event.key !== 'Enter') return;
event.preventDefault();
if (saveNamedSelection?.(nameInput.value)) {
nameInput.value = '';
renderSelectionMenu();
}
});
registerDocClickAway(event => {
if (!menu.hidden && !menu.contains(event.target) && event.target !== btn) menu.hidden = true;
});
}
}
// ── Filter menu (Blur sub-menu — Gaussian / Zoom) ──
{
const btn = document.getElementById('ge-filter-menu-btn');
@@ -134,7 +253,16 @@ export function wireTopbarMenus({
if (!item) return;
menu.hidden = true;
const action = item.dataset.filterAction;
if (action === 'blur-gaussian') applyGaussianBlur();
if (action === 'effect-blur-gaussian') addRetainedGaussianBlur?.();
else if (action === 'effect-sharpen') addRetainedEffect?.('sharpen');
else if (action === 'effect-color-overlay') addRetainedEffect?.('color-overlay');
else if (action === 'effect-drop-shadow') addRetainedEffect?.('drop-shadow');
else if (action === 'effect-stroke') addRetainedEffect?.('stroke');
else if (action === 'effect-preset-soft-blur') addRetainedGaussianBlur?.('soft-blur');
else if (action === 'effect-preset-crisp-detail') addRetainedEffect?.('sharpen', 'crisp-detail');
else if (action === 'effect-preset-soft-shadow') addRetainedEffect?.('drop-shadow', 'soft-shadow');
else if (action === 'effect-preset-white-outline') addRetainedEffect?.('stroke', 'white-outline');
else if (action === 'blur-gaussian') applyGaussianBlur();
else if (action === 'blur-zoom') applyZoomBlur();
});
registerDocClickAway((e) => {
@@ -157,12 +285,12 @@ export function wireTopbarMenus({
menu.querySelectorAll('[data-resize-w]').forEach(item => {
item.addEventListener('click', () => {
menu.hidden = true;
applyResize(parseInt(item.dataset.resizeW, 10), parseInt(item.dataset.resizeH, 10));
applyCanvasResize(parseInt(item.dataset.resizeW, 10), parseInt(item.dataset.resizeH, 10));
});
});
menu.querySelector('[data-resize-custom]')?.addEventListener('click', () => {
menu.hidden = true;
resizeCustomPrompt();
canvasSizePrompt();
});
registerDocClickAway((e) => {
if (!menu.hidden && !menu.contains(e.target) && e.target !== btn) menu.hidden = true;
@@ -170,5 +298,13 @@ export function wireTopbarMenus({
}
}
return { applyResize, resizeCustomPrompt };
return {
applyCanvasResize,
applyImageResize,
canvasSizePrompt,
imageSizePrompt,
renderSelectionMenu,
// Compatibility alias consumed by the existing Ctrl+Shift+T shortcut.
resizeCustomPrompt: canvasSizePrompt,
};
}