Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@
canvasStore.canvas to be initialized. -->
<template v-if="comfyAppReady">
<TitleEditor />
<SelectionOverlay v-if="selectionToolboxEnabled">
<SelectionToolbox />
</SelectionOverlay>
<SelectionToolbox v-if="selectionToolboxEnabled" />
<DomWidgets />
</template>
</template>
Expand All @@ -55,7 +53,6 @@ import DomWidgets from '@/components/graph/DomWidgets.vue'
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
import MiniMap from '@/components/graph/MiniMap.vue'
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
import TitleEditor from '@/components/graph/TitleEditor.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
Expand Down
106 changes: 0 additions & 106 deletions src/components/graph/SelectionOverlay.vue

This file was deleted.

89 changes: 43 additions & 46 deletions src/components/graph/SelectionToolbox.vue
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
<template>
<Panel
class="selection-toolbox absolute left-1/2 rounded-lg"
:class="{ 'animate-slide-up': shouldAnimate }"
:pt="{
header: 'hidden',
content: 'p-0 flex flex-row'
}"
@wheel="canvasInteractions.handleWheel"
>
<ExecuteButton />
<ColorPickerButton />
<BypassButton />
<PinButton />
<Load3DViewerButton />
<MaskEditorButton />
<ConvertToSubgraphButton />
<DeleteButton />
<RefreshSelectionButton />
<ExtensionCommandButton
v-for="command in extensionToolboxCommands"
:key="command.id"
:command="command"
/>
<HelpButton />
</Panel>
<Transition name="slide-up">
<Panel
v-show="visible"
class="selection-toolbox fixed left-0 top-0 rounded-lg z-40"
:style="style"
:pt="{
header: 'hidden',
content: 'p-0 flex flex-row'
}"
@wheel="canvasInteractions.handleWheel"
>
<ExecuteButton />
<ColorPickerButton />
<BypassButton />
<PinButton />
<Load3DViewerButton />
<MaskEditorButton />
<ConvertToSubgraphButton />
<DeleteButton />
<RefreshSelectionButton />
<ExtensionCommandButton
v-for="command in extensionToolboxCommands"
:key="command.id"
:command="command"
/>
<HelpButton />
</Panel>
</Transition>
</template>

<script setup lang="ts">
import Panel from 'primevue/panel'
import { computed, inject } from 'vue'
import { computed } from 'vue'

import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
Expand All @@ -41,23 +44,18 @@ import Load3DViewerButton from '@/components/graph/selectionToolbox/Load3DViewer
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
import { useRetriggerableAnimation } from '@/composables/element/useRetriggerableAnimation'
import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import { useExtensionService } from '@/services/extensionService'
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
import { SelectionOverlayInjectionKey } from '@/types/selectionOverlayTypes'

const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const extensionService = useExtensionService()
const canvasInteractions = useCanvasInteractions()

const selectionOverlayState = inject(SelectionOverlayInjectionKey)
const { shouldAnimate } = useRetriggerableAnimation(
selectionOverlayState?.updateCount,
{ animateOnMount: true }
)
const { style, visible } = useSelectionToolboxPosition()

const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
const commandIds = new Set<string>(
Expand All @@ -78,22 +76,21 @@ const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {

<style scoped>
.selection-toolbox {
transform: translateX(-50%) translateY(-120%);
transform: translateY(-120%);
}

/* Slide up animation using CSS animation */
@keyframes slideUp {
from {
transform: translateX(-50%) translateY(-100%);
opacity: 0;
}
to {
transform: translateX(-50%) translateY(-120%);
opacity: 1;
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.3s ease-out;
}

.animate-slide-up {
animation: slideUp 0.3s ease-out;
.slide-up-enter-from {
transform: translateY(-100%);
opacity: 0;
}

.slide-up-leave-to {
transform: translateY(-100%);
opacity: 0;
}
</style>
122 changes: 122 additions & 0 deletions src/composables/canvas/useSelectionToolboxPosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { ref, watch } from 'vue'
import type { CSSProperties } from 'vue'

import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { createBounds } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/stores/graphStore'

/**
* Manages the position of the selection toolbox independently.
* Uses transform for all positioning to avoid layout.
*/
export function useSelectionToolboxPosition() {
const canvasStore = useCanvasStore()
const lgCanvas = canvasStore.getCanvas()
const { getSelectableItems } = useSelectedLiteGraphItems()

// World position of selection center
const worldPosition = ref({ x: 0, y: 0, width: 0, height: 0 })

const visible = ref(false)

const style = ref<CSSProperties>({})

/**
* Update position based on selection
*/
const updateSelectionBounds = () => {
const selectableItems = getSelectableItems()

if (!selectableItems.size) {
visible.value = false
return
}

visible.value = true
const bounds = createBounds(selectableItems)

if (!bounds) {
return
}

worldPosition.value = {
x: bounds[0] + bounds[2] / 2,
y: bounds[1],
width: bounds[2],
height: bounds[3]
}

updateTransform()
}

const updateTransform = () => {
if (!visible.value) return

const { scale, offset } = lgCanvas.ds
const canvasRect = lgCanvas.canvas.getBoundingClientRect()

// Transform world to screen coordinates
// Position toolbox at top-center of selection
const screenX =
(worldPosition.value.x + offset[0]) * scale + canvasRect.left
const screenY = (worldPosition.value.y + offset[1]) * scale + canvasRect.top

// Position the toolbox above the selection bounds
// The -50% centers it horizontally,
const toolboxOffset = 45

style.value = {
transform: `translate(${screenX}px, ${screenY - toolboxOffset}px) translateX(-50%)`
}
}

// Sync with canvas transform
const { startSync, stopSync } = useCanvasTransformSync(updateTransform, {
autoStart: false
})

// Watch for selection changes
watch(
() => canvasStore.getCanvas().state.selectionChanged,
(changed) => {
if (changed) {
updateSelectionBounds()
canvasStore.getCanvas().state.selectionChanged = false

// Start transform sync if we have selection
if (visible.value) {
startSync()
} else {
stopSync()
}
}
},
{ immediate: true }
)

// Watch for dragging state
watch(
() => canvasStore.canvas?.state?.draggingItems,
(dragging) => {
if (dragging) {
// Hide during node dragging
style.value = {
...style.value,
visibility: 'hidden'
}
} else if (visible.value) {
// Show after dragging ends
requestAnimationFrame(() => {
updateSelectionBounds()
})
}
}
)

return {
style,
visible,
updateSelectionBounds
}
}
1 change: 1 addition & 0 deletions src/extensions/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import './previewAny'
import './rerouteNode'
import './saveImageExtraOutput'
import './saveMesh'
import './selectionBorder'
import './simpleTouchSupport'
import './slotDefaults'
import './uploadAudio'
Expand Down
Loading
Loading