Refactor widgets/nodes
This commit is contained in:
@@ -8,7 +8,7 @@ import { get } from "svelte/store";
|
|||||||
import type ComfyGraphNode from "./nodes/ComfyGraphNode";
|
import type ComfyGraphNode from "./nodes/ComfyGraphNode";
|
||||||
import type IComfyInputSlot from "./IComfyInputSlot";
|
import type IComfyInputSlot from "./IComfyInputSlot";
|
||||||
import type { ComfyBackendNode } from "./nodes/ComfyBackendNode";
|
import type { ComfyBackendNode } from "./nodes/ComfyBackendNode";
|
||||||
import type { ComfyComboNode, ComfyWidgetNode } from "./nodes";
|
import type { ComfyComboNode, ComfyWidgetNode } from "./nodes/widgets";
|
||||||
|
|
||||||
type ComfyGraphEvents = {
|
type ComfyGraphEvents = {
|
||||||
configured: (graph: LGraph) => void
|
configured: (graph: LGraph) => void
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutputs } from "./components/ComfyApp";
|
import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutputs } from "./components/ComfyApp";
|
||||||
import type TypedEmitter from "typed-emitter";
|
import type TypedEmitter from "typed-emitter";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type { ComfyExecutionResult, ComfyImageLocation } from "./nodes/ComfyWidgetNodes";
|
import type { ComfyImageLocation } from "$lib/utils";
|
||||||
import type { SerializedLGraph, UUID } from "@litegraph-ts/core";
|
import type { SerializedLGraph, UUID } from "@litegraph-ts/core";
|
||||||
import type { SerializedLayoutState } from "./stores/layoutState";
|
import type { SerializedLayoutState } from "./stores/layoutState";
|
||||||
import type { ComfyNodeDef } from "./ComfyNodeDef";
|
import type { ComfyNodeDef } from "./ComfyNodeDef";
|
||||||
|
|||||||
@@ -23,13 +23,21 @@
|
|||||||
export let dragDisabled: boolean = false;
|
export let dragDisabled: boolean = false;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
|
|
||||||
let attrsChanged: Writable<boolean> | null = null;
|
let attrsChanged: Writable<number> | null = null;
|
||||||
let children: IDragItem[] | null = null;
|
let children: IDragItem[] | null = null;
|
||||||
const flipDurationMs = 100;
|
const flipDurationMs = 100;
|
||||||
|
|
||||||
$: if (container) {
|
$: if (container) {
|
||||||
children = $layoutState.allItems[container.id].children;
|
const entry = $layoutState.allItems[container.id]
|
||||||
attrsChanged = container.attrsChanged
|
if (entry) {
|
||||||
|
children = $layoutState.allItems[container.id].children;
|
||||||
|
attrsChanged = container.attrsChanged
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
container = null;
|
||||||
|
children = null;
|
||||||
|
attrsChanged = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
children = null;
|
children = null;
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import "@litegraph-ts/nodes-logic"
|
|||||||
import "@litegraph-ts/nodes-math"
|
import "@litegraph-ts/nodes-math"
|
||||||
import "@litegraph-ts/nodes-strings"
|
import "@litegraph-ts/nodes-strings"
|
||||||
import "$lib/nodes/index"
|
import "$lib/nodes/index"
|
||||||
|
import "$lib/nodes/widgets/index"
|
||||||
import * as nodes from "$lib/nodes/index"
|
import * as nodes from "$lib/nodes/index"
|
||||||
|
import * as widgets from "$lib/nodes/widgets/index"
|
||||||
|
|
||||||
import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas";
|
import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas";
|
||||||
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||||
import * as widgets from "$lib/widgets/index"
|
|
||||||
import queueState from "$lib/stores/queueState";
|
import queueState from "$lib/stores/queueState";
|
||||||
import { type SvelteComponentDev } from "svelte/internal";
|
import { type SvelteComponentDev } from "svelte/internal";
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
@@ -32,9 +33,10 @@ import { download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, wor
|
|||||||
import notify from "$lib/notify";
|
import notify from "$lib/notify";
|
||||||
import configState from "$lib/stores/configState";
|
import configState from "$lib/stores/configState";
|
||||||
import { blankGraph } from "$lib/defaultGraph";
|
import { blankGraph } from "$lib/defaultGraph";
|
||||||
import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
|
import type { ComfyExecutionResult } from "$lib/utils";
|
||||||
import ComfyPromptSerializer from "./ComfyPromptSerializer";
|
import ComfyPromptSerializer from "./ComfyPromptSerializer";
|
||||||
import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef";
|
import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef";
|
||||||
|
import { ComfyComboNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||||
|
|
||||||
@@ -77,7 +79,7 @@ export type Progress = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BackendComboNode = {
|
type BackendComboNode = {
|
||||||
comboNode: nodes.ComfyComboNode
|
comboNode: ComfyComboNode,
|
||||||
inputSlot: IComfyInputSlot,
|
inputSlot: IComfyInputSlot,
|
||||||
backendNode: ComfyBackendNode
|
backendNode: ComfyBackendNode
|
||||||
}
|
}
|
||||||
@@ -747,7 +749,7 @@ export default class ComfyApp {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const inputIndex of found) {
|
for (const inputIndex of found) {
|
||||||
const comboNode = backendNode.getInputNode(inputIndex) as nodes.ComfyComboNode
|
const comboNode = backendNode.getInputNode(inputIndex) as ComfyComboNode
|
||||||
const inputSlot = backendNode.inputs[inputIndex] as IComfyInputSlot;
|
const inputSlot = backendNode.inputs[inputIndex] as IComfyInputSlot;
|
||||||
const def = defs[backendNode.type];
|
const def = defs[backendNode.type];
|
||||||
|
|
||||||
@@ -762,14 +764,14 @@ export default class ComfyApp {
|
|||||||
console.debug("[refreshComboInNodes] found:", backendUpdatedCombos.length, backendUpdatedCombos)
|
console.debug("[refreshComboInNodes] found:", backendUpdatedCombos.length, backendUpdatedCombos)
|
||||||
|
|
||||||
// Mark combo nodes without backend configs as being loaded already.
|
// Mark combo nodes without backend configs as being loaded already.
|
||||||
for (const node of this.lGraph.iterateNodesOfClassRecursive(nodes.ComfyComboNode)) {
|
for (const node of this.lGraph.iterateNodesOfClassRecursive(ComfyComboNode)) {
|
||||||
if (backendUpdatedCombos[node.id] != null) {
|
if (backendUpdatedCombos[node.id] != null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This node isn't connected to a backend node, so it's configured
|
// This node isn't connected to a backend node, so it's configured
|
||||||
// by the frontend instead.
|
// by the frontend instead.
|
||||||
const comboNode = node as nodes.ComfyComboNode;
|
const comboNode = node as ComfyComboNode;
|
||||||
let values = comboNode.properties.values;
|
let values = comboNode.properties.values;
|
||||||
|
|
||||||
// Frontend nodes can declare defaultWidgets which creates a
|
// Frontend nodes can declare defaultWidgets which creates a
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
import { startDrag, stopDrag } from "$lib/utils"
|
import { startDrag, stopDrag } from "$lib/utils"
|
||||||
import Container from "./Container.svelte"
|
import Container from "./Container.svelte"
|
||||||
import { type Writable } from "svelte/store"
|
import { type Writable } from "svelte/store"
|
||||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
import { NodeMode } from "@litegraph-ts/core";
|
|
||||||
import { isHidden } from "$lib/widgets/utils";
|
import { isHidden } from "$lib/widgets/utils";
|
||||||
|
|
||||||
export let dragItem: IDragItem | null = null;
|
export let dragItem: IDragItem | null = null;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { Styles } from "@gradio/utils";
|
import type { Styles } from "@gradio/utils";
|
||||||
|
|
||||||
export let style: Styles = {};
|
export let style: Styles = {};
|
||||||
export let elem_id: string;
|
export let elem_id: string | null;
|
||||||
export let elem_classes: Array<string> = [];
|
export let elem_classes: Array<string> = [];
|
||||||
export let visible: boolean = true;
|
export let visible: boolean = true;
|
||||||
export let variant: "default" | "panel" | "compact" = "default";
|
export let variant: "default" | "panel" | "compact" = "default";
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import queueState from "$lib/stores/queueState";
|
|||||||
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core";
|
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
||||||
import type { ComfyWidgetNode, ComfyExecutionResult, ComfyImageLocation } from "./ComfyWidgetNodes";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
import type { NotifyOptions } from "$lib/notify";
|
import type { NotifyOptions } from "$lib/notify";
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
import { convertComfyOutputToGradio, type ComfyUploadImageAPIResponse } from "$lib/utils";
|
import { type ComfyExecutionResult, type ComfyImageLocation, convertComfyOutputToGradio, type ComfyUploadImageAPIResponse } from "$lib/utils";
|
||||||
|
|
||||||
export class ComfyQueueEvents extends ComfyGraphNode {
|
export class ComfyQueueEvents extends ComfyGraphNode {
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
|
import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
|
||||||
import ComfyGraphNode from "./ComfyGraphNode";
|
import ComfyGraphNode from "./ComfyGraphNode";
|
||||||
import ComfyWidgets from "$lib/widgets"
|
import ComfyWidgets from "$lib/widgets"
|
||||||
import type { ComfyWidgetNode, ComfyExecutionResult } from "./ComfyWidgetNodes";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
import { BuiltInSlotShape, BuiltInSlotType, type SerializedLGraphNode } from "@litegraph-ts/core";
|
import { BuiltInSlotShape, BuiltInSlotType, type SerializedLGraphNode } from "@litegraph-ts/core";
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
||||||
import { iterateNodeDefOutputs, type ComfyNodeDef, iterateNodeDefInputs } from "$lib/ComfyNodeDef";
|
import { iterateNodeDefOutputs, type ComfyNodeDef, iterateNodeDefInputs } from "$lib/ComfyNodeDef";
|
||||||
|
import type { ComfyExecutionResult } from "$lib/utils";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Base class for any node with configuration sent by the backend.
|
* Base class for any node with configuration sent by the backend.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
||||||
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
||||||
import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
|
|
||||||
import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
||||||
import type { SvelteComponentDev } from "svelte/internal";
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
import type { ComfyWidgetNode, ComfyExecutionResult, ComfyImageLocation } from "./ComfyWidgetNodes";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
|
import type { ComfyExecutionResult, ComfyImageLocation } from "$lib/utils"
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"
|
|||||||
import ComfyGraphNode, { type ComfyGraphNodeProperties, type DefaultWidgetLayout } from "./ComfyGraphNode";
|
import ComfyGraphNode, { type ComfyGraphNodeProperties, type DefaultWidgetLayout } from "./ComfyGraphNode";
|
||||||
import { clamp } from "$lib/utils";
|
import { clamp } from "$lib/utils";
|
||||||
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||||
import { ComfyComboNode } from "./ComfyWidgetNodes";
|
import { ComfyComboNode } from "./widgets";
|
||||||
|
|
||||||
export interface ComfyValueControlProperties extends ComfyGraphNodeProperties {
|
export interface ComfyValueControlProperties extends ComfyGraphNodeProperties {
|
||||||
value: any,
|
value: any,
|
||||||
|
|||||||
@@ -1,897 +0,0 @@
|
|||||||
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType, type PropertyLayout, type IComboWidget, NodeMode, type INumberWidget, type UUID } from "@litegraph-ts/core";
|
|
||||||
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
|
||||||
import type { SvelteComponentDev } from "svelte/internal";
|
|
||||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
|
||||||
import { writable, type Unsubscriber, type Writable, get } from "svelte/store";
|
|
||||||
import { clamp, convertComfyOutputToGradio, range, type ComfyUploadImageType, isComfyBoxImageMetadata, filenameToComfyBoxMetadata, type ComfyBoxImageMetadata, isComfyExecutionResult, executionResultToImageMetadata, parseWhateverIntoImageMetadata } from "$lib/utils"
|
|
||||||
import layoutState from "$lib/stores/layoutState";
|
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
|
||||||
import queueState from "$lib/stores/queueState";
|
|
||||||
|
|
||||||
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
|
||||||
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
|
|
||||||
import TextWidget from "$lib/widgets/TextWidget.svelte";
|
|
||||||
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
|
||||||
import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
|
|
||||||
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
|
|
||||||
import RadioWidget from "$lib/widgets/RadioWidget.svelte";
|
|
||||||
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
|
|
||||||
import ImageEditorWidget from "$lib/widgets/ImageEditorWidget.svelte";
|
|
||||||
import type { ComfyNodeID } from "$lib/api";
|
|
||||||
|
|
||||||
export type AutoConfigOptions = {
|
|
||||||
includeProperties?: Set<string> | null,
|
|
||||||
setDefaultValue?: boolean
|
|
||||||
setWidgetTitle?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* NOTE: If you want to add a new widget but it has the same input/output type
|
|
||||||
* as another one of the existing widgets, best to create a new "variant" of
|
|
||||||
* that widget instead.
|
|
||||||
*
|
|
||||||
* - Go to layoutState, look for `ALL_ATTRIBUTES,` insert or find a "variant"
|
|
||||||
* attribute and set `validNodeTypes` to the type of the litegraph node
|
|
||||||
* - Add a new entry in the `values` array, like "knob" or "dial" for ComfySliderWidget
|
|
||||||
* - Add an {#if widget.attrs.variant === <...>} statement in the existing Svelte component
|
|
||||||
*
|
|
||||||
* Also, BEWARE of calling setOutputData() and triggerSlot() on the same frame!
|
|
||||||
* You will have to either implement an internal delay on the event triggering
|
|
||||||
* or use an Event Delay node to ensure the output slot data can propagate to
|
|
||||||
* the rest of the graph first (see `delayChangedEvent` for details)
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface ComfyWidgetProperties extends ComfyGraphNodeProperties {
|
|
||||||
defaultValue: any
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* A node that is tied to a UI widget in the frontend. When the frontend's
|
|
||||||
* widget is changed, the value of the first output in the node is updated
|
|
||||||
* in the litegraph instance.
|
|
||||||
*/
|
|
||||||
export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|
||||||
abstract properties: ComfyWidgetProperties;
|
|
||||||
|
|
||||||
value: Writable<T>
|
|
||||||
propsChanged: Writable<number> = writable(0) // dummy to indicate if props changed
|
|
||||||
unsubscribe: Unsubscriber;
|
|
||||||
|
|
||||||
/** Svelte class for the frontend logic */
|
|
||||||
abstract svelteComponentType: typeof SvelteComponentDev
|
|
||||||
|
|
||||||
/** If false, user manually set min/max/step, and should not be autoinherited from connected input */
|
|
||||||
autoConfig: boolean = true;
|
|
||||||
|
|
||||||
copyFromInputLink: boolean = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true wait until next frame update to trigger the changed event.
|
|
||||||
* Reason is, if the event is triggered immediately then other stuff that wants to run
|
|
||||||
* their own onExecute on the output value won't have completed yet.
|
|
||||||
*/
|
|
||||||
delayChangedEvent: boolean = true;
|
|
||||||
|
|
||||||
private _aboutToChange: number = 0;
|
|
||||||
private _aboutToChangeValue: any = null;
|
|
||||||
private _noChangedEvent: boolean = false;
|
|
||||||
|
|
||||||
abstract defaultValue: T;
|
|
||||||
|
|
||||||
/** Names of properties to add as inputs */
|
|
||||||
// shownInputProperties: string[] = []
|
|
||||||
|
|
||||||
/** Names of properties to add as outputs */
|
|
||||||
private shownOutputProperties: Record<string, { type: string, index: number }> = {}
|
|
||||||
outputProperties: { name: string, type: string }[] = []
|
|
||||||
|
|
||||||
override isBackendNode = false;
|
|
||||||
override serialize_widgets = true;
|
|
||||||
|
|
||||||
storeActionName: string | null = "store";
|
|
||||||
|
|
||||||
// output slots
|
|
||||||
outputSlotName: string | null = "value";
|
|
||||||
changedEventName: string | null = "changed";
|
|
||||||
|
|
||||||
displayWidget: ITextWidget;
|
|
||||||
|
|
||||||
override size: Vector2 = [60, 40];
|
|
||||||
|
|
||||||
constructor(name: string, value: T) {
|
|
||||||
const color = LGraphCanvas.node_colors["blue"]
|
|
||||||
super(name)
|
|
||||||
this.value = writable(value)
|
|
||||||
this.color ||= color.color
|
|
||||||
this.bgColor ||= color.bgColor
|
|
||||||
this.displayWidget = this.addWidget<ITextWidget>(
|
|
||||||
"text",
|
|
||||||
"Value",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
this.displayWidget.disabled = true; // prevent editing
|
|
||||||
this.unsubscribe = this.value.subscribe(this.onValueUpdated.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
addPropertyAsOutput(propertyName: string, type: string) {
|
|
||||||
if (this.shownOutputProperties["@" + propertyName])
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!(propertyName in this.properties)) {
|
|
||||||
throw `No property named ${propertyName} found!`
|
|
||||||
}
|
|
||||||
|
|
||||||
this.shownOutputProperties["@" + propertyName] = { type, index: this.outputs.length }
|
|
||||||
this.addOutput("@" + propertyName, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
formatValue(value: any): string {
|
|
||||||
return Watch.toString(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override changeMode(modeTo: NodeMode): boolean {
|
|
||||||
const result = super.changeMode(modeTo);
|
|
||||||
this.notifyPropsChanged();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onValueUpdated(value: any) {
|
|
||||||
// console.debug("[Widget] valueUpdated", this, value)
|
|
||||||
this.displayWidget.value = this.formatValue(value)
|
|
||||||
|
|
||||||
if (this.outputSlotName !== null) {
|
|
||||||
const outputIndex = this.findOutputSlotIndexByName(this.outputSlotName)
|
|
||||||
if (outputIndex !== -1)
|
|
||||||
this.setOutputData(outputIndex, get(this.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.changedEventName !== null && !this._noChangedEvent) {
|
|
||||||
if (!this.delayChangedEvent)
|
|
||||||
this.triggerChangeEvent(get(this.value))
|
|
||||||
else {
|
|
||||||
// console.debug("[Widget] queueChangeEvent", this, value)
|
|
||||||
this._aboutToChange = 2; // wait 1.5-2 frames, in case we're already in the middle of executing the graph
|
|
||||||
this._aboutToChangeValue = get(this.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._noChangedEvent = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private triggerChangeEvent(value: any) {
|
|
||||||
// console.debug("[Widget] trigger changed", this, value)
|
|
||||||
this.trigger(this.changedEventName, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
parseValue(value: any): T { return value as T };
|
|
||||||
|
|
||||||
getValue(): T {
|
|
||||||
return get(this.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue(value: any, noChangedEvent: boolean = false) {
|
|
||||||
if (noChangedEvent)
|
|
||||||
this._noChangedEvent = true;
|
|
||||||
|
|
||||||
const parsed = this.parseValue(value)
|
|
||||||
this.value.set(parsed)
|
|
||||||
|
|
||||||
// In case value.set() does not trigger onValueUpdated, we need to reset
|
|
||||||
// the counter here also.
|
|
||||||
this._noChangedEvent = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
|
||||||
if (this.shownOutputProperties != null) {
|
|
||||||
const data = this.shownOutputProperties[property]
|
|
||||||
if (data)
|
|
||||||
this.setOutputData(data.index, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Logic to run if this widget can be treated as output (slider, combo, text)
|
|
||||||
*/
|
|
||||||
override onExecute(param: any, options: object) {
|
|
||||||
if (this.outputSlotName != null) {
|
|
||||||
const outputIndex = this.findOutputSlotIndexByName(this.outputSlotName)
|
|
||||||
if (outputIndex !== -1)
|
|
||||||
this.setOutputData(outputIndex, get(this.value))
|
|
||||||
}
|
|
||||||
for (const propName in this.shownOutputProperties) {
|
|
||||||
const data = this.shownOutputProperties[propName]
|
|
||||||
this.setOutputData(data.index, this.properties[propName])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire a pending change event after one full step of the graph has
|
|
||||||
// finished processing
|
|
||||||
if (this._aboutToChange > 0) {
|
|
||||||
this._aboutToChange -= 1
|
|
||||||
if (this._aboutToChange <= 0) {
|
|
||||||
const value = this._aboutToChangeValue;
|
|
||||||
this._aboutToChange = 0;
|
|
||||||
this._aboutToChangeValue = null;
|
|
||||||
this.triggerChangeEvent(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override onAction(action: any, param: any, options: { action_call?: string }) {
|
|
||||||
if (action === this.storeActionName) {
|
|
||||||
let noChangedEvent = false;
|
|
||||||
let value = param;
|
|
||||||
if (param != null && typeof param === "object" && "value" in param) {
|
|
||||||
value = param.value
|
|
||||||
if ("noChangedEvent" in param)
|
|
||||||
noChangedEvent = Boolean(param.noChangedEvent)
|
|
||||||
}
|
|
||||||
this.setValue(value, noChangedEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onConnectOutput(
|
|
||||||
outputIndex: number,
|
|
||||||
inputType: INodeInputSlot["type"],
|
|
||||||
input: INodeInputSlot,
|
|
||||||
inputNode: LGraphNode,
|
|
||||||
inputIndex: number
|
|
||||||
): boolean {
|
|
||||||
const anyConnected = range(this.outputs.length).some(i => this.getOutputLinks(i).length > 0);
|
|
||||||
|
|
||||||
if (this.autoConfig && "config" in input && !anyConnected && (input as IComfyInputSlot).widgetNodeType === this.type) {
|
|
||||||
this.doAutoConfig(input as IComfyInputSlot)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
doAutoConfig(input: IComfyInputSlot, options: AutoConfigOptions = { setDefaultValue: true, setWidgetTitle: true }) {
|
|
||||||
// Copy properties from default config in input slot
|
|
||||||
const comfyInput = input as IComfyInputSlot;
|
|
||||||
for (const key in comfyInput.config) {
|
|
||||||
if (options.includeProperties == null || options.includeProperties.has(key))
|
|
||||||
this.setProperty(key, comfyInput.config[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.setDefaultValue) {
|
|
||||||
if ("defaultValue" in this.properties)
|
|
||||||
this.setValue(this.properties.defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.setWidgetTitle) {
|
|
||||||
const widget = layoutState.findLayoutForNode(this.id as ComfyNodeID)
|
|
||||||
if (widget && input.name !== "") {
|
|
||||||
widget.attrs.title = input.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.debug("Property copy", input, this.properties)
|
|
||||||
|
|
||||||
this.setValue(get(this.value))
|
|
||||||
|
|
||||||
this.onAutoConfig(input);
|
|
||||||
|
|
||||||
this.notifyPropsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
onAutoConfig(input: IComfyInputSlot) {
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyPropsChanged() {
|
|
||||||
const layoutEntry = layoutState.findLayoutEntryForNode(this.id as ComfyNodeID)
|
|
||||||
if (layoutEntry && layoutEntry.parent) {
|
|
||||||
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
|
|
||||||
}
|
|
||||||
// console.debug("propsChanged", this)
|
|
||||||
this.propsChanged.set(get(this.propsChanged) + 1)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override onConnectionsChange(
|
|
||||||
type: LConnectionKind,
|
|
||||||
slotIndex: number,
|
|
||||||
isConnected: boolean,
|
|
||||||
link: LLink,
|
|
||||||
ioSlot: (INodeOutputSlot | INodeInputSlot)
|
|
||||||
): void {
|
|
||||||
super.onConnectionsChange(type, slotIndex, isConnected, link, ioSlot);
|
|
||||||
this.clampConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
clampConfig() {
|
|
||||||
let changed = false;
|
|
||||||
for (const link of this.getOutputLinks(0)) {
|
|
||||||
if (link) { // can be undefined if the link is removed
|
|
||||||
const node = this.graph._nodes_by_id[link.target_id]
|
|
||||||
if (node) {
|
|
||||||
const input = node.inputs[link.target_slot]
|
|
||||||
if (input && "config" in input) {
|
|
||||||
this.clampOneConfig(input as IComfyInputSlot)
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force reactivity change so the frontend can be updated with the new props
|
|
||||||
this.notifyPropsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
clampOneConfig(input: IComfyInputSlot) { }
|
|
||||||
|
|
||||||
override onSerialize(o: SerializedLGraphNode) {
|
|
||||||
(o as any).comfyValue = get(this.value);
|
|
||||||
(o as any).shownOutputProperties = this.shownOutputProperties
|
|
||||||
super.onSerialize(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
override onConfigure(o: SerializedLGraphNode) {
|
|
||||||
const value = (o as any).comfyValue || LiteGraph.cloneObject(this.defaultValue);
|
|
||||||
this.value.set(value);
|
|
||||||
this.shownOutputProperties = (o as any).shownOutputProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
override stripUserState(o: SerializedLGraphNode) {
|
|
||||||
super.stripUserState(o);
|
|
||||||
(o as any).comfyValue = this.defaultValue;
|
|
||||||
o.properties.defaultValue = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComfySliderProperties extends ComfyWidgetProperties {
|
|
||||||
min: number,
|
|
||||||
max: number,
|
|
||||||
step: number,
|
|
||||||
precision: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfySliderNode extends ComfyWidgetNode<number> {
|
|
||||||
override properties: ComfySliderProperties = {
|
|
||||||
tags: [],
|
|
||||||
defaultValue: 0,
|
|
||||||
min: 0,
|
|
||||||
max: 10,
|
|
||||||
step: 1,
|
|
||||||
precision: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = RangeWidget
|
|
||||||
override defaultValue = 0;
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "value", type: "number" },
|
|
||||||
{ name: "changed", type: BuiltInSlotType.EVENT },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override outputProperties = [
|
|
||||||
{ name: "min", type: "number" },
|
|
||||||
{ name: "max", type: "number" },
|
|
||||||
{ name: "step", type: "number" },
|
|
||||||
{ name: "precision", type: "number" },
|
|
||||||
]
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(value: any): number {
|
|
||||||
if (typeof value !== "number")
|
|
||||||
return this.properties.min;
|
|
||||||
return clamp(value, this.properties.min, this.properties.max)
|
|
||||||
}
|
|
||||||
|
|
||||||
override clampOneConfig(input: IComfyInputSlot) {
|
|
||||||
// this.setProperty("min", clamp(this.properties.min, input.config.min, input.config.max))
|
|
||||||
// this.setProperty("max", clamp(this.properties.max, input.config.max, input.config.min))
|
|
||||||
// this.setProperty("step", Math.min(this.properties.step, input.config.step))
|
|
||||||
this.setValue(this.properties.defaultValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfySliderNode,
|
|
||||||
title: "UI.Slider",
|
|
||||||
desc: "Slider outputting a number value",
|
|
||||||
type: "ui/slider"
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfyComboProperties extends ComfyWidgetProperties {
|
|
||||||
values: string[]
|
|
||||||
|
|
||||||
/* JS Function body that takes a parameter named "value" as a parameter and returns the label for each combo entry */
|
|
||||||
convertValueToLabelCode: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyComboNode extends ComfyWidgetNode<string> {
|
|
||||||
override properties: ComfyComboProperties = {
|
|
||||||
tags: [],
|
|
||||||
defaultValue: "A",
|
|
||||||
values: ["A", "B", "C", "D"],
|
|
||||||
convertValueToLabelCode: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "value", type: "string" },
|
|
||||||
{ name: "changed", type: BuiltInSlotType.EVENT }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = ComboWidget
|
|
||||||
override defaultValue = "A";
|
|
||||||
override saveUserState = false;
|
|
||||||
|
|
||||||
// True if at least one combo box refresh has taken place
|
|
||||||
// Wait until the initial graph load for combo to be valid.
|
|
||||||
firstLoad: Writable<boolean>;
|
|
||||||
valuesForCombo: Writable<any[] | null>; // Changed when the combo box has values.
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, "A")
|
|
||||||
this.firstLoad = writable(false)
|
|
||||||
this.valuesForCombo = writable(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override onPropertyChanged(property: any, value: any) {
|
|
||||||
if (property === "values" || property === "convertValueToLabelCode") {
|
|
||||||
// this.formatValues(this.properties.values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formatValues(values: string[]) {
|
|
||||||
if (values == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.properties.values = values;
|
|
||||||
|
|
||||||
let formatter: any;
|
|
||||||
if (this.properties.convertValueToLabelCode)
|
|
||||||
formatter = new Function("value", this.properties.convertValueToLabelCode) as (v: string) => string;
|
|
||||||
else
|
|
||||||
formatter = (value: any) => `${value}`;
|
|
||||||
|
|
||||||
let valuesForCombo = []
|
|
||||||
|
|
||||||
try {
|
|
||||||
valuesForCombo = this.properties.values.map((value, index) => {
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
label: formatter(value),
|
|
||||||
index
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.error("Failed formatting!", err)
|
|
||||||
valuesForCombo = this.properties.values.map((value, index) => {
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
label: `${value}`,
|
|
||||||
index
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.firstLoad.set(true)
|
|
||||||
this.valuesForCombo.set(valuesForCombo);
|
|
||||||
}
|
|
||||||
|
|
||||||
onConnectOutput(
|
|
||||||
outputIndex: number,
|
|
||||||
inputType: INodeInputSlot["type"],
|
|
||||||
input: INodeInputSlot,
|
|
||||||
inputNode: LGraphNode,
|
|
||||||
inputIndex: number
|
|
||||||
): boolean {
|
|
||||||
if (!super.onConnectOutput(outputIndex, inputType, input, inputNode, inputIndex))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const thisProps = this.properties;
|
|
||||||
if (!("config" in input))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
const comfyInput = input as IComfyInputSlot;
|
|
||||||
const otherProps = comfyInput.config;
|
|
||||||
|
|
||||||
// Ensure combo options match
|
|
||||||
if (!(otherProps.values instanceof Array))
|
|
||||||
return false;
|
|
||||||
if (thisProps.values.find((v, i) => otherProps.values.indexOf(v) === -1))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(value: any): string {
|
|
||||||
if (typeof value !== "string" || this.properties.values.indexOf(value) === -1)
|
|
||||||
return this.properties.values[0]
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
override clampOneConfig(input: IComfyInputSlot) {
|
|
||||||
if (!input.config.values)
|
|
||||||
this.setValue("")
|
|
||||||
else if (input.config.values.indexOf(this.properties.value) === -1) {
|
|
||||||
if (input.config.values.length === 0)
|
|
||||||
this.setValue("")
|
|
||||||
else
|
|
||||||
this.setValue(input.config.defaultValue || input.config.values[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override onSerialize(o: SerializedLGraphNode) {
|
|
||||||
super.onSerialize(o);
|
|
||||||
// TODO fix saving combo nodes with huge values lists
|
|
||||||
o.properties.values = []
|
|
||||||
}
|
|
||||||
|
|
||||||
override stripUserState(o: SerializedLGraphNode) {
|
|
||||||
super.stripUserState(o);
|
|
||||||
o.properties.values = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyComboNode,
|
|
||||||
title: "UI.Combo",
|
|
||||||
desc: "Combo box outputting a string value",
|
|
||||||
type: "ui/combo"
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfyTextProperties extends ComfyWidgetProperties {
|
|
||||||
multiline: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyTextNode extends ComfyWidgetNode<string> {
|
|
||||||
override properties: ComfyTextProperties = {
|
|
||||||
tags: [],
|
|
||||||
defaultValue: "",
|
|
||||||
multiline: false
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "value", type: "string" },
|
|
||||||
{ name: "changed", type: BuiltInSlotType.EVENT }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = TextWidget
|
|
||||||
override defaultValue = "";
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(value: any): string {
|
|
||||||
return `${value}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyTextNode,
|
|
||||||
title: "UI.Text",
|
|
||||||
desc: "Textbox outputting a string value",
|
|
||||||
type: "ui/text"
|
|
||||||
})
|
|
||||||
|
|
||||||
/** Raw output as received from ComfyUI's backend */
|
|
||||||
export interface ComfyExecutionResult {
|
|
||||||
// Technically this response can contain arbitrary data, but "images" is the
|
|
||||||
// most frequently used as it's output by LoadImage and PreviewImage, the
|
|
||||||
// only two output nodes in base ComfyUI.
|
|
||||||
images: ComfyImageLocation[] | null,
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Raw output entry as received from ComfyUI's backend */
|
|
||||||
export type ComfyImageLocation = {
|
|
||||||
/* Filename with extension in the subfolder. */
|
|
||||||
filename: string,
|
|
||||||
/* Subfolder in the containing folder. */
|
|
||||||
subfolder: string,
|
|
||||||
/* Base ComfyUI folder where the image is located. */
|
|
||||||
type: ComfyUploadImageType
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
|
||||||
index: number,
|
|
||||||
updateMode: "replace" | "append",
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
|
|
||||||
override properties: ComfyGalleryProperties = {
|
|
||||||
tags: [],
|
|
||||||
defaultValue: [],
|
|
||||||
index: 0,
|
|
||||||
updateMode: "replace",
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "images", type: "OUTPUT" },
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "images", type: "COMFYBOX_IMAGES" },
|
|
||||||
{ name: "selected_index", type: "number" },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
static propertyLayout: PropertyLayout = [
|
|
||||||
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
|
|
||||||
]
|
|
||||||
|
|
||||||
override svelteComponentType = GalleryWidget
|
|
||||||
override defaultValue = []
|
|
||||||
override saveUserState = false;
|
|
||||||
override outputSlotName = null;
|
|
||||||
override changedEventName = null;
|
|
||||||
|
|
||||||
selectedFilename: string | null = null;
|
|
||||||
|
|
||||||
selectedIndexWidget: ITextWidget;
|
|
||||||
modeWidget: IComboWidget;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, [])
|
|
||||||
this.selectedIndexWidget = this.addWidget("text", "Selected", String(this.properties.index), "index")
|
|
||||||
this.selectedIndexWidget.disabled = true;
|
|
||||||
this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] })
|
|
||||||
}
|
|
||||||
|
|
||||||
override onPropertyChanged(property: any, value: any) {
|
|
||||||
if (property === "updateMode") {
|
|
||||||
this.modeWidget.value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override onExecute() {
|
|
||||||
this.setOutputData(0, get(this.value))
|
|
||||||
this.setOutputData(1, this.properties.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override onAction(action: any, param: any, options: { action_call?: string }) {
|
|
||||||
super.onAction(action, param, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
override formatValue(value: ComfyBoxImageMetadata[] | null): string {
|
|
||||||
return `Images: ${value?.length || 0}`
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(param: any): ComfyBoxImageMetadata[] {
|
|
||||||
const meta = parseWhateverIntoImageMetadata(param) || [];
|
|
||||||
|
|
||||||
console.debug("[ComfyGalleryNode] Received output!", param)
|
|
||||||
|
|
||||||
if (this.properties.updateMode === "append") {
|
|
||||||
const currentValue = get(this.value)
|
|
||||||
return currentValue.concat(meta)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return meta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override setValue(value: any, noChangedEvent: boolean = false) {
|
|
||||||
super.setValue(value, noChangedEvent)
|
|
||||||
this.setProperty("index", null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyGalleryNode,
|
|
||||||
title: "UI.Gallery",
|
|
||||||
desc: "Gallery that shows most recent outputs",
|
|
||||||
type: "ui/gallery"
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfyButtonProperties extends ComfyWidgetProperties {
|
|
||||||
param: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
|
|
||||||
override properties: ComfyButtonProperties = {
|
|
||||||
tags: [],
|
|
||||||
defaultValue: false,
|
|
||||||
param: "bang"
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
outputs: [
|
|
||||||
{ name: "clicked", type: BuiltInSlotType.EVENT },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = ButtonWidget;
|
|
||||||
override defaultValue = false;
|
|
||||||
override outputSlotName = null;
|
|
||||||
override changedEventName = null;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(param: any): boolean {
|
|
||||||
return Boolean(param);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClick() {
|
|
||||||
this.setValue(true)
|
|
||||||
this.triggerSlot(0, this.properties.param);
|
|
||||||
this.setValue(false) // TODO onRelease
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyButtonNode,
|
|
||||||
title: "UI.Button",
|
|
||||||
desc: "Button that triggers an event when clicked",
|
|
||||||
type: "ui/button"
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfyCheckboxProperties extends ComfyWidgetProperties {
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
|
|
||||||
override properties: ComfyCheckboxProperties = {
|
|
||||||
tags: [],
|
|
||||||
defaultValue: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "value", type: "boolean" },
|
|
||||||
{ name: "changed", type: BuiltInSlotType.EVENT },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = CheckboxWidget;
|
|
||||||
override defaultValue = false;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(param: any) {
|
|
||||||
return Boolean(param);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyCheckboxNode,
|
|
||||||
title: "UI.Checkbox",
|
|
||||||
desc: "Checkbox that stores a boolean value",
|
|
||||||
type: "ui/checkbox"
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfyRadioProperties extends ComfyWidgetProperties {
|
|
||||||
choices: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyRadioNode extends ComfyWidgetNode<string> {
|
|
||||||
override properties: ComfyRadioProperties = {
|
|
||||||
tags: [],
|
|
||||||
choices: ["Choice A", "Choice B", "Choice C"],
|
|
||||||
defaultValue: "Choice A",
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "value", type: "string" },
|
|
||||||
{ name: "index", type: "number" },
|
|
||||||
{ name: "changed", type: BuiltInSlotType.EVENT },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = RadioWidget;
|
|
||||||
override defaultValue = "";
|
|
||||||
|
|
||||||
indexWidget: INumberWidget;
|
|
||||||
|
|
||||||
index = 0;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, "Choice A")
|
|
||||||
this.indexWidget = this.addWidget("number", "Index", this.index)
|
|
||||||
this.indexWidget.disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
override onExecute(param: any, options: object) {
|
|
||||||
super.onExecute(param, options);
|
|
||||||
this.setOutputData(1, this.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override setValue(value: string, noChangedEvent: boolean = false) {
|
|
||||||
super.setValue(value, noChangedEvent)
|
|
||||||
|
|
||||||
value = get(this.value);
|
|
||||||
|
|
||||||
const index = this.properties.choices.indexOf(value)
|
|
||||||
if (index === -1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.index = index;
|
|
||||||
this.indexWidget.value = index;
|
|
||||||
this.setOutputData(1, this.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(param: any): string {
|
|
||||||
if (typeof param === "string") {
|
|
||||||
if (this.properties.choices.indexOf(param) === -1)
|
|
||||||
return this.properties.choices[0]
|
|
||||||
return param
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const index = clamp(parseInt(param), 0, this.properties.choices.length - 1)
|
|
||||||
return this.properties.choices[index] || this.properties.defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyRadioNode,
|
|
||||||
title: "UI.Radio",
|
|
||||||
desc: "Radio that outputs a string and index",
|
|
||||||
type: "ui/radio"
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfyImageEditorNodeProperties extends ComfyWidgetProperties {
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyImageEditorNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
|
|
||||||
override properties: ComfyImageEditorNodeProperties = {
|
|
||||||
defaultValue: [],
|
|
||||||
tags: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "images", type: "COMFYBOX_IMAGES" }, // TODO support batches
|
|
||||||
{ name: "changed", type: BuiltInSlotType.EVENT },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = ImageEditorWidget;
|
|
||||||
override defaultValue = [];
|
|
||||||
override storeActionName = "store";
|
|
||||||
override saveUserState = false;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(value: any): ComfyBoxImageMetadata[] {
|
|
||||||
return parseWhateverIntoImageMetadata(value) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
override formatValue(value: GradioFileData[]): string {
|
|
||||||
return `Images: ${value?.length || 0}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyImageEditorNode,
|
|
||||||
title: "UI.ImageEditor",
|
|
||||||
desc: "Widget that lets you upload and edit a multi-layered image. Can also act like a standalone image uploader.",
|
|
||||||
type: "ui/image_editor"
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
export { default as ComfyReroute } from "./ComfyReroute"
|
export { default as ComfyReroute } from "./ComfyReroute"
|
||||||
export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes"
|
|
||||||
export {
|
export {
|
||||||
ComfyQueueEvents,
|
ComfyQueueEvents,
|
||||||
ComfyCopyAction,
|
ComfyCopyAction,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import layoutState, { type WidgetLayout } from "$lib/stores/layoutState"
|
|||||||
import selectionState from "$lib/stores/selectionState"
|
import selectionState from "$lib/stores/selectionState"
|
||||||
import type { SvelteComponentDev } from "svelte/internal";
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, GraphInput } from "@litegraph-ts/core";
|
import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, GraphInput } from "@litegraph-ts/core";
|
||||||
import type { ComfyExecutionResult, ComfyImageLocation } from "./nodes/ComfyWidgetNodes";
|
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
import type { ComfyNodeID } from "./api";
|
import type { ComfyNodeID } from "./api";
|
||||||
|
|
||||||
@@ -332,6 +331,24 @@ export async function uploadImageToComfyUI(blob: Blob, filename: string, type: C
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Raw output as received from ComfyUI's backend */
|
||||||
|
export interface ComfyExecutionResult {
|
||||||
|
// Technically this response can contain arbitrary data, but "images" is the
|
||||||
|
// most frequently used as it's output by LoadImage and PreviewImage, the
|
||||||
|
// only two output nodes in base ComfyUI.
|
||||||
|
images: ComfyImageLocation[] | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Raw output entry as received from ComfyUI's backend */
|
||||||
|
export type ComfyImageLocation = {
|
||||||
|
/* Filename with extension in the subfolder. */
|
||||||
|
filename: string,
|
||||||
|
/* Subfolder in the containing folder. */
|
||||||
|
subfolder: string,
|
||||||
|
/* Base ComfyUI folder where the image is located. */
|
||||||
|
type: ComfyUploadImageType
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convenient type for passing around image filepaths and their metadata with
|
* Convenient type for passing around image filepaths and their metadata with
|
||||||
* wires. Needs to be converted to a filename for use with LoadImage.
|
* wires. Needs to be converted to a filename for use with LoadImage.
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import type { IWidget, LGraphNode } from "@litegraph-js/core";
|
import { LGraphNode, LiteGraph } from "@litegraph-ts/core";
|
||||||
import ComfyValueControlWidget from "./widgets/ComfyValueControlWidget";
|
|
||||||
import type { ComfyInputConfig } from "./IComfyInputSlot";
|
|
||||||
import type IComfyInputSlot from "./IComfyInputSlot";
|
import type IComfyInputSlot from "./IComfyInputSlot";
|
||||||
import { BuiltInSlotShape, LiteGraph } from "@litegraph-ts/core";
|
import type { ComfyInputConfig } from "./IComfyInputSlot";
|
||||||
import { ComfyComboNode, ComfySliderNode, ComfyTextNode } from "./nodes";
|
import { ComfyComboNode, ComfyNumberNode, ComfyTextNode } from "./nodes/widgets";
|
||||||
|
|
||||||
type WidgetFactory = (node: LGraphNode, inputName: string, inputData: any) => IComfyInputSlot;
|
type WidgetFactory = (node: LGraphNode, inputName: string, inputData: any) => IComfyInputSlot;
|
||||||
|
|
||||||
@@ -37,12 +35,12 @@ function addComfyInput(node: LGraphNode, inputName: string, extraInfo: Partial<I
|
|||||||
|
|
||||||
const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
||||||
const config = getNumberDefaults(inputData, 0.5);
|
const config = getNumberDefaults(inputData, 0.5);
|
||||||
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfySliderNode })
|
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfyNumberNode })
|
||||||
}
|
}
|
||||||
|
|
||||||
const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
||||||
const config = getNumberDefaults(inputData, 1);
|
const config = getNumberDefaults(inputData, 1);
|
||||||
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfySliderNode })
|
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfyNumberNode })
|
||||||
};
|
};
|
||||||
|
|
||||||
const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ComfyButtonNode } from "$lib/nodes/ComfyWidgetNodes";
|
|
||||||
import type { ComfySliderNode } from "$lib/nodes/index";
|
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import { Button } from "@gradio/button";
|
import { Button } from "@gradio/button";
|
||||||
import { get, type Writable, writable } from "svelte/store";
|
import { get, type Writable, writable } from "svelte/store";
|
||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
|
import type { ComfyButtonNode } from "$lib/nodes/widgets";
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
let node: ComfyButtonNode | null = null;
|
let node: ComfyButtonNode | null = null;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ComfyCheckboxNode } from "$lib/nodes/ComfyWidgetNodes";
|
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import { Block } from "@gradio/atoms";
|
import { Block } from "@gradio/atoms";
|
||||||
import { Checkbox } from "@gradio/form";
|
import { Checkbox } from "@gradio/form";
|
||||||
import { get, type Writable, writable } from "svelte/store";
|
import { get, type Writable, writable } from "svelte/store";
|
||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
import type { SelectData } from "@gradio/utils";
|
import type { SelectData } from "@gradio/utils";
|
||||||
|
import type { ComfyCheckboxNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import Select from 'svelte-select';
|
import Select from 'svelte-select';
|
||||||
// import VirtualList from '$lib/components/VirtualList.svelte';
|
// import VirtualList from '$lib/components/VirtualList.svelte';
|
||||||
import VirtualList from 'svelte-tiny-virtual-list';
|
import VirtualList from 'svelte-tiny-virtual-list';
|
||||||
import type { ComfyComboNode } from "$lib/nodes/index";
|
import type { ComfyComboNode } from "$lib/nodes/widgets";
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
|
||||||
import type { IWidget, LGraphNode, SerializedLGraphNode, Vector2, WidgetCallback, WidgetTypes } from "@litegraph-ts/core";
|
|
||||||
|
|
||||||
export default abstract class ComfyWidget<T = any, V = any> implements IWidget<T, V> {
|
|
||||||
name: string;
|
|
||||||
value: V;
|
|
||||||
node: ComfyGraphNode;
|
|
||||||
|
|
||||||
constructor(name: string, value: V, node: ComfyGraphNode) {
|
|
||||||
this.name = name;
|
|
||||||
this.value = value
|
|
||||||
this.node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
isVirtual?: boolean;
|
|
||||||
options?: T;
|
|
||||||
type?: WidgetTypes | string | any;
|
|
||||||
y?: number;
|
|
||||||
property?: string;
|
|
||||||
last_y?: number;
|
|
||||||
width?: number;
|
|
||||||
clicked?: boolean;
|
|
||||||
marker?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
callback?: WidgetCallback<this>;
|
|
||||||
|
|
||||||
setValue(value: V) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
draw?(ctx: CanvasRenderingContext2D, node: LGraphNode, width: number, posY: number, height: number): void;
|
|
||||||
|
|
||||||
mouse?(event: MouseEvent, pos: Vector2, node: LGraphNode): boolean;
|
|
||||||
|
|
||||||
computeSize?(width: number): [number, number];
|
|
||||||
|
|
||||||
afterQueued?(): void;
|
|
||||||
|
|
||||||
serializeValue?(serialized: SerializedLGraphNode<LGraphNode>, slot: number): Promise<any>;
|
|
||||||
}
|
|
||||||
@@ -7,11 +7,11 @@
|
|||||||
import type { Styles } from "@gradio/utils";
|
import type { Styles } from "@gradio/utils";
|
||||||
import type { WidgetLayout } from "$lib/stores/layoutState";
|
import type { WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
|
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
||||||
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
|
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
|
||||||
import { f7 } from "framework7-svelte";
|
import { f7 } from "framework7-svelte";
|
||||||
|
import type { ComfyGalleryNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
|
import { Block } from "@gradio/atoms";
|
||||||
|
import { TextBox } from "@gradio/form";
|
||||||
|
import Row from "$lib/components/gradio/app/Row.svelte";
|
||||||
|
import { get, type Writable } from "svelte/store";
|
||||||
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
|
import { Button } from "@gradio/button";
|
||||||
|
import { Embed as Klecks } from "klecks";
|
||||||
|
|
||||||
|
import "klecks/style/style.scss";
|
||||||
|
import ImageUpload from "$lib/components/ImageUpload.svelte";
|
||||||
|
import { uploadImageToComfyUI, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata, comfyBoxImageToComfyURL, comfyBoxImageToComfyFile, type ComfyUploadImageType, type ComfyImageLocation } from "$lib/utils";
|
||||||
|
import notify from "$lib/notify";
|
||||||
|
import NumberInput from "$lib/components/NumberInput.svelte";
|
||||||
|
import type { ComfyImageEditorNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
|
export let widget: WidgetLayout | null = null;
|
||||||
|
export let isMobile: boolean = false;
|
||||||
|
let node: ComfyImageEditorNode | null = null;
|
||||||
|
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
||||||
|
let attrsChanged: Writable<number> | null = null;
|
||||||
|
|
||||||
|
let imgWidth: number = 0;
|
||||||
|
let imgHeight: number = 0;
|
||||||
|
|
||||||
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
|
$: if ($nodeValue && $nodeValue.length > 0) {
|
||||||
|
// TODO improve
|
||||||
|
if (imgWidth > 0 && imgHeight > 0) {
|
||||||
|
$nodeValue[0].width = imgWidth
|
||||||
|
$nodeValue[0].height = imgHeight
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$nodeValue[0].width = 0
|
||||||
|
$nodeValue[0].height = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNodeValue(widget: WidgetLayout) {
|
||||||
|
if (widget) {
|
||||||
|
node = widget.node as ComfyImageEditorNode
|
||||||
|
nodeValue = node.value;
|
||||||
|
attrsChanged = widget.attrsChanged;
|
||||||
|
status = $nodeValue && $nodeValue.length > 0 ? "uploaded" : "empty"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let editorRoot: HTMLDivElement | null = null;
|
||||||
|
let showModal = false;
|
||||||
|
let kl: Klecks | null = null;
|
||||||
|
|
||||||
|
function disposeEditor() {
|
||||||
|
console.warn("[ImageEditorWidget] CLOSING", widget, $nodeValue)
|
||||||
|
|
||||||
|
if (editorRoot) {
|
||||||
|
while (editorRoot.firstChild) {
|
||||||
|
editorRoot.removeChild(editorRoot.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kl = null;
|
||||||
|
showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBlankCanvas(width: number, height: number, fill: string = "#fff"): HTMLCanvasElement {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = fill,
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.restore();
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadImage(imageURL: string): Promise<HTMLImageElement> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const e = new Image();
|
||||||
|
e.setAttribute('crossorigin', 'anonymous'); // Don't taint the canvas from loading files on-disk
|
||||||
|
e.addEventListener("load", () => { resolve(e); });
|
||||||
|
e.src = imageURL;
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateImageCanvas(imageURL: string): Promise<[HTMLCanvasElement, number, number]> {
|
||||||
|
const image = await loadImage(imageURL);
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = image.width;
|
||||||
|
canvas.height = image.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = "rgba(255, 255, 255, 0.0)";
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
ctx.restore();
|
||||||
|
return [canvas, image.width, image.height];
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILENAME: string = "ComfyUITemp.png";
|
||||||
|
const SUBFOLDER: string = "ComfyBox_Editor";
|
||||||
|
const DIRECTORY: ComfyUploadImageType = "input";
|
||||||
|
|
||||||
|
async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
|
||||||
|
const blob = kl.getPNG();
|
||||||
|
|
||||||
|
status = "uploading"
|
||||||
|
|
||||||
|
await uploadImageToComfyUI(blob, FILENAME, DIRECTORY, SUBFOLDER)
|
||||||
|
.then((entry: ComfyImageLocation) => {
|
||||||
|
const meta: ComfyBoxImageMetadata = comfyFileToComfyBoxMetadata(entry);
|
||||||
|
$nodeValue = [meta] // TODO more than one image
|
||||||
|
status = "uploaded"
|
||||||
|
notify("Saved image to ComfyUI!", { type: "success" })
|
||||||
|
onSuccess();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notify(`Failed to upload image from editor: ${err}`, { type: "error", timeout: 10000 })
|
||||||
|
status = "error"
|
||||||
|
uploadError = err;
|
||||||
|
$nodeValue = []
|
||||||
|
onError();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let closeDialog = null;
|
||||||
|
|
||||||
|
async function saveAndClose() {
|
||||||
|
console.log(closeDialog, kl)
|
||||||
|
if (!closeDialog || !kl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
submitKlecksToComfyUI(() => {}, () => {});
|
||||||
|
closeDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
let blankImageWidth = 512;
|
||||||
|
let blankImageHeight = 512;
|
||||||
|
|
||||||
|
async function openImageEditor() {
|
||||||
|
if (!editorRoot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
showModal = true;
|
||||||
|
|
||||||
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
|
|
||||||
|
kl = new Klecks({
|
||||||
|
embedUrl: url,
|
||||||
|
onSubmit: submitKlecksToComfyUI,
|
||||||
|
targetEl: editorRoot,
|
||||||
|
warnOnPageClose: false
|
||||||
|
});
|
||||||
|
|
||||||
|
console.warn("[ImageEditorWidget] OPENING", widget, $nodeValue)
|
||||||
|
|
||||||
|
let canvas = null;
|
||||||
|
let width = blankImageWidth;
|
||||||
|
let height = blankImageHeight;
|
||||||
|
|
||||||
|
if ($nodeValue && $nodeValue.length > 0) {
|
||||||
|
const comfyImage = $nodeValue[0];
|
||||||
|
const comfyURL = comfyBoxImageToComfyURL(comfyImage);
|
||||||
|
[canvas, width, height] = await generateImageCanvas(comfyURL);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
canvas = generateBlankCanvas(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
kl.openProject({
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
layers: [{
|
||||||
|
name: 'Image',
|
||||||
|
opacity: 1,
|
||||||
|
mixModeStr: 'source-over',
|
||||||
|
image: canvas
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
kl.klApp?.out("yo");
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = "empty";
|
||||||
|
let uploadError = null;
|
||||||
|
|
||||||
|
function onUploading() {
|
||||||
|
console.warn("UPLOADING!!!")
|
||||||
|
uploadError = null;
|
||||||
|
status = "uploading"
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUploaded(e: CustomEvent<ComfyImageLocation[]>) {
|
||||||
|
console.warn("UPLOADED!!!")
|
||||||
|
uploadError = null;
|
||||||
|
status = "uploaded"
|
||||||
|
$nodeValue = e.detail.map(comfyFileToComfyBoxMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() {
|
||||||
|
console.warn("CLEAR!!!")
|
||||||
|
uploadError = null;
|
||||||
|
status = "empty"
|
||||||
|
$nodeValue = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUploadError(e: CustomEvent<any>) {
|
||||||
|
console.warn("ERROR!!!")
|
||||||
|
status = "error"
|
||||||
|
uploadError = e.detail
|
||||||
|
$nodeValue = []
|
||||||
|
notify(`Failed to upload image to ComfyUI: ${uploadError}`, { type: "error", timeout: 10000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(e: CustomEvent<ComfyImageLocation[]>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
let _value: ComfyImageLocation[] = []
|
||||||
|
$: if ($nodeValue)
|
||||||
|
_value = $nodeValue.map(comfyBoxImageToComfyFile)
|
||||||
|
else
|
||||||
|
_value = []
|
||||||
|
|
||||||
|
$: canEdit = status === "empty" || status === "uploaded";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper comfy-image-editor">
|
||||||
|
{#if widget.attrs.variant === "fileUpload" || isMobile}
|
||||||
|
<ImageUpload value={_value}
|
||||||
|
bind:imgWidth
|
||||||
|
bind:imgHeight
|
||||||
|
fileCount={"single"}
|
||||||
|
elem_classes={[]}
|
||||||
|
style={""}
|
||||||
|
label={widget.attrs.title}
|
||||||
|
on:uploading={onUploading}
|
||||||
|
on:uploaded={onUploaded}
|
||||||
|
on:upload_error={onUploadError}
|
||||||
|
on:clear={onClear}
|
||||||
|
on:change={onChange}
|
||||||
|
on:image_clicked={openImageEditor}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="comfy-image-editor-panel">
|
||||||
|
<ImageUpload value={_value}
|
||||||
|
bind:imgWidth
|
||||||
|
bind:imgHeight
|
||||||
|
fileCount={"single"}
|
||||||
|
elem_classes={[]}
|
||||||
|
style={""}
|
||||||
|
label={widget.attrs.title}
|
||||||
|
on:uploading={onUploading}
|
||||||
|
on:uploaded={onUploaded}
|
||||||
|
on:upload_error={onUploadError}
|
||||||
|
on:clear={onClear}
|
||||||
|
on:change={onChange}
|
||||||
|
on:image_clicked={openImageEditor}
|
||||||
|
/>
|
||||||
|
<Modal bind:showModal closeOnClick={false} on:close={disposeEditor} bind:closeDialog>
|
||||||
|
<div>
|
||||||
|
<div id="klecks-loading-screen">
|
||||||
|
<span id="klecks-loading-screen-text"></span>
|
||||||
|
</div>
|
||||||
|
<div class="image-editor-root" bind:this={editorRoot} />
|
||||||
|
</div>
|
||||||
|
<div slot="buttons">
|
||||||
|
<Button variant="primary" on:click={saveAndClose}>
|
||||||
|
Save and Close
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" on:click={closeDialog}>
|
||||||
|
Discard Edits
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<Block>
|
||||||
|
{#if !$nodeValue || $nodeValue.length === 0}
|
||||||
|
<Row>
|
||||||
|
<Row>
|
||||||
|
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
||||||
|
Create Image
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
|
||||||
|
</div>
|
||||||
|
{#if uploadError}
|
||||||
|
<div>
|
||||||
|
Upload error: {uploadError}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<NumberInput label={"Width"} min={64} max={2048} step={64} bind:value={blankImageWidth} />
|
||||||
|
<NumberInput label={"Height"} min={64} max={2048} step={64} bind:value={blankImageHeight} />
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
{:else}
|
||||||
|
<Row>
|
||||||
|
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
||||||
|
Edit Image
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<TextBox label={""} show_label={false} disabled={true} lines={1} max_lines={1} value="Status: {status}"/>
|
||||||
|
</div>
|
||||||
|
{#if uploadError}
|
||||||
|
<div>
|
||||||
|
Upload error: {uploadError}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Row>
|
||||||
|
{/if}
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.image-editor-root {
|
||||||
|
width: 75vw;
|
||||||
|
height: 75vh;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
:global(> .g-root) {
|
||||||
|
height: calc(100% - 59px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-image-editor {
|
||||||
|
:global(> dialog) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.kl-popup) {
|
||||||
|
z-index: 999999999999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
173
src/lib/widgets/NumberWidget.svelte
Normal file
173
src/lib/widgets/NumberWidget.svelte
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ComfyNumberNode } from "$lib/nodes/widgets";
|
||||||
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
|
import { Range } from "$lib/components/gradio/form";
|
||||||
|
import { get, type Writable } from "svelte/store";
|
||||||
|
import { debounce } from "$lib/utils";
|
||||||
|
import interfaceState from "$lib/stores/interfaceState";
|
||||||
|
import { isDisabled } from "./utils"
|
||||||
|
export let widget: WidgetLayout | null = null;
|
||||||
|
export let isMobile: boolean = false;
|
||||||
|
let node: ComfyNumberNode | null = null;
|
||||||
|
let nodeValue: Writable<number> | null = null;
|
||||||
|
let propsChanged: Writable<number> | null = null;
|
||||||
|
let option: number | null = null;
|
||||||
|
let isDragging: boolean = false;
|
||||||
|
|
||||||
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
|
function setNodeValue(widget: WidgetLayout) {
|
||||||
|
if (widget) {
|
||||||
|
node = widget.node as ComfyNumberNode
|
||||||
|
nodeValue = node.value;
|
||||||
|
propsChanged = node.propsChanged;
|
||||||
|
setOption($nodeValue); // don't react on option
|
||||||
|
}
|
||||||
|
isDragging = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// I don't know why but this is necessary to watch for changes to node
|
||||||
|
// properties from ComfyWidgetNode.
|
||||||
|
$: if (nodeValue !== null && (!$propsChanged || $propsChanged)) {
|
||||||
|
setOption($nodeValue)
|
||||||
|
setNodeValue(widget)
|
||||||
|
node.properties = node.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOption(value: any) {
|
||||||
|
option = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRelease(e: Event) {
|
||||||
|
if (nodeValue && option != null) {
|
||||||
|
$nodeValue = option
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBackgroundSize(input: HTMLInputElement) {
|
||||||
|
input.style.setProperty("--background-size", `${getBackgroundSize(input)}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBackgroundSize(input: HTMLInputElement) {
|
||||||
|
const min = +input.min || 0;
|
||||||
|
const max = +input.max || 100;
|
||||||
|
const value = +input.value;
|
||||||
|
|
||||||
|
return (value - min) / (max - min) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSliderForMobile() {
|
||||||
|
const target = elem.querySelector<HTMLInputElement>("input[type=range]");
|
||||||
|
setBackgroundSize(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
let elem: HTMLDivElement = null;
|
||||||
|
|
||||||
|
$: if (elem) {
|
||||||
|
updateSliderForMobile()
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (elem && node !== null && option !== null && (!$propsChanged || $propsChanged)) {
|
||||||
|
const slider = elem.querySelector("input[type='range']") as any
|
||||||
|
//const range_selectors = "[id$='_clone']:is(input[type='range'])";
|
||||||
|
let spacing = ((slider.step / ( slider.max - slider.min )) * 100.0);
|
||||||
|
let tsp = 'max(3px, calc('+spacing+'% - 2px))';
|
||||||
|
let fsp = 'max(4px, calc('+spacing+'% + 0px))';
|
||||||
|
const style = elem.style;
|
||||||
|
style.setProperty('--ae-slider-bg-overlay', 'repeating-linear-gradient( 90deg, transparent, transparent '+tsp+', var(--ae-input-border-color) '+tsp+', var(--ae-input-border-color) '+fsp+' )');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerDown(e: PointerEvent) {
|
||||||
|
if (!isMobile)
|
||||||
|
return;
|
||||||
|
|
||||||
|
interfaceState.showIndicator(e.clientX, e.clientY, option);
|
||||||
|
}
|
||||||
|
|
||||||
|
let canVibrate = true;
|
||||||
|
let lastDisplayValue = null;
|
||||||
|
|
||||||
|
function onPointerMove(e: PointerEvent) {
|
||||||
|
if (!isMobile)
|
||||||
|
return;
|
||||||
|
interfaceState.showIndicator(e.clientX, e.clientY, option);
|
||||||
|
|
||||||
|
if (canVibrate && lastDisplayValue != option) {
|
||||||
|
lastDisplayValue = option;
|
||||||
|
canVibrate = false;
|
||||||
|
setTimeout(() => { canVibrate = true }, 30)
|
||||||
|
navigator.vibrate(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper gradio-slider" class:mobile={isMobile} bind:this={elem}>
|
||||||
|
{#if node !== null && option !== null}
|
||||||
|
<Range
|
||||||
|
bind:value={option}
|
||||||
|
disabled={isDisabled(widget)}
|
||||||
|
minimum={node.properties.min}
|
||||||
|
maximum={node.properties.max}
|
||||||
|
step={node.properties.step}
|
||||||
|
label={widget.attrs.title}
|
||||||
|
show_label={true}
|
||||||
|
on:release={onRelease}
|
||||||
|
on:change={updateSliderForMobile}
|
||||||
|
on:pointerdown={onPointerDown}
|
||||||
|
on:pointermove={onPointerMove}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.wrapper {
|
||||||
|
padding: 2px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
:global(input[type=number]) {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
&:disabled {
|
||||||
|
@include disable-input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mobile {
|
||||||
|
// Prevent swiping on the slider track from accidentally changing the value
|
||||||
|
:global(input[type="range"]) {
|
||||||
|
pointer-events: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: default;
|
||||||
|
height: 0.6rem;
|
||||||
|
padding: initial;
|
||||||
|
border: initial;
|
||||||
|
margin: 0.8rem 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
background: linear-gradient(to right, var(--color-blue-600), var(--color-blue-600)), #D7D7D7;
|
||||||
|
background-size: var(--background-size, 0%) 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid var(--neutral-400);
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
pointer-events: all;
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-blue-600);
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid var(--neutral-100);
|
||||||
|
box-shadow: 0px 0px 0px 1px var(--neutral-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(input[type=number]) {
|
||||||
|
font-size: 16px;
|
||||||
|
height: var(--size-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ComfyRadioNode } from "$lib/nodes/ComfyWidgetNodes";
|
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import { Block } from "@gradio/atoms";
|
import { Block } from "@gradio/atoms";
|
||||||
import { Radio } from "@gradio/form";
|
import { Radio } from "@gradio/form";
|
||||||
@@ -7,6 +6,7 @@
|
|||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
import type { SelectData } from "@gradio/utils";
|
import type { SelectData } from "@gradio/utils";
|
||||||
import { clamp } from "$lib/utils";
|
import { clamp } from "$lib/utils";
|
||||||
|
import type { ComfyRadioNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { ComfySliderNode } from "$lib/nodes/index";
|
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
|
||||||
import { Range } from "$lib/components/gradio/form";
|
|
||||||
import { get, type Writable } from "svelte/store";
|
|
||||||
import { debounce } from "$lib/utils";
|
|
||||||
import interfaceState from "$lib/stores/interfaceState";
|
|
||||||
import { isDisabled } from "./utils"
|
|
||||||
export let widget: WidgetLayout | null = null;
|
|
||||||
export let isMobile: boolean = false;
|
|
||||||
let node: ComfySliderNode | null = null;
|
|
||||||
let nodeValue: Writable<number> | null = null;
|
|
||||||
let propsChanged: Writable<number> | null = null;
|
|
||||||
let option: number | null = null;
|
|
||||||
let isDragging: boolean = false;
|
|
||||||
|
|
||||||
$: widget && setNodeValue(widget);
|
|
||||||
|
|
||||||
function setNodeValue(widget: WidgetLayout) {
|
|
||||||
if (widget) {
|
|
||||||
node = widget.node as ComfySliderNode
|
|
||||||
nodeValue = node.value;
|
|
||||||
propsChanged = node.propsChanged;
|
|
||||||
setOption($nodeValue); // don't react on option
|
|
||||||
}
|
|
||||||
isDragging = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// I don't know why but this is necessary to watch for changes to node
|
|
||||||
// properties from ComfyWidgetNode.
|
|
||||||
$: if (nodeValue !== null && (!$propsChanged || $propsChanged)) {
|
|
||||||
setOption($nodeValue)
|
|
||||||
setNodeValue(widget)
|
|
||||||
node.properties = node.properties
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOption(value: any) {
|
|
||||||
option = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRelease(e: Event) {
|
|
||||||
if (nodeValue && option != null) {
|
|
||||||
$nodeValue = option
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setBackgroundSize(input: HTMLInputElement) {
|
|
||||||
input.style.setProperty("--background-size", `${getBackgroundSize(input)}%`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBackgroundSize(input: HTMLInputElement) {
|
|
||||||
const min = +input.min || 0;
|
|
||||||
const max = +input.max || 100;
|
|
||||||
const value = +input.value;
|
|
||||||
|
|
||||||
return (value - min) / (max - min) * 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSliderForMobile() {
|
|
||||||
const target = elem.querySelector<HTMLInputElement>("input[type=range]");
|
|
||||||
setBackgroundSize(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
let elem: HTMLDivElement = null;
|
|
||||||
|
|
||||||
$: if (elem) {
|
|
||||||
updateSliderForMobile()
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (elem && node !== null && option !== null && (!$propsChanged || $propsChanged)) {
|
|
||||||
const slider = elem.querySelector("input[type='range']") as any
|
|
||||||
//const range_selectors = "[id$='_clone']:is(input[type='range'])";
|
|
||||||
let spacing = ((slider.step / ( slider.max - slider.min )) * 100.0);
|
|
||||||
let tsp = 'max(3px, calc('+spacing+'% - 2px))';
|
|
||||||
let fsp = 'max(4px, calc('+spacing+'% + 0px))';
|
|
||||||
const style = elem.style;
|
|
||||||
style.setProperty('--ae-slider-bg-overlay', 'repeating-linear-gradient( 90deg, transparent, transparent '+tsp+', var(--ae-input-border-color) '+tsp+', var(--ae-input-border-color) '+fsp+' )');
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPointerDown(e: PointerEvent) {
|
|
||||||
if (!isMobile)
|
|
||||||
return;
|
|
||||||
|
|
||||||
interfaceState.showIndicator(e.clientX, e.clientY, option);
|
|
||||||
}
|
|
||||||
|
|
||||||
let canVibrate = true;
|
|
||||||
let lastDisplayValue = null;
|
|
||||||
|
|
||||||
function onPointerMove(e: PointerEvent) {
|
|
||||||
if (!isMobile)
|
|
||||||
return;
|
|
||||||
interfaceState.showIndicator(e.clientX, e.clientY, option);
|
|
||||||
|
|
||||||
if (canVibrate && lastDisplayValue != option) {
|
|
||||||
lastDisplayValue = option;
|
|
||||||
canVibrate = false;
|
|
||||||
setTimeout(() => { canVibrate = true }, 30)
|
|
||||||
navigator.vibrate(10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="wrapper gradio-slider" class:mobile={isMobile} bind:this={elem}>
|
|
||||||
{#if node !== null && option !== null}
|
|
||||||
<Range
|
|
||||||
bind:value={option}
|
|
||||||
disabled={isDisabled(widget)}
|
|
||||||
minimum={node.properties.min}
|
|
||||||
maximum={node.properties.max}
|
|
||||||
step={node.properties.step}
|
|
||||||
label={widget.attrs.title}
|
|
||||||
show_label={true}
|
|
||||||
on:release={onRelease}
|
|
||||||
on:change={updateSliderForMobile}
|
|
||||||
on:pointerdown={onPointerDown}
|
|
||||||
on:pointermove={onPointerMove}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.wrapper {
|
|
||||||
padding: 2px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
:global(input[type=number]) {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
&:disabled {
|
|
||||||
@include disable-input;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mobile {
|
|
||||||
// Prevent swiping on the slider track from accidentally changing the value
|
|
||||||
:global(input[type="range"]) {
|
|
||||||
pointer-events: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
cursor: default;
|
|
||||||
height: 0.6rem;
|
|
||||||
padding: initial;
|
|
||||||
border: initial;
|
|
||||||
margin: 0.8rem 0;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
background: linear-gradient(to right, var(--color-blue-600), var(--color-blue-600)), #D7D7D7;
|
|
||||||
background-size: var(--background-size, 0%) 100%;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
border-radius: 1rem;
|
|
||||||
border: 1px solid var(--neutral-400);
|
|
||||||
|
|
||||||
&::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
pointer-events: all;
|
|
||||||
width: 1.75rem;
|
|
||||||
height: 1.75rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--color-blue-600);
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid var(--neutral-100);
|
|
||||||
box-shadow: 0px 0px 0px 1px var(--neutral-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(input[type=number]) {
|
|
||||||
font-size: 16px;
|
|
||||||
height: var(--size-6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { TextBox } from "@gradio/form";
|
import { TextBox } from "@gradio/form";
|
||||||
import type { ComfyComboNode } from "$lib/nodes/index";
|
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import { get, type Writable } from "svelte/store";
|
import { type Writable } from "svelte/store";
|
||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
|
import type { ComfyTextNode } from "$lib/nodes/widgets";
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
let node: ComfyComboNode | null = null;
|
|
||||||
|
let node: ComfyTextNode | null = null;
|
||||||
let nodeValue: Writable<string> | null = null;
|
let nodeValue: Writable<string> | null = null;
|
||||||
let propsChanged: Writable<number> | null = null;
|
let propsChanged: Writable<number> | null = null;
|
||||||
let itemValue: WidgetUIStateStore | null = null;
|
|
||||||
|
|
||||||
$: widget && setNodeValue(widget);
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
function setNodeValue(widget: WidgetLayout) {
|
function setNodeValue(widget: WidgetLayout) {
|
||||||
if (widget) {
|
if (widget) {
|
||||||
node = widget.node as ComfySliderNode
|
node = widget.node as ComfyTextNode
|
||||||
nodeValue = node.value;
|
nodeValue = node.value;
|
||||||
propsChanged = node.propsChanged;
|
propsChanged = node.propsChanged;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export { default as ComfyGalleryWidget } from "./ComfyGalleryWidget"
|
|
||||||
export { default as ComfyGalleryWidget_Svelte } from "./ComfyGalleryWidget.svelte"
|
|
||||||
Reference in New Issue
Block a user