Two-way selection

This commit is contained in:
space-nuko
2023-05-16 19:04:34 -05:00
parent b515ac885a
commit cd8c93b853
18 changed files with 278 additions and 126 deletions

View File

@@ -88,7 +88,7 @@ export default class ComfyGraph extends LGraph {
if (!("svelteComponentType" in node) && options.addedBy == null) { if (!("svelteComponentType" in node) && options.addedBy == null) {
console.debug("[ComfyGraph] AutoAdd UI") console.debug("[ComfyGraph] AutoAdd UI")
const comfyNode = node as ComfyGraphNode; const comfyNode = node as ComfyGraphNode;
const widgetNodesAdded = [] const widgetNodesAdded: ComfyWidgetNode[] = []
for (let index = 0; index < comfyNode.inputs.length; index++) { for (let index = 0; index < comfyNode.inputs.length; index++) {
const input = comfyNode.inputs[index]; const input = comfyNode.inputs[index];
if ("config" in input) { if ("config" in input) {
@@ -109,10 +109,10 @@ export default class ComfyGraph extends LGraph {
} }
} }
} }
const dragItems = widgetNodesAdded.map(wn => get(layoutState).allItemsByNode[wn.id]?.dragItem).filter(di => di) const dragItemIDs = widgetNodesAdded.map(wn => get(layoutState).allItemsByNode[wn.id]?.dragItem?.id).filter(Boolean)
console.debug("[ComfyGraph] Group new widgets", dragItems) console.debug("[ComfyGraph] Group new widgets", dragItemIDs)
layoutState.groupItems(dragItems, { title: node.title }) layoutState.groupItems(dragItemIDs, { title: node.title })
} }
} }

View File

@@ -1,11 +1,13 @@
import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode, type ContextMenuItem, type IContextMenuItem, Subgraph, LLink } from "@litegraph-ts/core"; import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode, type ContextMenuItem, type IContextMenuItem, Subgraph, LLink, type NodeID } from "@litegraph-ts/core";
import type ComfyApp from "./components/ComfyApp"; import type ComfyApp from "./components/ComfyApp";
import queueState from "./stores/queueState"; import queueState from "./stores/queueState";
import { get } from "svelte/store"; import { get, type Unsubscriber } from "svelte/store";
import uiState from "./stores/uiState"; import uiState from "./stores/uiState";
import layoutState from "./stores/layoutState"; import layoutState from "./stores/layoutState";
import { Watch } from "@litegraph-ts/nodes-basic"; import { Watch } from "@litegraph-ts/nodes-basic";
import { ComfyReroute } from "./nodes"; import { ComfyReroute } from "./nodes";
import type { Progress } from "./components/ComfyApp";
import selectionState from "./stores/selectionState";
export type SerializedGraphCanvasState = { export type SerializedGraphCanvasState = {
offset: Vector2, offset: Vector2,
@@ -14,6 +16,7 @@ export type SerializedGraphCanvasState = {
export default class ComfyGraphCanvas extends LGraphCanvas { export default class ComfyGraphCanvas extends LGraphCanvas {
app: ComfyApp | null; app: ComfyApp | null;
private _unsubscribe: Unsubscriber;
constructor( constructor(
app: ComfyApp, app: ComfyApp,
@@ -27,8 +30,22 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
) { ) {
super(canvas, app.lGraph, options); super(canvas, app.lGraph, options);
this.app = app; this.app = app;
this._unsubscribe = selectionState.subscribe(ss => {
for (const node of Object.values(this.selected_nodes)) {
node.is_selected = false;
}
this.selected_nodes = {}
for (const node of ss.currentSelectionNodes) {
this.selected_nodes[node.id] = node;
node.is_selected = true
}
this._selectedNodes = new Set()
this.setDirty(true, true);
})
} }
_selectedNodes: Set<NodeID> = new Set();
serialize(): SerializedGraphCanvasState { serialize(): SerializedGraphCanvasState {
return { return {
offset: this.ds.offset, offset: this.ds.offset,
@@ -58,51 +75,61 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
super.drawNodeShape(node, ctx, size, fgColor, bgColor, selected, mouseOver); super.drawNodeShape(node, ctx, size, fgColor, bgColor, selected, mouseOver);
let state = get(queueState); let state = get(queueState);
let ss = get(selectionState);
let color = null; let color = null;
if (node.id === +state.runningNodeID) { let thickness = 1;
// if (this._selectedNodes.has(node.id)) {
// color = "yellow";
// thickness = 5;
// }
if (ss.currentHoveredNodes.has(node.id)) {
color = "lightblue";
}
else if (node.id === +state.runningNodeID) {
color = "#0f0"; color = "#0f0";
// this.app can be null inside the constructor if rendering is taking place already
} else if (this.app && this.app.dragOverNode && node.id === this.app.dragOverNode.id) {
color = "dodgerblue";
} }
if (color) { if (color) {
const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE; this.drawNodeOutline(node, ctx, state.progress, size, fgColor, bgColor, color, thickness)
ctx.lineWidth = 1; }
ctx.globalAlpha = 0.8; }
ctx.beginPath();
if (shape == BuiltInSlotShape.BOX_SHAPE)
ctx.rect(-6, -6 + LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT);
else if (shape == BuiltInSlotShape.ROUND_SHAPE || (shape == BuiltInSlotShape.CARD_SHAPE && node.flags.collapsed))
ctx.roundRect(
-6,
-6 - LiteGraph.NODE_TITLE_HEIGHT,
12 + size[0] + 1,
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
this.round_radius * 2
);
else if (shape == BuiltInSlotShape.CARD_SHAPE)
ctx.roundRect(
-6,
-6 + LiteGraph.NODE_TITLE_HEIGHT,
12 + size[0] + 1,
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
this.round_radius * 2,
2
);
else if (shape == BuiltInSlotShape.CIRCLE_SHAPE)
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2);
ctx.strokeStyle = color;
ctx.stroke();
ctx.strokeStyle = fgColor;
ctx.globalAlpha = 1;
if (state.progress) { private drawNodeOutline(node: LGraphNode, ctx: CanvasRenderingContext2D, progress?: Progress, size: Vector2, fgColor: string, bgColor: string, outlineColor: string, outlineThickness: number) {
ctx.fillStyle = "green"; const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE;
ctx.fillRect(0, 0, size[0] * (state.progress.value / state.progress.max), 6); ctx.lineWidth = outlineThickness;
ctx.fillStyle = bgColor; ctx.globalAlpha = 0.8;
} ctx.beginPath();
if (shape == BuiltInSlotShape.BOX_SHAPE)
ctx.rect(-6, -6 + LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT);
else if (shape == BuiltInSlotShape.ROUND_SHAPE || (shape == BuiltInSlotShape.CARD_SHAPE && node.flags.collapsed))
ctx.roundRect(
-6,
-6 - LiteGraph.NODE_TITLE_HEIGHT,
12 + size[0] + 1,
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
this.round_radius * 2
);
else if (shape == BuiltInSlotShape.CARD_SHAPE)
ctx.roundRect(
-6,
-6 + LiteGraph.NODE_TITLE_HEIGHT,
12 + size[0] + 1,
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
this.round_radius * 2,
2
);
else if (shape == BuiltInSlotShape.CIRCLE_SHAPE)
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2);
ctx.strokeStyle = outlineColor;
ctx.stroke();
ctx.strokeStyle = fgColor;
ctx.globalAlpha = 1;
if (progress) {
ctx.fillStyle = "green";
ctx.fillRect(0, 0, size[0] * (progress.value / progress.max), 6);
ctx.fillStyle = bgColor;
} }
} }
@@ -235,10 +262,43 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
} }
override onSelectionChange(nodes: Record<number, LGraphNode>) { override onSelectionChange(nodes: Record<number, LGraphNode>) {
const ls = get(layoutState) selectionState.update(ss => {
ls.currentSelectionNodes = Object.values(nodes) ss.currentSelectionNodes = Object.values(nodes)
ls.currentSelection = [] ss.currentSelection = []
layoutState.set(ls) const ls = get(layoutState)
for (const node of ss.currentSelectionNodes) {
const widget = ls.allItemsByNode[node.id]
if (widget)
ss.currentSelection.push(widget.dragItem.id)
}
return ss
})
}
override onHoverChange(node: LGraphNode | null) {
selectionState.update(ss => {
ss.currentHoveredNodes.clear()
if (node) {
ss.currentHoveredNodes.add(node.id)
}
ss.currentHovered.clear()
const ls = get(layoutState)
for (const nodeID of ss.currentHoveredNodes) {
const widget = ls.allItemsByNode[nodeID]
if (widget)
ss.currentHovered.add(widget.dragItem.id)
}
return ss
})
}
override clear() {
super.clear();
selectionState.update(ss => {
ss.currentSelectionNodes = [];
ss.currentHoveredNodes.clear()
return ss;
})
} }
override onNodeMoved(node: LGraphNode) { override onNodeMoved(node: LGraphNode) {

View File

@@ -2,6 +2,7 @@
import { Block, BlockTitle } from "@gradio/atoms"; import { Block, BlockTitle } from "@gradio/atoms";
import Accordion from "$lib/components/gradio/app/Accordion.svelte"; import Accordion from "$lib/components/gradio/app/Accordion.svelte";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import selectionState from "$lib/stores/selectionState";
import WidgetContainer from "./WidgetContainer.svelte" import WidgetContainer from "./WidgetContainer.svelte"
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action'; import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
@@ -61,9 +62,10 @@
</script> </script>
{#if container && children} {#if container && children}
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)}
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}" <div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
class:hide-block={container.attrs.containerVariant === "hidden"} class:hide-block={container.attrs.containerVariant === "hidden"}
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)} class:selected
class:root-container={zIndex === 0} class:root-container={zIndex === 0}
class:is-executing={container.isNodeExecuting} class:is-executing={container.isNodeExecuting}
class:edit={edit}> class:edit={edit}>
@@ -120,6 +122,10 @@
<style lang="scss"> <style lang="scss">
.container { .container {
&.selected {
background: var(--comfy-container-selected-background-fill) !important;
}
> :global(*) { > :global(*) {
border-radius: 0; border-radius: 0;
} }

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Block, BlockTitle } from "@gradio/atoms"; import { Block, BlockTitle } from "@gradio/atoms";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import selectionState from "$lib/stores/selectionState";
import WidgetContainer from "./WidgetContainer.svelte" import WidgetContainer from "./WidgetContainer.svelte"
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action'; import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
@@ -49,9 +50,10 @@
</script> </script>
{#if container && children} {#if container && children}
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)}
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}" <div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
class:hide-block={container.attrs.containerVariant === "hidden"} class:hide-block={container.attrs.containerVariant === "hidden"}
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)} class:selected
class:root-container={zIndex === 0} class:root-container={zIndex === 0}
class:is-executing={container.isNodeExecuting} class:is-executing={container.isNodeExecuting}
class:mobile={isMobile} class:mobile={isMobile}
@@ -154,6 +156,10 @@
.container { .container {
display: flex; display: flex;
&.selected {
background: var(--comfy-container-selected-background-fill) !important;
}
> :global(*) { > :global(*) {
border-radius: 0; border-radius: 0;
} }

View File

@@ -9,6 +9,7 @@
import { Checkbox, TextBox } from "@gradio/form" import { Checkbox, TextBox } from "@gradio/form"
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import layoutState from "$lib/stores/layoutState"; import layoutState from "$lib/stores/layoutState";
import selectionState from "$lib/stores/selectionState";
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
import type { ComfyAPIStatus } from "$lib/api"; import type { ComfyAPIStatus } from "$lib/api";
import { SvelteToast, toast } from '@zerodevx/svelte-toast' import { SvelteToast, toast } from '@zerodevx/svelte-toast'
@@ -54,12 +55,12 @@
if (!$uiState.uiUnlocked) { if (!$uiState.uiUnlocked) {
app.lCanvas.deselectAllNodes(); app.lCanvas.deselectAllNodes();
$layoutState.currentSelectionNodes = [] $selectionState.currentSelectionNodes = []
} }
} }
$: if ($uiState.uiEditMode) $: if ($uiState.uiEditMode)
$layoutState.currentSelection = [] $selectionState.currentSelection = []
let graphSize = 0; let graphSize = 0;
let graphTransitioning = false; let graphTransitioning = false;

View File

@@ -92,7 +92,6 @@ export default class ComfyApp {
dropZone: HTMLElement | null = null; dropZone: HTMLElement | null = null;
nodeOutputs: Record<string, any> = {}; nodeOutputs: Record<string, any> = {};
dragOverNode: LGraphNode | null = null;
shiftDown: boolean = false; shiftDown: boolean = false;
selectedGroupMoving: boolean = false; selectedGroupMoving: boolean = false;

View File

@@ -4,6 +4,7 @@
import { LGraphNode } from "@litegraph-ts/core" import { LGraphNode } from "@litegraph-ts/core"
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState" import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
import uiState from "$lib/stores/uiState" import uiState from "$lib/stores/uiState"
import selectionState from "$lib/stores/selectionState"
import { get, type Writable, writable } from "svelte/store" import { get, type Writable, writable } from "svelte/store"
import type { ComfyWidgetNode } from "$lib/nodes"; import type { ComfyWidgetNode } from "$lib/nodes";
import ComfyNumberProperty from "./ComfyNumberProperty.svelte"; import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
@@ -17,8 +18,8 @@
$: refreshPropsPanel = $layoutState.refreshPropsPanel; $: refreshPropsPanel = $layoutState.refreshPropsPanel;
$: if ($layoutState.currentSelection.length > 0) { $: if ($selectionState.currentSelection.length > 0) {
const targetId = $layoutState.currentSelection.slice(-1)[0] const targetId = $selectionState.currentSelection.slice(-1)[0]
target = $layoutState.allItems[targetId].dragItem target = $layoutState.allItems[targetId].dragItem
attrsChanged = target.attrsChanged; attrsChanged = target.attrsChanged;
if (target.type === "widget") { if (target.type === "widget") {
@@ -28,9 +29,9 @@
node = null; node = null;
} }
} }
else if ($layoutState.currentSelectionNodes.length > 0) { else if ($selectionState.currentSelectionNodes.length > 0) {
target = null; target = null;
node = $layoutState.currentSelectionNodes[0] node = $selectionState.currentSelectionNodes[0]
attrsChanged = null; attrsChanged = null;
} }
else { else {

View File

@@ -8,6 +8,7 @@
import WidgetContainer from "./WidgetContainer.svelte"; import WidgetContainer from "./WidgetContainer.svelte";
import layoutState, { type ContainerLayout, type DragItem, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type DragItem, type IDragItem } from "$lib/stores/layoutState";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import selectionState from "$lib/stores/selectionState";
import Menu from './menu/Menu.svelte'; import Menu from './menu/Menu.svelte';
import MenuOption from './menu/MenuOption.svelte'; import MenuOption from './menu/MenuOption.svelte';
@@ -30,18 +31,23 @@
} }
function groupWidgets(horizontal: boolean) { function groupWidgets(horizontal: boolean) {
const items = layoutState.getCurrentSelection() const items = $selectionState.currentSelection
$layoutState.currentSelection = [] $selectionState.currentSelection = []
layoutState.groupItems(items, { direction: horizontal ? "horizontal" : "vertical" }) layoutState.groupItems(items, { direction: horizontal ? "horizontal" : "vertical" })
} }
let canUngroup = false; let canUngroup = false;
let isDeleteGroup = false; let isDeleteGroup = false;
$: canUngroup = $layoutState.currentSelection.length === 1 $: {
&& layoutState.getCurrentSelection()[0].type === "container" canUngroup = false;
if ($selectionState.currentSelection.length === 1) {
const item = $layoutState.allItems[$selectionState.currentSelection[0]].dragItem;
canUngroup = item.type === "container"
}
}
$: if (canUngroup) { $: if (canUngroup) {
const dragItem = layoutState.getCurrentSelection()[0]; const dragItemID = $selectionState.currentSelection[0];
const entry = $layoutState.allItems[dragItem.id]; const entry = $layoutState.allItems[dragItemID];
isDeleteGroup = entry.children.length === 0 isDeleteGroup = entry.children.length === 0
} }
else { else {
@@ -49,11 +55,15 @@
} }
function ungroup() { function ungroup() {
const item = layoutState.getCurrentSelection()[0] const itemID = $selectionState.currentSelection[0]
if (!item || item.type !== "container") if (itemID == null)
return; return;
$layoutState.currentSelection = [] const item = $layoutState.allItems[$selectionState.currentSelection[0]].dragItem;
if(item.type !== "container")
return
$selectionState.currentSelection = []
layoutState.ungroup(item as ContainerLayout) layoutState.ungroup(item as ContainerLayout)
} }
@@ -94,11 +104,11 @@
{#if showMenu} {#if showMenu}
<Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}> <Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}>
<MenuOption <MenuOption
isDisabled={$layoutState.currentSelection.length === 0} isDisabled={$selectionState.currentSelection.length === 0}
on:click={() => groupWidgets(false)} on:click={() => groupWidgets(false)}
text="Group" /> text="Group" />
<MenuOption <MenuOption
isDisabled={$layoutState.currentSelection.length === 0} isDisabled={$selectionState.currentSelection.length === 0}
on:click={() => groupWidgets(true)} on:click={() => groupWidgets(true)}
text="Group Horizontally" /> text="Group Horizontally" />
<MenuOption <MenuOption

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Block, BlockTitle } from "@gradio/atoms"; import { Block, BlockTitle } from "@gradio/atoms";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import selectionState from "$lib/stores/selectionState";
import WidgetContainer from "./WidgetContainer.svelte" import WidgetContainer from "./WidgetContainer.svelte"
import BlockContainer from "./BlockContainer.svelte" import BlockContainer from "./BlockContainer.svelte"
import AccordionContainer from "./AccordionContainer.svelte" import AccordionContainer from "./AccordionContainer.svelte"
@@ -34,7 +35,7 @@
{#if container} {#if container}
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1} {@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
{@const dragDisabled = zIndex === 0 || $layoutState.currentSelection.length > 2 || !$uiState.uiUnlocked} {@const dragDisabled = zIndex === 0 || $selectionState.currentSelection.length > 2 || !$uiState.uiUnlocked}
{#key $attrsChanged} {#key $attrsChanged}
{#if edit || !isHidden(container)} {#if edit || !isHidden(container)}
{#if container.attrs.variant === "tabs"} {#if container.attrs.variant === "tabs"}

View File

@@ -2,6 +2,7 @@
import { Block, BlockTitle } from "@gradio/atoms"; import { Block, BlockTitle } from "@gradio/atoms";
import { Tabs, TabItem } from "@gradio/tabs"; import { Tabs, TabItem } from "@gradio/tabs";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import selectionState from "$lib/stores/selectionState";
import WidgetContainer from "./WidgetContainer.svelte" import WidgetContainer from "./WidgetContainer.svelte"
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action'; import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
@@ -68,9 +69,10 @@
</script> </script>
{#if container && children} {#if container && children}
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)}
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}" <div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
class:hide-block={container.attrs.containerVariant === "hidden"} class:hide-block={container.attrs.containerVariant === "hidden"}
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)} class:selected
class:root-container={zIndex === 0} class:root-container={zIndex === 0}
class:is-executing={container.isNodeExecuting} class:is-executing={container.isNodeExecuting}
class:edit={edit}> class:edit={edit}>
@@ -133,6 +135,10 @@
.container { .container {
display: flex; display: flex;
&.selected {
background: var(--comfy-container-selected-background-fill) !important;
}
> :global(*) { > :global(*) {
border-radius: 0; border-radius: 0;
} }

View File

@@ -3,6 +3,7 @@
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import selectionState from "$lib/stores/selectionState";
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"
@@ -67,21 +68,24 @@
{:else if widget && widget.node} {:else if widget && widget.node}
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1} {@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
{@const hidden = isHidden(widget)} {@const hidden = isHidden(widget)}
{@const hovered = $uiState.uiUnlocked && $selectionState.currentHovered.has(widget.id)}
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(widget.id)}
{#key $attrsChanged} {#key $attrsChanged}
{#key $propsChanged} {#key $propsChanged}
<div class="widget {widget.attrs.classes} {getWidgetClass()}" <div class="widget {widget.attrs.classes} {getWidgetClass()}"
class:edit={edit} class:edit={edit}
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(widget.id)} class:hovered
class:is-executing={$queueState.runningNodeID && $queueState.runningNodeId == widget.node.id} class:selected
class:hidden={hidden} class:is-executing={$queueState.runningNodeID && $queueState.runningNodeID == widget.node.id}
> class:hidden={hidden}
>
<svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} /> <svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} />
</div> </div>
{#if hidden && edit} {#if hidden && edit}
<div class="handle handle-hidden" class:hidden={!edit} /> <div class="handle handle-hidden" class:hidden={!edit} />
{/if} {/if}
{#if showHandles} {#if showHandles || hovered}
<div class="handle handle-widget" data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/> <div class="handle handle-widget" class:hovered data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
{/if} {/if}
{/key} {/key}
{/key} {/key}
@@ -92,12 +96,9 @@
height: 100%; height: 100%;
&.selected { &.selected {
background: var(--color-yellow-200); background: var(--comfy-widget-selected-background-fill);
} }
} }
.container.selected {
background: var(--color-yellow-400);
}
.is-executing { .is-executing {
border: 3px dashed var(--color-green-600) !important; border: 3px dashed var(--color-green-600) !important;
@@ -123,8 +124,10 @@
background-color: #40404080; background-color: #40404080;
} }
.handle-widget:hover { .handle-widget {
background-color: #add8e680; &:hover, &.hovered {
background-color: #add8e680;
}
} }
.node-type { .node-type {

View File

@@ -1,5 +1,7 @@
import ComfyGraph from '$lib/ComfyGraph'; import ComfyGraph from '$lib/ComfyGraph';
import { LGraphCanvas, LiteGraph, Subgraph } from '@litegraph-ts/core'; import { LGraphCanvas, LiteGraph, Subgraph } from '@litegraph-ts/core';
import layoutState from './stores/layoutState';
import { get } from 'svelte/store';
export function configureLitegraph(isMobile: boolean = false) { export function configureLitegraph(isMobile: boolean = false) {
LiteGraph.catch_exceptions = false; LiteGraph.catch_exceptions = false;
@@ -16,4 +18,5 @@ export function configureLitegraph(isMobile: boolean = false) {
(window as any).LiteGraph = LiteGraph; (window as any).LiteGraph = LiteGraph;
(window as any).LGraphCanvas = LGraphCanvas; (window as any).LGraphCanvas = LGraphCanvas;
(window as any).layoutState = get(layoutState)
} }

View File

@@ -115,15 +115,15 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
} }
addPropertyAsOutput(propertyName: string, type: string) { addPropertyAsOutput(propertyName: string, type: string) {
if (this.shownOutputProperties[propertyName]) if (this.shownOutputProperties["@" + propertyName])
return; return;
if (!(propertyName in this.properties)) { if (!(propertyName in this.properties)) {
throw `No property named ${propertyName} found!` throw `No property named ${propertyName} found!`
} }
this.shownOutputProperties[propertyName] = { type, index: this.outputs.length } this.shownOutputProperties["@" + propertyName] = { type, index: this.outputs.length }
this.addOutput(propertyName, type) this.addOutput("@" + propertyName, type)
} }
formatValue(value: any): string { formatValue(value: any): string {

View File

@@ -1,11 +1,12 @@
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 type ComfyApp from "$lib/components/ComfyApp" import type ComfyApp from "$lib/components/ComfyApp"
import { type LGraphNode, type IWidget, type LGraph, NodeMode, type LGraphRemoveNodeOptions, type LGraphAddNodeOptions, type UUID } from "@litegraph-ts/core" import { type LGraphNode, type IWidget, type LGraph, NodeMode, type LGraphRemoveNodeOptions, type LGraphAddNodeOptions, type UUID, type NodeID } from "@litegraph-ts/core"
import { SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action'; import { SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { ComfyWidgetNode } from '$lib/nodes'; import type { ComfyWidgetNode } from '$lib/nodes';
import type { ComfyNodeID } from '$lib/api'; import type { ComfyNodeID } from '$lib/api';
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import IComfyInputSlot from '$lib/IComfyInputSlot';
type DragItemEntry = { type DragItemEntry = {
/* /*
@@ -62,16 +63,6 @@ export type LayoutState = {
*/ */
allItemsByNode: Record<ComfyNodeID, DragItemEntry>, allItemsByNode: Record<ComfyNodeID, DragItemEntry>,
/*
* Selected drag items.
*/
currentSelection: DragItemID[],
/*
* Selected LGraphNodes inside the litegraph canvas.
*/
currentSelectionNodes: LGraphNode[],
/* /*
* If true, a saved workflow is being deserialized, so ignore any * If true, a saved workflow is being deserialized, so ignore any
* nodeAdded/nodeRemoved events. * nodeAdded/nodeRemoved events.
@@ -660,9 +651,8 @@ type LayoutStateOps = {
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[], updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
nodeAdded: (node: LGraphNode, options: LGraphAddNodeOptions) => void, nodeAdded: (node: LGraphNode, options: LGraphAddNodeOptions) => void,
nodeRemoved: (node: LGraphNode, options: LGraphRemoveNodeOptions) => void, nodeRemoved: (node: LGraphNode, options: LGraphRemoveNodeOptions) => void,
groupItems: (dragItems: IDragItem[], attrs?: Partial<Attributes>) => ContainerLayout, groupItems: (dragItemIDs: DragItemID[], attrs?: Partial<Attributes>) => ContainerLayout,
ungroup: (container: ContainerLayout) => void, ungroup: (container: ContainerLayout) => void,
getCurrentSelection: () => IDragItem[],
findLayoutEntryForNode: (nodeId: ComfyNodeID) => DragItemEntry | null, findLayoutEntryForNode: (nodeId: ComfyNodeID) => DragItemEntry | null,
findLayoutForNode: (nodeId: ComfyNodeID) => IDragItem | null, findLayoutForNode: (nodeId: ComfyNodeID) => IDragItem | null,
serialize: () => SerializedLayoutState, serialize: () => SerializedLayoutState,
@@ -676,8 +666,6 @@ const store: Writable<LayoutState> = writable({
root: null, root: null,
allItems: {}, allItems: {},
allItemsByNode: {}, allItemsByNode: {},
currentSelection: [],
currentSelectionNodes: [],
isMenuOpen: false, isMenuOpen: false,
isConfiguring: true, isConfiguring: true,
refreshPropsPanel: writable(0), refreshPropsPanel: writable(0),
@@ -858,33 +846,29 @@ function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
store.set(state) store.set(state)
} }
function getCurrentSelection(): IDragItem[] { function groupItems(dragItemIDs: DragItemID[], attrs: Partial<Attributes> = {}): ContainerLayout {
const state = get(store) if (dragItemIDs.length === 0)
return state.currentSelection.map(id => state.allItems[id].dragItem)
}
function groupItems(dragItems: IDragItem[], attrs: Partial<Attributes> = {}): ContainerLayout {
if (dragItems.length === 0)
return; return;
const state = get(store) const state = get(store)
const parent = state.allItems[dragItems[0].id].parent || findDefaultContainerForInsertion(); const parent = state.allItems[dragItemIDs[0]].parent || findDefaultContainerForInsertion();
if (parent === null || parent.type !== "container") if (parent === null || parent.type !== "container")
return; return;
let index = undefined; let index = undefined;
if (parent) { if (parent) {
const indexFound = state.allItems[parent.id].children.findIndex(c => c.id === dragItems[0].id) const indexFound = state.allItems[parent.id].children.findIndex(c => c.id === dragItemIDs[0])
if (indexFound !== -1) if (indexFound !== -1)
index = indexFound index = indexFound
} }
const title = dragItems.length <= 1 ? "" : "Group"; const title = dragItemIDs.length <= 1 ? "" : "Group";
const container = addContainer(parent as ContainerLayout, { title, ...attrs }, index) const container = addContainer(parent as ContainerLayout, { title, ...attrs }, index)
for (const item of dragItems) { for (const itemID of dragItemIDs) {
const item = state.allItems[itemID].dragItem;
moveItem(item, container) moveItem(item, container)
} }
@@ -946,8 +930,6 @@ function initDefaultLayout() {
root: null, root: null,
allItems: {}, allItems: {},
allItemsByNode: {}, allItemsByNode: {},
currentSelection: [],
currentSelectionNodes: [],
isMenuOpen: false, isMenuOpen: false,
isConfiguring: false, isConfiguring: false,
refreshPropsPanel: writable(0), refreshPropsPanel: writable(0),
@@ -1061,8 +1043,6 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
root, root,
allItems, allItems,
allItemsByNode, allItemsByNode,
currentSelection: [],
currentSelectionNodes: [],
isMenuOpen: false, isMenuOpen: false,
isConfiguring: false, isConfiguring: false,
refreshPropsPanel: writable(0), refreshPropsPanel: writable(0),
@@ -1093,7 +1073,6 @@ const layoutStateStore: WritableLayoutStateStore =
updateChildren, updateChildren,
nodeAdded, nodeAdded,
nodeRemoved, nodeRemoved,
getCurrentSelection,
groupItems, groupItems,
findLayoutEntryForNode, findLayoutEntryForNode,
findLayoutForNode, findLayoutForNode,

View File

@@ -0,0 +1,57 @@
import { writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import type { DragItemID, IDragItem } from './layoutState';
import type { LGraphNode, NodeID } from '@litegraph-ts/core';
export type SelectionState = {
/*
* Selected drag items.
* NOTE: Order is important, for node grouping actions.
*/
currentSelection: DragItemID[],
/*
* Hovered drag items.
*/
currentHovered: Set<DragItemID>,
/*
* Selected LGraphNodes inside the litegraph canvas.
* NOTE: Order is important, for node grouping actions.
*/
currentSelectionNodes: LGraphNode[],
/*
* Currently hovered nodes.
*/
currentHoveredNodes: Set<NodeID>
}
type SelectionStateOps = {
clear: () => void,
}
export type WritableSelectionStateStore = Writable<SelectionState> & SelectionStateOps;
const store: Writable<SelectionState> = writable(
{
currentSelection: [],
currentSelectionNodes: [],
currentHovered: new Set(),
currentHoveredNodes: new Set(),
})
function clear() {
store.set({
currentSelection: [],
currentSelectionNodes: [],
currentHovered: new Set(),
currentHoveredNodes: new Set(),
})
}
const uiStateStore: WritableSelectionStateStore =
{
...store,
clear
}
export default uiStateStore;

View File

@@ -3,7 +3,8 @@ import ComboWidget from "$lib/widgets/ComboWidget.svelte";
import RangeWidget from "$lib/widgets/RangeWidget.svelte"; import RangeWidget from "$lib/widgets/RangeWidget.svelte";
import TextWidget from "$lib/widgets/TextWidget.svelte"; import TextWidget from "$lib/widgets/TextWidget.svelte";
import { get } from "svelte/store" import { get } from "svelte/store"
import layoutState from "$lib/stores/layoutState" import layoutState, { type WidgetLayout } from "$lib/stores/layoutState"
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 { ComfyExecutionResult, ComfyImageLocation } from "./nodes/ComfyWidgetNodes";
@@ -45,11 +46,12 @@ export function download(filename: string, text: string, type: string = "text/pl
export function startDrag(evt: MouseEvent) { export function startDrag(evt: MouseEvent) {
const dragItemId: string = evt.target.dataset["dragItemId"]; const dragItemId: string = evt.target.dataset["dragItemId"];
const ss = get(selectionState)
const ls = get(layoutState) const ls = get(layoutState)
if (evt.button !== 0) { if (evt.button !== 0) {
if (ls.currentSelection.length <= 1 && !ls.isMenuOpen) if (ss.currentSelection.length <= 1 && !ls.isMenuOpen)
ls.currentSelection = [dragItemId] ss.currentSelection = [dragItemId]
return; return;
} }
@@ -58,19 +60,29 @@ export function startDrag(evt: MouseEvent) {
console.debug("startDrag", item) console.debug("startDrag", item)
if (evt.ctrlKey) { if (evt.ctrlKey) {
const index = ls.currentSelection.indexOf(item.id) const index = ss.currentSelection.indexOf(item.id)
if (index === -1) if (index === -1)
ls.currentSelection.push(item.id); ss.currentSelection.push(item.id);
else else
ls.currentSelection.splice(index, 1); ss.currentSelection.splice(index, 1);
ls.currentSelection = ls.currentSelection; ss.currentSelection = ss.currentSelection;
} }
else { else {
ls.currentSelection = [item.id] ss.currentSelection = [item.id]
}
ss.currentSelectionNodes = [];
for (const id of ss.currentSelection) {
const item = ls.allItems[id].dragItem
if (item.type === "widget") {
const node = (item as WidgetLayout).node;
if (node) {
ss.currentSelectionNodes.push(node)
}
}
} }
ls.currentSelectionNodes = [];
layoutState.set(ls) layoutState.set(ls)
selectionState.set(ss)
}; };
export function stopDrag(evt: MouseEvent) { export function stopDrag(evt: MouseEvent) {

View File

@@ -15,6 +15,10 @@ body {
--color-blue-500: #3985f5; --color-blue-500: #3985f5;
--comfy-accent-soft: var(--neutral-300); --comfy-accent-soft: var(--neutral-300);
--comfy-widget-selected-background-fill: var(--color-yellow-100);
--comfy-widget-hovered-background-fill: var(--secondary-200);
--comfy-container-selected-background-fill: var(--color-yellow-300);
--comfy-container-hovered-background-fill: var(--secondary-300);
--comfy-disabled-label-color: var(--neutral-400); --comfy-disabled-label-color: var(--neutral-400);
--comfy-disabled-textbox-background-fill: var(--neutral-200); --comfy-disabled-textbox-background-fill: var(--neutral-200);
--comfy-disabled-textbox-border-color: var(--neutral-300); --comfy-disabled-textbox-border-color: var(--neutral-300);
@@ -38,6 +42,10 @@ body {
color-scheme: dark; color-scheme: dark;
--comfy-accent-soft: var(--neutral-600); --comfy-accent-soft: var(--neutral-600);
--comfy-widget-selected-background-fill: var(--primary-500);
--comfy-widget-hovered-background-fill: var(--neutral-600);
--comfy-container-selected-background-fill: var(--primary-700);
--comfy-container-hovered-background-fill: var(--neutral-500);
--comfy-disabled-label-color: var(--neutral-500); --comfy-disabled-label-color: var(--neutral-500);
--comfy-disabled-textbox-background-fill: var(--neutral-800); --comfy-disabled-textbox-background-fill: var(--neutral-800);
--comfy-disabled-textbox-border-color: var(--neutral-700); --comfy-disabled-textbox-border-color: var(--neutral-700);