117 lines
3.6 KiB
TypeScript
117 lines
3.6 KiB
TypeScript
// SimpleSyrup - workflow-focused ComfyUI extensions for image generation
|
|
// Copyright (C) 2026 Artificial Sweetener and contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import type { NativePreviewSlot } from "./orderedMediaPreviewAffordances";
|
|
|
|
export interface OrderedMediaPreviewItem {
|
|
readonly sourceUrl: string;
|
|
readonly image: HTMLImageElement;
|
|
}
|
|
|
|
export interface OrderedMediaPreviewSurface {
|
|
readonly items: OrderedMediaPreviewItem[];
|
|
readonly slots: Array<() => NativePreviewSlot>;
|
|
readonly apply: (items: OrderedMediaPreviewItem[]) => void;
|
|
readonly release: () => void;
|
|
}
|
|
|
|
export interface OrderedMediaPreviewTransactionOptions {
|
|
readonly captureSurface: () => OrderedMediaPreviewSurface | null;
|
|
readonly authoritativeReady: () => boolean;
|
|
readonly stateChanged: () => void;
|
|
}
|
|
|
|
/** Reorder Comfy's loaded preview media without creating another drawing layer. */
|
|
export class OrderedMediaPreviewTransaction {
|
|
private surface: OrderedMediaPreviewSurface | null = null;
|
|
private items: OrderedMediaPreviewItem[] | null = null;
|
|
private handoffFrame: number | null = null;
|
|
|
|
constructor(private readonly options: OrderedMediaPreviewTransactionOptions) {}
|
|
|
|
/** Move an already-loaded native item without rebuilding its gallery. */
|
|
move(index: number, destination: number): boolean {
|
|
if (!this.begin()) return false;
|
|
const items = this.items;
|
|
if (
|
|
!items ||
|
|
index < 0 ||
|
|
index >= items.length ||
|
|
destination < 0 ||
|
|
destination >= items.length ||
|
|
index === destination
|
|
) {
|
|
return false;
|
|
}
|
|
const [moved] = items.splice(index, 1);
|
|
if (!moved) return false;
|
|
items.splice(destination, 0, moved);
|
|
this.apply();
|
|
return true;
|
|
}
|
|
|
|
/** Remove an already-loaded item and let the native gallery compact itself. */
|
|
remove(index: number): boolean {
|
|
if (!this.begin()) return false;
|
|
const items = this.items;
|
|
if (!items || index < 0 || index >= items.length) return false;
|
|
items.splice(index, 1);
|
|
this.apply();
|
|
return true;
|
|
}
|
|
|
|
/** Return live native-cell geometry while an optimistic transaction is active. */
|
|
activeSlots(): NativePreviewSlot[] | null {
|
|
if (!this.surface || !this.items) return null;
|
|
return this.surface.slots.slice(0, this.items.length).map((slot) => slot());
|
|
}
|
|
|
|
/** End the transaction after Comfy's authoritative native preview is ready. */
|
|
authoritativePublished(): void {
|
|
if (!this.surface || this.handoffFrame !== null) return;
|
|
this.scheduleHandoff();
|
|
}
|
|
|
|
/** Release native surface changes and stop any pending handoff check. */
|
|
dispose(): void {
|
|
if (this.handoffFrame !== null) cancelAnimationFrame(this.handoffFrame);
|
|
this.handoffFrame = null;
|
|
this.finish();
|
|
}
|
|
|
|
private begin(): boolean {
|
|
if (this.surface && this.items) return true;
|
|
const surface = this.options.captureSurface();
|
|
if (!surface || surface.items.length === 0) return false;
|
|
this.surface = surface;
|
|
this.items = [...surface.items];
|
|
return true;
|
|
}
|
|
|
|
private apply(): void {
|
|
if (!this.surface || !this.items) return;
|
|
this.surface.apply(this.items);
|
|
this.options.stateChanged();
|
|
}
|
|
|
|
private scheduleHandoff(): void {
|
|
this.handoffFrame = requestAnimationFrame(() => {
|
|
this.handoffFrame = null;
|
|
if (!this.surface) return;
|
|
if (this.options.authoritativeReady()) {
|
|
this.finish();
|
|
this.options.stateChanged();
|
|
return;
|
|
}
|
|
this.scheduleHandoff();
|
|
});
|
|
}
|
|
|
|
private finish(): void {
|
|
this.surface?.release();
|
|
this.surface = null;
|
|
this.items = null;
|
|
}
|
|
}
|