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) {
console.debug("[ComfyGraph] AutoAdd UI")
const comfyNode = node as ComfyGraphNode;
const widgetNodesAdded = []
const widgetNodesAdded: ComfyWidgetNode[] = []
for (let index = 0; index < comfyNode.inputs.length; index++) {
const input = comfyNode.inputs[index];
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)
console.debug("[ComfyGraph] Group new widgets", dragItems)
const dragItemIDs = widgetNodesAdded.map(wn => get(layoutState).allItemsByNode[wn.id]?.dragItem?.id).filter(Boolean)
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 queueState from "./stores/queueState";
import { get } from "svelte/store";
import { get, type Unsubscriber } from "svelte/store";
import uiState from "./stores/uiState";
import layoutState from "./stores/layoutState";
import { Watch } from "@litegraph-ts/nodes-basic";
import { ComfyReroute } from "./nodes";
import type { Progress } from "./components/ComfyApp";
import selectionState from "./stores/selectionState";
export type SerializedGraphCanvasState = {
offset: Vector2,
@@ -14,6 +16,7 @@ export type SerializedGraphCanvasState = {
export default class ComfyGraphCanvas extends LGraphCanvas {
app: ComfyApp | null;
private _unsubscribe: Unsubscriber;
constructor(
app: ComfyApp,
@@ -27,8 +30,22 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
) {
super(canvas, app.lGraph, options);
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 {
return {
offset: this.ds.offset,
@@ -58,51 +75,61 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
super.drawNodeShape(node, ctx, size, fgColor, bgColor, selected, mouseOver);
let state = get(queueState);
let ss = get(selectionState);
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";
// 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) {
const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE;
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;
this.drawNodeOutline(node, ctx, state.progress, size, fgColor, bgColor, color, thickness)
}
}
if (state.progress) {
ctx.fillStyle = "green";
ctx.fillRect(0, 0, size[0] * (state.progress.value / state.progress.max), 6);
ctx.fillStyle = bgColor;
}
private drawNodeOutline(node: LGraphNode, ctx: CanvasRenderingContext2D, progress?: Progress, size: Vector2, fgColor: string, bgColor: string, outlineColor: string, outlineThickness: number) {
const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE;
ctx.lineWidth = outlineThickness;
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>) {
const ls = get(layoutState)
ls.currentSelectionNodes = Object.values(nodes)
ls.currentSelection = []
layoutState.set(ls)
selectionState.update(ss => {
ss.currentSelectionNodes = Object.values(nodes)
ss.currentSelection = []
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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { Block, BlockTitle } from "@gradio/atoms";
import uiState from "$lib/stores/uiState";
import selectionState from "$lib/stores/selectionState";
import WidgetContainer from "./WidgetContainer.svelte"
import BlockContainer from "./BlockContainer.svelte"
import AccordionContainer from "./AccordionContainer.svelte"
@@ -34,7 +35,7 @@
{#if container}
{@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}
{#if edit || !isHidden(container)}
{#if container.attrs.variant === "tabs"}

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
import ComfyGraph from '$lib/ComfyGraph';
import { LGraphCanvas, LiteGraph, Subgraph } from '@litegraph-ts/core';
import layoutState from './stores/layoutState';
import { get } from 'svelte/store';
export function configureLitegraph(isMobile: boolean = false) {
LiteGraph.catch_exceptions = false;
@@ -16,4 +18,5 @@ export function configureLitegraph(isMobile: boolean = false) {
(window as any).LiteGraph = LiteGraph;
(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) {
if (this.shownOutputProperties[propertyName])
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)
this.shownOutputProperties["@" + propertyName] = { type, index: this.outputs.length }
this.addOutput("@" + propertyName, type)
}
formatValue(value: any): string {

View File

@@ -1,11 +1,12 @@
import { get, writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
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 type { ComfyWidgetNode } from '$lib/nodes';
import type { ComfyNodeID } from '$lib/api';
import { v4 as uuidv4 } from "uuid";
import IComfyInputSlot from '$lib/IComfyInputSlot';
type DragItemEntry = {
/*
@@ -62,16 +63,6 @@ export type LayoutState = {
*/
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
* nodeAdded/nodeRemoved events.
@@ -660,9 +651,8 @@ type LayoutStateOps = {
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
nodeAdded: (node: LGraphNode, options: LGraphAddNodeOptions) => 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,
getCurrentSelection: () => IDragItem[],
findLayoutEntryForNode: (nodeId: ComfyNodeID) => DragItemEntry | null,
findLayoutForNode: (nodeId: ComfyNodeID) => IDragItem | null,
serialize: () => SerializedLayoutState,
@@ -676,8 +666,6 @@ const store: Writable<LayoutState> = writable({
root: null,
allItems: {},
allItemsByNode: {},
currentSelection: [],
currentSelectionNodes: [],
isMenuOpen: false,
isConfiguring: true,
refreshPropsPanel: writable(0),
@@ -858,33 +846,29 @@ function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
store.set(state)
}
function getCurrentSelection(): IDragItem[] {
const state = get(store)
return state.currentSelection.map(id => state.allItems[id].dragItem)
}
function groupItems(dragItems: IDragItem[], attrs: Partial<Attributes> = {}): ContainerLayout {
if (dragItems.length === 0)
function groupItems(dragItemIDs: DragItemID[], attrs: Partial<Attributes> = {}): ContainerLayout {
if (dragItemIDs.length === 0)
return;
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")
return;
let index = undefined;
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)
index = indexFound
}
const title = dragItems.length <= 1 ? "" : "Group";
const title = dragItemIDs.length <= 1 ? "" : "Group";
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)
}
@@ -946,8 +930,6 @@ function initDefaultLayout() {
root: null,
allItems: {},
allItemsByNode: {},
currentSelection: [],
currentSelectionNodes: [],
isMenuOpen: false,
isConfiguring: false,
refreshPropsPanel: writable(0),
@@ -1061,8 +1043,6 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
root,
allItems,
allItemsByNode,
currentSelection: [],
currentSelectionNodes: [],
isMenuOpen: false,
isConfiguring: false,
refreshPropsPanel: writable(0),
@@ -1093,7 +1073,6 @@ const layoutStateStore: WritableLayoutStateStore =
updateChildren,
nodeAdded,
nodeRemoved,
getCurrentSelection,
groupItems,
findLayoutEntryForNode,
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 TextWidget from "$lib/widgets/TextWidget.svelte";
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 { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, GraphInput } from "@litegraph-ts/core";
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) {
const dragItemId: string = evt.target.dataset["dragItemId"];
const ss = get(selectionState)
const ls = get(layoutState)
if (evt.button !== 0) {
if (ls.currentSelection.length <= 1 && !ls.isMenuOpen)
ls.currentSelection = [dragItemId]
if (ss.currentSelection.length <= 1 && !ls.isMenuOpen)
ss.currentSelection = [dragItemId]
return;
}
@@ -58,19 +60,29 @@ export function startDrag(evt: MouseEvent) {
console.debug("startDrag", item)
if (evt.ctrlKey) {
const index = ls.currentSelection.indexOf(item.id)
const index = ss.currentSelection.indexOf(item.id)
if (index === -1)
ls.currentSelection.push(item.id);
ss.currentSelection.push(item.id);
else
ls.currentSelection.splice(index, 1);
ls.currentSelection = ls.currentSelection;
ss.currentSelection.splice(index, 1);
ss.currentSelection = ss.currentSelection;
}
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)
selectionState.set(ss)
};
export function stopDrag(evt: MouseEvent) {

View File

@@ -15,6 +15,10 @@ body {
--color-blue-500: #3985f5;
--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-textbox-background-fill: var(--neutral-200);
--comfy-disabled-textbox-border-color: var(--neutral-300);
@@ -38,6 +42,10 @@ body {
color-scheme: dark;
--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-textbox-background-fill: var(--neutral-800);
--comfy-disabled-textbox-border-color: var(--neutral-700);