Files
ComfyAssets-ComfyUI_PromptM…/web/js/workspace/image-layout.mjs
T
Vito Sansevero b8489d0369 feat(annotations): Tag image dialog beside Info on every image card
Every Images and Library-embedded gallery card shows the concept's Tag
after Info. Tag opens the concept's Tag image dialog (image-tagging.mjs
over the C1 controller) in the shared modal shell with the sticky
tagging-modal footer: capture preview, manual tags, the four legacy
engines with real defaults, explicit Generate, removable suggestion
chips (D83), legacy model labels (D84), a Suggestions count (D91) and
Retry where the controller allows it. The capture is released on close,
route change and pagehide.

The Tags page gains the concept's Auto-tag images for the image selected
in Images (aria-disabled without one, D115) and real Images counts that
follow every save. Retires D9, #245 D2 and #245 D3. The toast now uses
the mock's copy verbatim ("tags" even for one), C1's D60/D61/D68
markers are renumbered to D83/D84/D91, and at extreme zoom a card's info
row stacks (and wraps Info and Tag) when its row is narrower than the
narrowest bounded card, list rows included (D31).
2026-10-22 12:07:57 -07:00

72 lines
4.0 KiB
JavaScript

// The concept card (the concept stylesheet's `.media-card`): a 1px border around `aspect-ratio: 1.33` media and the measured
// `.media-info` row; a list row is the concept's 95px image plus that border.
const MEDIA_RATIO = 1.33, BORDER = 2, FOOTER_HEIGHT = 54, LIST_HEIGHT = 97;
// Bounded rendering keeps a card readable when zoom shrinks the grid below the concept's columns (D3, D31).
export const MINIMUM_CARD_WIDTH = 120;
const AUTOMATIC_CARD_WIDTH = 240;
// A list row's art: the concept's 160px, yielding to 45% of the row at extreme zoom (the overlay's D31).
const LIST_ART_WIDTH = 160, LIST_ART_SHARE = 0.45;
/**
* Whether a card's info row stacks its text above Info and Tag (D31): the row would be narrower than the narrowest
* bounded card, which only extreme zoom reaches. A list row's info is what its art leaves of the row.
*/
export function stacksCardInfo(width, mode) {
const info = mode === 'list' ? width - Math.min(LIST_ART_WIDTH, width * LIST_ART_SHARE) : width;
return info < MINIMUM_CARD_WIDTH;
}
/** The concept grid's own columns and gap, read from its computed style as the concept's mountMasonry does (D3). */
export function gridMetrics(grid) {
const style = getComputedStyle(grid);
const tracks = String(style.gridTemplateColumns || '').trim();
const gap = Number.parseFloat(style.columnGap);
return {columns: tracks && tracks !== 'none' ? tracks.split(/\s+/).length : 1, gap: Number.isFinite(gap) ? gap : 0};
}
export function layoutImages(images, width, mode, metrics = {}) {
const {gap = 13, footerHeight = FOOTER_HEIGHT, listHeight = LIST_HEIGHT} = metrics;
const fitting = Math.max(1, Math.floor((width + gap) / (MINIMUM_CARD_WIDTH + gap)));
const asked = metrics.columns ?? Math.max(1, Math.floor((width + gap) / (AUTOMATIC_CARD_WIDTH + gap)));
const columns = mode === 'list' ? 1 : Math.min(asked, fitting);
const cardWidth = Math.max(1, (width - gap * (columns - 1)) / columns);
const bottoms = Array(columns).fill(0);
let maxHeight = 0;
const positions = images.map((image, index) => {
const column = mode === 'masonry' ? bottoms.indexOf(Math.min(...bottoms)) : index % columns;
const ratio = Number.isFinite(image.width) && Number.isFinite(image.height) && image.width > 0 && image.height > 0 ? image.width / image.height : 4 / 3;
const mediaWidth = Math.max(1, cardWidth - BORDER);
const mediaHeight = mode === 'masonry' ? mediaWidth / ratio : mediaWidth / MEDIA_RATIO;
const height = mode === 'list' ? listHeight : (Number.isFinite(mediaHeight) ? mediaHeight : mediaWidth / MEDIA_RATIO) + footerHeight + BORDER;
maxHeight = Math.max(maxHeight, height);
const y = mode === 'grid' ? Math.floor(index / columns) * (height + gap) : bottoms[column];
bottoms[column] = y + height + gap;
return { x: column * (cardWidth + gap), y, width: cardWidth, height };
});
return { positions, columns, height: Math.max(0, ...bottoms) - (images.length ? gap : 0), maxHeight };
}
export function visibleImages(positions, top, viewportHeight, focusedIndex = -1, maxHeight = 1024) {
const buffer = Math.max(600, viewportHeight);
const firstTop = top - buffer - maxHeight;
const lastTop = top + viewportHeight + buffer;
let low = 0, high = positions.length;
// Every layout emits monotonically increasing tops, including shortest-column masonry.
while (low < high) {
const middle = Math.floor((low + high) / 2);
if (positions[middle].y < firstTop) low = middle + 1;
else high = middle;
}
const indexes = [];
for (let index = low; index < positions.length; index++) {
const position = positions[index];
if (position.y > lastTop) break;
if (index === focusedIndex || position.y + position.height >= top - buffer) indexes.push(index);
}
if (focusedIndex >= 0 && focusedIndex < positions.length && !indexes.includes(focusedIndex)) {
indexes.push(focusedIndex);
indexes.sort((a, b) => a - b);
}
return indexes;
}