diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index f3c098c..ae686bb 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -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) { let inputIndex: number; if (typeof inputSlot === "number") @@ -201,7 +203,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas { else inputIndex = node.findInputSlotIndexByName(inputSlot) if (inputIndex !== -1) { - let pos = node.getConnectionPos(true, inputIndex); + let pos = node.getConnectionPos(true, inputIndex, ComfyGraphCanvas.CONNECTION_POS); ctx.beginPath(); ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false) ctx.stroke(); diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 751040c..d7e696c 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -274,6 +274,16 @@ export default class ComfyApp { console.error(`Failed to load config, falling back to defaults`, error) 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 { diff --git a/src/lib/components/ComfySettingsView.svelte b/src/lib/components/ComfySettingsView.svelte index 01fcb41..15d3dcd 100644 --- a/src/lib/components/ComfySettingsView.svelte +++ b/src/lib/components/ComfySettingsView.svelte @@ -1,5 +1,5 @@ @@ -88,6 +97,16 @@ {value.join(",")} + {:else if def.type === "enum"} +
{def.description}
+ + + {:else} (Unknown config type {def.type}) {/if} @@ -195,6 +214,17 @@ font-size: 11pt; } } + + &.enum { + select { + -webkit-appearance: none; + -moz-appearance: none; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position-x: 100%; + background-position-y: 8px; + } + } } } diff --git a/src/lib/stores/configDefs.ts b/src/lib/stores/configDefs.ts index df75e9f..0962f4f 100644 --- a/src/lib/stores/configDefs.ts +++ b/src/lib/stores/configDefs.ts @@ -1,7 +1,9 @@ +import { LinkRenderMode } from "@litegraph-ts/core"; + /* * Supported config option types. */ -type ConfigDefType = "boolean" | "number" | "string" | "string[]"; +type ConfigDefType = "boolean" | "number" | "string" | "string[]" | "enum"; // A simple parameter description interface export interface ConfigDef { @@ -17,21 +19,46 @@ export interface ConfigDef -type ConfigDefBoolean = ConfigDef; +export type ConfigDefBoolean = ConfigDef; -type NumberOptions = { +export type NumberOptions = { min?: number, max?: number, step: number } -type ConfigDefNumber = ConfigDef; +export type ConfigDefNumber = ConfigDef; -type ConfigDefString = ConfigDef; -type ConfigDefStringArray = ConfigDef; +export type ConfigDefString = ConfigDef; +export type ConfigDefStringArray = ConfigDef; + +export interface EnumValue { + label: string, + value: T +} +export interface EnumOptions { + values: EnumValue[] +} +export type ConfigDefEnum = ConfigDef>; + +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) => o.value === v)); + } + return false; +} // Configuration parameters ------------------------------------ @@ -102,6 +129,30 @@ const defBuiltInTemplates: ConfigDefStringArray<"builtInTemplates"> = { 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 ------------------------------------ export const CONFIG_DEFS = [ @@ -112,6 +163,7 @@ export const CONFIG_DEFS = [ defConfirmWhenUnloadingUnsavedChanges, defCacheBuiltInResources, defBuiltInTemplates, + // defLinkDisplayType ] as const; export const CONFIG_DEFS_BY_NAME: Record diff --git a/src/lib/stores/configState.ts b/src/lib/stores/configState.ts index 1772bca..4ecf4ce 100644 --- a/src/lib/stores/configState.ts +++ b/src/lib/stores/configState.ts @@ -2,41 +2,34 @@ import { debounce } from '$lib/utils'; import { toHashMap } from '@litegraph-ts/core'; import { get, 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 = { getBackendURL: () => string, - load: (data: any) => ConfigState - loadDefault: () => ConfigState - setConfigOption: (def: ConfigDefAny, v: any) => boolean + + load: (data: any, runOnChanged?: boolean) => ConfigState + loadDefault: (runOnChanged?: boolean) => ConfigState + setConfigOption: (def: ConfigDefAny, v: any, runOnChanged: boolean) => boolean validateConfigOption: (def: ConfigDefAny, v: any) => boolean + onChange: (optionName: K, callback: ConfigOnChangeCallback) => void + runOnChangedEvents: () => void, } export type WritableConfigStateStore = Writable & ConfigStateOps; const store: Writable = writable({ ...defaultConfig }) +const callbacks: Record[]> = {} +let changedOptions: Partial> = {} function getBackendURL(): string { const state = get(store); return `${window.location.protocol}//${state.comfyUIHostname}:${state.comfyUIPort}` } -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"); - } - return false; -} - -function setConfigOption(def: ConfigDefAny, v: any): boolean { +function setConfigOption(def: ConfigDefAny, v: any, runOnChanged: boolean): boolean { let valid = false; store.update(state => { + const oldValue = state[def.name] + valid = validateConfigOption(def, v); if (!valid) { @@ -47,12 +40,33 @@ function setConfigOption(def: ConfigDefAny, v: any): boolean { 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 valid; } -function load(data: any): ConfigState { +function load(data: any, runOnChanged: boolean = false): ConfigState { + changedOptions = {} + store.set({ ...defaultConfig }) if (data != null && typeof data === "object") { for (const [k, v] of Object.entries(data)) { @@ -62,15 +76,36 @@ function load(data: any): ConfigState { continue; } - setConfigOption(def, v); + setConfigOption(def, v, runOnChanged); } } return get(store); } -function loadDefault() { - return load(null); +function loadDefault(runOnChanged: boolean = false) { + return load(null, runOnChanged); +} + +export type ConfigOnChangeCallback = (value: V, oldValue?: V) => void; + +function onChange(optionName: K, callback: ConfigOnChangeCallback) { + 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 = @@ -80,6 +115,8 @@ const configStateStore: WritableConfigStateStore = validateConfigOption, setConfigOption, load, - loadDefault + loadDefault, + onChange, + runOnChangedEvents } export default configStateStore;