Enum support
This commit is contained in:
@@ -194,6 +194,8 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CONNECTION_POS: Vector2 = [0, 0];
|
||||||
|
|
||||||
private highlightNodeInput(node: LGraphNode, inputSlot: SlotNameOrIndex, ctx: CanvasRenderingContext2D) {
|
private highlightNodeInput(node: LGraphNode, inputSlot: SlotNameOrIndex, ctx: CanvasRenderingContext2D) {
|
||||||
let inputIndex: number;
|
let inputIndex: number;
|
||||||
if (typeof inputSlot === "number")
|
if (typeof inputSlot === "number")
|
||||||
@@ -201,7 +203,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
else
|
else
|
||||||
inputIndex = node.findInputSlotIndexByName(inputSlot)
|
inputIndex = node.findInputSlotIndexByName(inputSlot)
|
||||||
if (inputIndex !== -1) {
|
if (inputIndex !== -1) {
|
||||||
let pos = node.getConnectionPos(true, inputIndex);
|
let pos = node.getConnectionPos(true, inputIndex, ComfyGraphCanvas.CONNECTION_POS);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false)
|
ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false)
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|||||||
@@ -274,6 +274,16 @@ export default class ComfyApp {
|
|||||||
console.error(`Failed to load config, falling back to defaults`, error)
|
console.error(`Failed to load config, falling back to defaults`, error)
|
||||||
configState.loadDefault();
|
configState.loadDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// configState.onChange("linkDisplayType", (newValue) => {
|
||||||
|
// if (!this.lCanvas)
|
||||||
|
// return;
|
||||||
|
|
||||||
|
// this.lCanvas.links_render_mode = newValue;
|
||||||
|
// this.lCanvas.setDirty(true, true);
|
||||||
|
// })
|
||||||
|
|
||||||
|
configState.runOnChangedEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadBuiltInTemplates(): Promise<SerializedComfyBoxTemplate[]> {
|
async loadBuiltInTemplates(): Promise<SerializedComfyBoxTemplate[]> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { CONFIG_CATEGORIES, CONFIG_DEFS_BY_CATEGORY, type ConfigDefAny } from "$lib/stores/configDefs";
|
import { CONFIG_CATEGORIES, CONFIG_DEFS_BY_CATEGORY, CONFIG_DEFS_BY_NAME, type ConfigDefAny, type ConfigDefEnum, type ConfigState } from "$lib/stores/configDefs";
|
||||||
import { capitalize } from "$lib/utils";
|
import { capitalize } from "$lib/utils";
|
||||||
import { Checkbox } from "@gradio/form";
|
import { Checkbox } from "@gradio/form";
|
||||||
import configState from "$lib/stores/configState";
|
import configState from "$lib/stores/configState";
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
export let app: ComfyApp
|
export let app: ComfyApp
|
||||||
|
|
||||||
let selectedCategory = CONFIG_CATEGORIES[0];
|
let selectedCategory = CONFIG_CATEGORIES[0];
|
||||||
let changes = {}
|
let changes: Partial<Record<keyof ConfigState, any>> = {}
|
||||||
|
|
||||||
const toastOptions = {
|
const toastOptions = {
|
||||||
intro: { duration: 200 },
|
intro: { duration: 200 },
|
||||||
@@ -28,14 +28,23 @@
|
|||||||
|
|
||||||
function setOption(def: ConfigDefAny, value: any) {
|
function setOption(def: ConfigDefAny, value: any) {
|
||||||
if (!configState.validateConfigOption(def, value)) {
|
if (!configState.validateConfigOption(def, value)) {
|
||||||
console.warn(`[configState] Invalid value for option ${def.name} (${v}), setting to default (${def.defaultValue})`);
|
console.warn(`[configState] Invalid value for option ${def.name} (${value}), setting to default (${def.defaultValue})`);
|
||||||
value = def.defaultValue
|
value = def.defaultValue
|
||||||
}
|
}
|
||||||
changes[def.name] = value;
|
changes[def.name] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setEnumOption(def: ConfigDefEnum<any, any>, e: Event): void {
|
||||||
|
const select = e.target as HTMLSelectElement;
|
||||||
|
const index = select.selectedIndex
|
||||||
|
setOption(def, def.options.values[index].value)
|
||||||
|
}
|
||||||
|
|
||||||
function doSave() {
|
function doSave() {
|
||||||
$configState = { ...$configState, ...changes };
|
for (const [k, v] of Object.entries(changes)) {
|
||||||
|
const def = CONFIG_DEFS_BY_NAME[k]
|
||||||
|
configState.setConfigOption(def, v, true);
|
||||||
|
}
|
||||||
changes = {};
|
changes = {};
|
||||||
const json = JSON.stringify($configState);
|
const json = JSON.stringify($configState);
|
||||||
localStorage.setItem("config", json);
|
localStorage.setItem("config", json);
|
||||||
@@ -46,7 +55,7 @@
|
|||||||
if (!confirm("Are you sure you want to reset the config to the defaults?"))
|
if (!confirm("Are you sure you want to reset the config to the defaults?"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
configState.loadDefault();
|
configState.loadDefault(true);
|
||||||
notify("Config reset!")
|
notify("Config reset!")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -88,6 +97,16 @@
|
|||||||
<span class="ctrl string-array">
|
<span class="ctrl string-array">
|
||||||
{value.join(",")}
|
{value.join(",")}
|
||||||
</span>
|
</span>
|
||||||
|
{:else if def.type === "enum"}
|
||||||
|
<div class="description">{def.description}</div>
|
||||||
|
<span class="ctrl enum">
|
||||||
|
<select id="ui-theme" name="ui-theme" on:change={(e) => setEnumOption(def, e)}>
|
||||||
|
{#each def.options.values as option, i}
|
||||||
|
{@const selected = def.options.values[i].value === value}
|
||||||
|
<option value={option.value} {selected}>{option.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
(Unknown config type {def.type})
|
(Unknown config type {def.type})
|
||||||
{/if}
|
{/if}
|
||||||
@@ -195,6 +214,17 @@
|
|||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.enum {
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,<svg fill='white' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position-x: 100%;
|
||||||
|
background-position-y: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { LinkRenderMode } from "@litegraph-ts/core";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Supported config option types.
|
* Supported config option types.
|
||||||
*/
|
*/
|
||||||
type ConfigDefType = "boolean" | "number" | "string" | "string[]";
|
type ConfigDefType = "boolean" | "number" | "string" | "string[]" | "enum";
|
||||||
|
|
||||||
// A simple parameter description interface
|
// A simple parameter description interface
|
||||||
export interface ConfigDef<IdType extends string, TypeType extends ConfigDefType, ValueType, OptionsType = any> {
|
export interface ConfigDef<IdType extends string, TypeType extends ConfigDefType, ValueType, OptionsType = any> {
|
||||||
@@ -17,21 +19,46 @@ export interface ConfigDef<IdType extends string, TypeType extends ConfigDefType
|
|||||||
|
|
||||||
defaultValue: ValueType,
|
defaultValue: ValueType,
|
||||||
|
|
||||||
options: OptionsType
|
options: OptionsType,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfigDefAny = ConfigDef<string, any, any>
|
export type ConfigDefAny = ConfigDef<string, any, any>
|
||||||
type ConfigDefBoolean<IdType extends string> = ConfigDef<IdType, "boolean", boolean>;
|
export type ConfigDefBoolean<IdType extends string> = ConfigDef<IdType, "boolean", boolean>;
|
||||||
|
|
||||||
type NumberOptions = {
|
export type NumberOptions = {
|
||||||
min?: number,
|
min?: number,
|
||||||
max?: number,
|
max?: number,
|
||||||
step: number
|
step: number
|
||||||
}
|
}
|
||||||
type ConfigDefNumber<IdType extends string> = ConfigDef<IdType, "number", number, NumberOptions>;
|
export type ConfigDefNumber<IdType extends string> = ConfigDef<IdType, "number", number, NumberOptions>;
|
||||||
|
|
||||||
type ConfigDefString<IdType extends string> = ConfigDef<IdType, "string", string>;
|
export type ConfigDefString<IdType extends string> = ConfigDef<IdType, "string", string>;
|
||||||
type ConfigDefStringArray<IdType extends string> = ConfigDef<IdType, "string[]", string[]>;
|
export type ConfigDefStringArray<IdType extends string> = ConfigDef<IdType, "string[]", string[]>;
|
||||||
|
|
||||||
|
export interface EnumValue<T> {
|
||||||
|
label: string,
|
||||||
|
value: T
|
||||||
|
}
|
||||||
|
export interface EnumOptions<T> {
|
||||||
|
values: EnumValue<T>[]
|
||||||
|
}
|
||||||
|
export type ConfigDefEnum<IdType extends string, T> = ConfigDef<IdType, "enum", T, EnumOptions<T>>;
|
||||||
|
|
||||||
|
export function validateConfigOption(def: ConfigDefAny, v: any): boolean {
|
||||||
|
switch (def.type) {
|
||||||
|
case "boolean":
|
||||||
|
return typeof v === "boolean";
|
||||||
|
case "number":
|
||||||
|
return typeof v === "number";
|
||||||
|
case "string":
|
||||||
|
return typeof v === "string";
|
||||||
|
case "string[]":
|
||||||
|
return Array.isArray(v) && v.every(vs => typeof vs === "string");
|
||||||
|
case "enum":
|
||||||
|
return Boolean(def.options.values.find((o: EnumValue<any>) => o.value === v));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Configuration parameters ------------------------------------
|
// Configuration parameters ------------------------------------
|
||||||
|
|
||||||
@@ -102,6 +129,30 @@ const defBuiltInTemplates: ConfigDefStringArray<"builtInTemplates"> = {
|
|||||||
options: {}
|
options: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const defLinkDisplayType: ConfigDefEnum<"linkDisplayType", LinkRenderMode> = {
|
||||||
|
// name: "linkDisplayType",
|
||||||
|
// type: "enum",
|
||||||
|
// defaultValue: LinkRenderMode.SPLINE_LINK,
|
||||||
|
// category: "graph",
|
||||||
|
// description: "How to display links in the graph",
|
||||||
|
// options: {
|
||||||
|
// values: [
|
||||||
|
// {
|
||||||
|
// value: LinkRenderMode.STRAIGHT_LINK,
|
||||||
|
// label: "Straight"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: LinkRenderMode.LINEAR_LINK,
|
||||||
|
// label: "Linear"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: LinkRenderMode.SPLINE_LINK,
|
||||||
|
// label: "Spline"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
// Configuration exports ------------------------------------
|
// Configuration exports ------------------------------------
|
||||||
|
|
||||||
export const CONFIG_DEFS = [
|
export const CONFIG_DEFS = [
|
||||||
@@ -112,6 +163,7 @@ export const CONFIG_DEFS = [
|
|||||||
defConfirmWhenUnloadingUnsavedChanges,
|
defConfirmWhenUnloadingUnsavedChanges,
|
||||||
defCacheBuiltInResources,
|
defCacheBuiltInResources,
|
||||||
defBuiltInTemplates,
|
defBuiltInTemplates,
|
||||||
|
// defLinkDisplayType
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const CONFIG_DEFS_BY_NAME: Record<string, ConfigDefAny>
|
export const CONFIG_DEFS_BY_NAME: Record<string, ConfigDefAny>
|
||||||
|
|||||||
@@ -2,41 +2,34 @@ import { debounce } from '$lib/utils';
|
|||||||
import { toHashMap } from '@litegraph-ts/core';
|
import { toHashMap } from '@litegraph-ts/core';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
import { defaultConfig, type ConfigState, type ConfigDefAny, CONFIG_DEFS_BY_NAME } from './configDefs';
|
import { defaultConfig, type ConfigState, type ConfigDefAny, CONFIG_DEFS_BY_NAME, validateConfigOption } from './configDefs';
|
||||||
|
|
||||||
type ConfigStateOps = {
|
type ConfigStateOps = {
|
||||||
getBackendURL: () => string,
|
getBackendURL: () => string,
|
||||||
load: (data: any) => ConfigState
|
|
||||||
loadDefault: () => ConfigState
|
load: (data: any, runOnChanged?: boolean) => ConfigState
|
||||||
setConfigOption: (def: ConfigDefAny, v: any) => boolean
|
loadDefault: (runOnChanged?: boolean) => ConfigState
|
||||||
|
setConfigOption: (def: ConfigDefAny, v: any, runOnChanged: boolean) => boolean
|
||||||
validateConfigOption: (def: ConfigDefAny, v: any) => boolean
|
validateConfigOption: (def: ConfigDefAny, v: any) => boolean
|
||||||
|
onChange: <K extends keyof ConfigState>(optionName: K, callback: ConfigOnChangeCallback<ConfigState[K]>) => void
|
||||||
|
runOnChangedEvents: () => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WritableConfigStateStore = Writable<ConfigState> & ConfigStateOps;
|
export type WritableConfigStateStore = Writable<ConfigState> & ConfigStateOps;
|
||||||
const store: Writable<ConfigState> = writable({ ...defaultConfig })
|
const store: Writable<ConfigState> = writable({ ...defaultConfig })
|
||||||
|
const callbacks: Record<string, ConfigOnChangeCallback<any>[]> = {}
|
||||||
|
let changedOptions: Partial<Record<keyof ConfigState, [any, any]>> = {}
|
||||||
|
|
||||||
function getBackendURL(): string {
|
function getBackendURL(): string {
|
||||||
const state = get(store);
|
const state = get(store);
|
||||||
return `${window.location.protocol}//${state.comfyUIHostname}:${state.comfyUIPort}`
|
return `${window.location.protocol}//${state.comfyUIHostname}:${state.comfyUIPort}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateConfigOption(def: ConfigDefAny, v: any): boolean {
|
function setConfigOption(def: ConfigDefAny, v: any, runOnChanged: boolean): boolean {
|
||||||
switch (def.type) {
|
|
||||||
case "boolean":
|
|
||||||
return typeof v === "boolean";
|
|
||||||
case "number":
|
|
||||||
return typeof v === "number";
|
|
||||||
case "string":
|
|
||||||
return typeof v === "string";
|
|
||||||
case "string[]":
|
|
||||||
return Array.isArray(v) && v.every(vs => typeof vs === "string");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setConfigOption(def: ConfigDefAny, v: any): boolean {
|
|
||||||
let valid = false;
|
let valid = false;
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
const oldValue = state[def.name]
|
||||||
|
|
||||||
valid = validateConfigOption(def, v);
|
valid = validateConfigOption(def, v);
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
@@ -47,12 +40,33 @@ function setConfigOption(def: ConfigDefAny, v: any): boolean {
|
|||||||
state[def.name] = v
|
state[def.name] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changed = oldValue != state[def.name];
|
||||||
|
if (changed) {
|
||||||
|
if (runOnChanged) {
|
||||||
|
if (callbacks[def.name]) {
|
||||||
|
for (const callback of callbacks[def.name]) {
|
||||||
|
callback(state[def.name], oldValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (changedOptions[def.name] == null) {
|
||||||
|
changedOptions[def.name] = [oldValue, state[def.name]];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
changedOptions[def.name][1] = state[def.name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
})
|
})
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
function load(data: any): ConfigState {
|
function load(data: any, runOnChanged: boolean = false): ConfigState {
|
||||||
|
changedOptions = {}
|
||||||
|
|
||||||
store.set({ ...defaultConfig })
|
store.set({ ...defaultConfig })
|
||||||
if (data != null && typeof data === "object") {
|
if (data != null && typeof data === "object") {
|
||||||
for (const [k, v] of Object.entries(data)) {
|
for (const [k, v] of Object.entries(data)) {
|
||||||
@@ -62,15 +76,36 @@ function load(data: any): ConfigState {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfigOption(def, v);
|
setConfigOption(def, v, runOnChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return get(store);
|
return get(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadDefault() {
|
function loadDefault(runOnChanged: boolean = false) {
|
||||||
return load(null);
|
return load(null, runOnChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConfigOnChangeCallback<V> = (value: V, oldValue?: V) => void;
|
||||||
|
|
||||||
|
function onChange<K extends keyof ConfigState>(optionName: K, callback: ConfigOnChangeCallback<ConfigState[K]>) {
|
||||||
|
callbacks[optionName] ||= []
|
||||||
|
callbacks[optionName].push(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function runOnChangedEvents() {
|
||||||
|
console.debug("Running changed events for config...")
|
||||||
|
for (const [optionName, [oldValue, newValue]] of Object.entries(changedOptions)) {
|
||||||
|
const def = CONFIG_DEFS_BY_NAME[optionName]
|
||||||
|
if (callbacks[optionName]) {
|
||||||
|
console.debug("Running callback!", optionName, oldValue, newValue)
|
||||||
|
for (const callback of callbacks[def.name]) {
|
||||||
|
callback(newValue, oldValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changedOptions = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const configStateStore: WritableConfigStateStore =
|
const configStateStore: WritableConfigStateStore =
|
||||||
@@ -80,6 +115,8 @@ const configStateStore: WritableConfigStateStore =
|
|||||||
validateConfigOption,
|
validateConfigOption,
|
||||||
setConfigOption,
|
setConfigOption,
|
||||||
load,
|
load,
|
||||||
loadDefault
|
loadDefault,
|
||||||
|
onChange,
|
||||||
|
runOnChangedEvents
|
||||||
}
|
}
|
||||||
export default configStateStore;
|
export default configStateStore;
|
||||||
|
|||||||
Reference in New Issue
Block a user