Files
odysseus/static/js/editor/history-budget.js
T
pewdiepie-archdaemon 2e8413a54a Preserve preview harness, editor, email and task improvements
Snapshot current maintainer-preview application changes and regression fixtures for integration into lab. Excludes local runtime data, evaluation outputs and source backups. Focused Python regression selection: 140 passed; full suite not certified.
2026-10-01 01:34:26 +00:00

88 lines
3.2 KiB
JavaScript

/** Adaptive bounds for raw ImageData history snapshots. */
export const MAX_HISTORY_ENTRIES = 30;
export const MAX_HISTORY_BYTES = 192 * 1024 * 1024;
function imageDataBytes(imageData) {
return Number(imageData?.data?.byteLength || imageData?.data?.length || 0);
}
function pixelSlots(snapshot) {
const slots = [];
const add = (object, key = 'imageData') => { if (object?.[key]) slots.push([object, key]); };
add(snapshot?.wand); add(snapshot?.lastSelection);
for (const item of snapshot?.savedSelections || []) add(item);
for (const group of snapshot?.layerGroups || []) {
for (const mask of group.masks || []) add(mask);
for (const effect of group.effects || []) add(effect.mask);
}
for (const layer of snapshot?.layers || []) {
add(layer); add(layer.placed, 'sourceImageData');
for (const mask of layer.masks || []) add(mask);
for (const effect of layer.effects || []) add(effect.mask);
}
return slots;
}
export function shareSnapshotPixels(snapshot, previous) {
if (!previous) return snapshot;
const candidates = pixelSlots(previous).map(([object, key]) => object[key]);
for (const [object, key] of pixelSlots(snapshot)) {
const image = object[key];
const match = candidates.find(other => {
if (image === other) return true;
if (image.width !== other.width || image.height !== other.height || image.data.length !== other.data.length) return false;
for (let i = 0; i < image.data.length; i++) if (image.data[i] !== other.data[i]) return false;
return true;
});
if (match) object[key] = match;
}
return snapshot;
}
export function historyByteSize(stack) {
const buffers = new Set();
let bytes = 0;
for (const snapshot of stack) {
const slots = pixelSlots(snapshot);
if (!slots.length) { bytes += snapshotByteSize(snapshot); continue; }
for (const [object, key] of slots) {
const image = object[key], buffer = image.data.buffer || image.data;
if (!buffers.has(buffer)) { buffers.add(buffer); bytes += buffer.byteLength || imageDataBytes(image); }
}
}
return bytes;
}
export function snapshotByteSize(snapshot) {
if (!snapshot) return 0;
if (Number.isFinite(snapshot._bytes)) return snapshot._bytes;
let bytes = imageDataBytes(snapshot.wand?.imageData);
bytes += imageDataBytes(snapshot.lastSelection?.imageData);
for (const selection of snapshot.savedSelections || []) bytes += imageDataBytes(selection.imageData);
for (const group of snapshot.layerGroups || []) {
for (const mask of group.masks || []) bytes += imageDataBytes(mask.imageData);
}
for (const layer of snapshot.layers || []) {
bytes += imageDataBytes(layer.imageData);
bytes += imageDataBytes(layer.placed?.sourceImageData);
for (const mask of layer.masks || []) bytes += imageDataBytes(mask.imageData);
}
return bytes;
}
export function trimHistoryStack(
stack,
maxEntries = MAX_HISTORY_ENTRIES,
maxBytes = MAX_HISTORY_BYTES,
) {
while (stack.length > maxEntries) stack.shift();
let bytes = historyByteSize(stack);
// Keep the newest state even when one snapshot alone exceeds the budget.
while (stack.length > 1 && bytes > maxBytes) {
stack.shift();
bytes = historyByteSize(stack);
}
return bytes;
}