Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 3 additions & 4 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
canvasStore.canvas to be initialized. -->
<template v-if="comfyAppReady">
<TitleEditor />
<SelectionOverlay v-if="selectionToolboxEnabled">
<SelectionToolbox />
</SelectionOverlay>
<!-- SelectionToolbox is now standalone, no parent overlay needed -->
<!-- LiteGraph already draws selection borders in canvas -->
<SelectionToolbox v-if="selectionToolboxEnabled" />
<DomWidgets />
</template>
</template>
Expand All @@ -55,7 +55,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.

47 changes: 17 additions & 30 deletions src/components/graph/SelectionToolbox.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<template>
<Panel
class="selection-toolbox absolute left-1/2 rounded-lg"
v-show="visible"
class="selection-toolbox rounded-lg z-40"
:class="{ 'animate-slide-up': shouldAnimate }"
:style="style"
:pt="{
header: 'hidden',
content: 'p-0 flex flex-row'
Expand All @@ -28,7 +30,7 @@

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

import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
Expand All @@ -41,23 +43,30 @@ 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 { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition'
import { useRetriggerableAnimation } from '@/composables/element/useRetriggerableAnimation'
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 }
)
// Get position and visibility from the new composable
const { style, visible } = useSelectionToolboxPosition()

// Track selection changes for animation
const selectionUpdateCount = ref(0)
watch(visible, () => {
selectionUpdateCount.value++
})

const { shouldAnimate } = useRetriggerableAnimation(selectionUpdateCount, {
animateOnMount: true
})

const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
const commandIds = new Set<string>(
Expand All @@ -75,25 +84,3 @@ const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
.filter((command): command is ComfyCommandImpl => command !== undefined)
})
</script>

<style scoped>
.selection-toolbox {
transform: translateX(-50%) 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;
}
}

.animate-slide-up {
animation: slideUp 0.3s ease-out;
}
</style>
141 changes: 141 additions & 0 deletions src/composables/canvas/useSelectionToolboxPosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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 })

// Visibility state
const visible = ref(false)

// Style for toolbox positioning
const style = ref<CSSProperties>({
position: 'fixed',
left: '0',
top: '0',
visibility: 'hidden'
})

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

if (!selectableItems.size) {
visible.value = false
style.value = {
...style.value,
visibility: 'hidden'
}
return
}

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

if (bounds) {
// Store world coordinates
// bounds = [x, y, width, height]
worldPosition.value = {
x: bounds[0] + bounds[2] / 2, // Center X of bounds
y: bounds[1], // Top Y of bounds
width: bounds[2],
height: bounds[3]
}

updateTransform()
}
}

/**
* Update transform based on canvas state
*/
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, and we subtract pixels to position above
const toolboxOffset = 45 // Pixels above the selection

style.value = {
position: 'fixed',
left: '0',
top: '0',
transform: `translate(${screenX}px, ${screenY - toolboxOffset}px) translateX(-50%)`,
visibility: 'visible',
willChange: 'transform'
}
}

// 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