Two-way selection
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
57
src/lib/stores/selectionState.ts
Normal file
57
src/lib/stores/selectionState.ts
Normal 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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user