mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 14:45:36 +00:00
Compare commits
6 Commits
manager/lo
...
ft/filtere
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a35562d3d | ||
|
|
ceac8f3741 | ||
|
|
b1057f164b | ||
|
|
4a189bdc93 | ||
|
|
f0adb4c9d3 | ||
|
|
d5d0aa52c2 |
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 67 KiB |
@@ -10,7 +10,8 @@
|
||||
:class="{
|
||||
'flex items-center gap-1': isActive,
|
||||
'p-breadcrumb-item-link-menu-visible': menu?.overlayVisible,
|
||||
'p-breadcrumb-item-link-icon-visible': isActive
|
||||
'p-breadcrumb-item-link-icon-visible': isActive,
|
||||
'active-breadcrumb-item': isActive
|
||||
}"
|
||||
@click="handleClick"
|
||||
>
|
||||
@@ -111,21 +112,7 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
{
|
||||
label: t('g.rename'),
|
||||
icon: 'pi pi-pencil',
|
||||
command: async () => {
|
||||
let initialName =
|
||||
workflowStore.activeSubgraph?.name ??
|
||||
workflowStore.activeWorkflow?.filename
|
||||
|
||||
if (!initialName) return
|
||||
|
||||
const newName = await dialogService.prompt({
|
||||
title: t('g.rename'),
|
||||
message: t('breadcrumbsMenu.enterNewName'),
|
||||
defaultValue: initialName
|
||||
})
|
||||
|
||||
await rename(newName, initialName)
|
||||
}
|
||||
command: startRename
|
||||
},
|
||||
{
|
||||
label: t('breadcrumbsMenu.duplicate'),
|
||||
@@ -175,20 +162,24 @@ const handleClick = (event: MouseEvent) => {
|
||||
menu.value?.hide()
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
isEditing.value = true
|
||||
itemLabel.value = props.item.label as string
|
||||
void nextTick(() => {
|
||||
if (itemInputRef.value?.$el) {
|
||||
itemInputRef.value.$el.focus()
|
||||
itemInputRef.value.$el.select()
|
||||
if (wrapperRef.value) {
|
||||
itemInputRef.value.$el.style.width = `${Math.max(200, wrapperRef.value.offsetWidth)}px`
|
||||
}
|
||||
}
|
||||
})
|
||||
startRename()
|
||||
}
|
||||
}
|
||||
|
||||
const startRename = () => {
|
||||
isEditing.value = true
|
||||
itemLabel.value = props.item.label as string
|
||||
void nextTick(() => {
|
||||
if (itemInputRef.value?.$el) {
|
||||
itemInputRef.value.$el.focus()
|
||||
itemInputRef.value.$el.select()
|
||||
if (wrapperRef.value) {
|
||||
itemInputRef.value.$el.style.width = `${Math.max(200, wrapperRef.value.offsetWidth)}px`
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const inputBlur = async (doRename: boolean) => {
|
||||
if (doRename) {
|
||||
await rename(itemLabel.value, props.item.label as string)
|
||||
@@ -212,4 +203,8 @@ const inputBlur = async (doRename: boolean) => {
|
||||
.p-breadcrumb-item-label {
|
||||
@apply whitespace-nowrap text-ellipsis overflow-hidden;
|
||||
}
|
||||
|
||||
.active-breadcrumb-item {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex items-center justify-center"
|
||||
:style="{ width: size + 'px', height: size + 'px' }"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
class="animate-spin"
|
||||
:style="{ animationDuration: duration }"
|
||||
>
|
||||
<g clip-path="url(#clip0_776_9582)">
|
||||
<!-- Top dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7 2.21053C7.61042 2.21053 8.10526 1.71568 8.10526 1.10526C8.10526 0.494843 7.61042 0 7 0C6.38958 0 5.89474 0.494843 5.89474 1.10526C5.89474 1.71568 6.38958 2.21053 7 2.21053Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.25s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.21053 7C2.21053 7.61042 1.71568 8.10526 1.10526 8.10526C0.494843 8.10526 0 7.61042 0 7C0 6.38958 0.494843 5.89474 1.10526 5.89474C1.71568 5.89474 2.21053 6.38958 2.21053 7Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.5s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14 7C14 7.61042 13.5052 8.10526 12.8947 8.10526C12.2843 8.10526 11.7895 7.61042 11.7895 7C11.7895 6.38958 12.2843 5.89474 12.8947 5.89474C13.5052 5.89474 14 6.38958 14 7Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.75s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.10526 12.8947C8.10526 13.5052 7.61041 14 6.99999 14C6.38957 14 5.89473 13.5052 5.89473 12.8947C5.89473 12.2843 6.38957 11.7895 6.99999 11.7895C7.61041 11.7895 8.10526 12.2843 8.10526 12.8947Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Top-left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.125s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.05039 3.61349C2.48203 4.04513 3.18184 4.04513 3.61347 3.61349C4.0451 3.18186 4.0451 2.48205 3.61347 2.05042C3.18184 1.61878 2.48203 1.61878 2.05039 2.05042C1.61876 2.48205 1.61876 3.18186 2.05039 3.61349Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom-right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.625s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.9496 11.9496C11.518 12.3812 10.8182 12.3812 10.3865 11.9496C9.9549 11.5179 9.9549 10.8181 10.3865 10.3865C10.8182 9.95485 11.518 9.95485 11.9496 10.3865C12.3812 10.8181 12.3812 11.5179 11.9496 11.9496Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom-left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.875s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.05039 11.9496C2.48203 12.3812 3.18184 12.3812 3.61347 11.9496C4.0451 11.5179 4.0451 10.8181 3.61347 10.3865C3.18184 9.95485 2.48203 9.95485 2.05039 10.3865C1.61876 10.8181 1.61876 11.5179 2.05039 11.9496Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Top-right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.375s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.9496 3.61349C11.518 4.04513 10.8182 4.04513 10.3865 3.61349C9.9549 3.18186 9.9549 2.48205 10.3865 2.05042C10.8182 1.61878 11.518 1.61878 11.9496 2.05042C12.3812 2.48205 12.3812 3.18186 11.9496 3.61349Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_776_9582">
|
||||
<rect width="14" height="14" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
const { size = 24, duration = '2s' } = defineProps<{
|
||||
size?: number
|
||||
duration?: string
|
||||
}>()
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const color = computed(() =>
|
||||
colorPaletteStore.completedActivePalette.light_theme ? '#2C2B30' : '#D4D4D4'
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dot-animation {
|
||||
animation: dot-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes dot-pulse {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
40% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
15
src/components/custom/button/IconButton.vue
Normal file
15
src/components/custom/button/IconButton.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<button
|
||||
class="flex justify-center items-center outline-none border-none p-0 bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white w-8 h-8 rounded-lg cursor-pointer"
|
||||
role="button"
|
||||
@click="onClick"
|
||||
>
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { onClick } = defineProps<{
|
||||
onClick: () => void
|
||||
}>()
|
||||
</script>
|
||||
67
src/components/custom/widget/ModelSelector.vue
Normal file
67
src/components/custom/widget/ModelSelector.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<BaseWidgetLayout>
|
||||
<template #leftPanel>
|
||||
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation">
|
||||
<template #header-icon>
|
||||
<i-lucide:puzzle class="text-neutral" />
|
||||
</template>
|
||||
<template #header-title>
|
||||
<span class="text-neutral text-base">{{ t('g.title') }}</span>
|
||||
</template>
|
||||
</LeftSidePanel>
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
<!-- here -->
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<!-- here -->
|
||||
</template>
|
||||
|
||||
<template #rightPanel>
|
||||
<RightSidePanel></RightSidePanel>
|
||||
</template>
|
||||
</BaseWidgetLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { provide, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { NavGroupData, NavItemData } from '@/types/custom_components/navTypes'
|
||||
import { OnCloseKey } from '@/types/custom_components/widgetTypes'
|
||||
|
||||
import BaseWidgetLayout from './layout/BaseWidgetLayout.vue'
|
||||
import LeftSidePanel from './panel/LeftSidePanel.vue'
|
||||
import RightSidePanel from './panel/RightSidePanel.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { onClose } = defineProps<{
|
||||
onClose: () => void
|
||||
}>()
|
||||
|
||||
provide(OnCloseKey, onClose)
|
||||
|
||||
const tempNavigation = ref<(NavItemData | NavGroupData)[]>([
|
||||
{ id: 'installed', label: 'Installed' },
|
||||
{
|
||||
title: 'TAGS',
|
||||
items: [
|
||||
{ id: 'tag-sd15', label: 'SD 1.5' },
|
||||
{ id: 'tag-sdxl', label: 'SDXL' },
|
||||
{ id: 'tag-utility', label: 'Utility' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'CATEGORIES',
|
||||
items: [
|
||||
{ id: 'cat-models', label: 'Models' },
|
||||
{ id: 'cat-nodes', label: 'Nodes' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const selectedNavItem = ref<string | null>('installed')
|
||||
</script>
|
||||
176
src/components/custom/widget/layout/BaseWidgetLayout.vue
Normal file
176
src/components/custom/widget/layout/BaseWidgetLayout.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div
|
||||
class="base-widget-layout rounded-2xl overflow-hidden relative bg-zinc-100 dark-theme:bg-zinc-800"
|
||||
>
|
||||
<IconButton
|
||||
v-show="!isRightPanelOpen && hasRightPanel"
|
||||
class="absolute top-4 right-16 z-10 transition-opacity duration-200"
|
||||
:class="{
|
||||
'opacity-0 pointer-events-none': isRightPanelOpen || !hasRightPanel
|
||||
}"
|
||||
@click="toggleRightPanel"
|
||||
>
|
||||
<i-lucide:panel-right class="text-sm" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
class="absolute top-4 right-6 z-10 transition-opacity duration-200"
|
||||
@click="closeDialog"
|
||||
>
|
||||
<i class="pi pi-times text-sm"></i>
|
||||
</IconButton>
|
||||
<div class="flex w-full h-full">
|
||||
<Transition name="slide-panel">
|
||||
<nav
|
||||
v-if="$slots.leftPanel && showLeftPanel"
|
||||
:class="[
|
||||
PANEL_SIZES.width,
|
||||
PANEL_SIZES.minWidth,
|
||||
PANEL_SIZES.maxWidth
|
||||
]"
|
||||
>
|
||||
<slot name="leftPanel"></slot>
|
||||
</nav>
|
||||
</Transition>
|
||||
|
||||
<div class="flex-1 flex bg-zinc-100 dark-theme:bg-neutral-900">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<header
|
||||
v-if="$slots.header"
|
||||
class="w-full h-16 px-6 py-4 flex justify-between gap-2"
|
||||
>
|
||||
<div class="flex-1 flex gap-2 flex-shrink-0">
|
||||
<IconButton v-if="!notMobile" @click="toggleLeftPanel">
|
||||
<i-lucide:panel-left v-if="!showLeftPanel" class="text-sm" />
|
||||
<i-lucide:panel-left-close v-else class="text-sm" />
|
||||
</IconButton>
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 min-w-20">
|
||||
<slot name="header-right-area"></slot>
|
||||
<IconButton
|
||||
v-if="isRightPanelOpen && hasRightPanel"
|
||||
@click="toggleRightPanel"
|
||||
>
|
||||
<i-lucide:panel-right-close class="text-sm" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex-1">
|
||||
<slot name="content"></slot>
|
||||
</main>
|
||||
</div>
|
||||
<Transition name="slide-panel-right">
|
||||
<aside
|
||||
v-if="hasRightPanel && isRightPanelOpen"
|
||||
class="w-1/4 min-w-40 max-w-80"
|
||||
>
|
||||
<slot name="rightPanel"></slot>
|
||||
</aside>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useBreakpoints } from '@vueuse/core'
|
||||
import { computed, inject, ref, useSlots, watch } from 'vue'
|
||||
|
||||
import IconButton from '@/components/custom/button/IconButton.vue'
|
||||
import { OnCloseKey } from '@/types/custom_components/widgetTypes'
|
||||
|
||||
const BREAKPOINTS = { sm: 480 }
|
||||
const PANEL_SIZES = {
|
||||
width: 'w-1/3',
|
||||
minWidth: 'min-w-40',
|
||||
maxWidth: 'max-w-56'
|
||||
}
|
||||
|
||||
const slots = useSlots()
|
||||
const closeDialog = inject(OnCloseKey, () => {})
|
||||
|
||||
const breakpoints = useBreakpoints(BREAKPOINTS)
|
||||
const notMobile = breakpoints.greater('sm')
|
||||
|
||||
const isLeftPanelOpen = ref<boolean>(true)
|
||||
const isRightPanelOpen = ref<boolean>(false)
|
||||
const mobileMenuOpen = ref<boolean>(false)
|
||||
|
||||
const hasRightPanel = computed(() => !!slots.rightPanel)
|
||||
|
||||
watch(notMobile, (isDesktop) => {
|
||||
if (!isDesktop) {
|
||||
mobileMenuOpen.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const showLeftPanel = computed(() => {
|
||||
const shouldShow = notMobile.value
|
||||
? isLeftPanelOpen.value
|
||||
: mobileMenuOpen.value
|
||||
return shouldShow
|
||||
})
|
||||
|
||||
const toggleLeftPanel = () => {
|
||||
if (notMobile.value) {
|
||||
isLeftPanelOpen.value = !isLeftPanelOpen.value
|
||||
} else {
|
||||
mobileMenuOpen.value = !mobileMenuOpen.value
|
||||
}
|
||||
}
|
||||
|
||||
const toggleRightPanel = () => {
|
||||
isRightPanelOpen.value = !isRightPanelOpen.value
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.base-widget-layout {
|
||||
height: 80vh;
|
||||
width: 90vw;
|
||||
max-width: 1280px;
|
||||
aspect-ratio: 20/13;
|
||||
}
|
||||
|
||||
@media (min-width: 1450px) {
|
||||
.base-widget-layout {
|
||||
max-width: 1724px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fade transition for buttons */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Slide transition for left panel */
|
||||
.slide-panel-enter-active,
|
||||
.slide-panel-leave-active {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.slide-panel-enter-from,
|
||||
.slide-panel-leave-to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
/* Slide transition for right panel */
|
||||
.slide-panel-right-enter-active,
|
||||
.slide-panel-right-leave-active {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.slide-panel-right-enter-from,
|
||||
.slide-panel-right-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
</style>
|
||||
24
src/components/custom/widget/nav/NavItem.vue
Normal file
24
src/components/custom/widget/nav/NavItem.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center gap-2 px-4 py-2 text-xs rounded-md transition-colors cursor-pointer"
|
||||
:class="
|
||||
active
|
||||
? 'bg-neutral-100 dark-theme:bg-zinc-700 text-neutral'
|
||||
: 'text-neutral hover:bg-zinc-100 hover:dark-theme:bg-zinc-700/50'
|
||||
"
|
||||
role="button"
|
||||
@click="onClick"
|
||||
>
|
||||
<i-lucide:folder class="text-xs text-neutral" />
|
||||
<span>
|
||||
<slot></slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { active, onClick } = defineProps<{
|
||||
active?: boolean
|
||||
onClick: () => void
|
||||
}>()
|
||||
</script>
|
||||
13
src/components/custom/widget/nav/NavTitle.vue
Normal file
13
src/components/custom/widget/nav/NavTitle.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<h3
|
||||
class="m-0 px-3 py-0 pt-5 text-sm font-bold uppercase text-neutral-400 dark-theme:text-neutral-400"
|
||||
>
|
||||
{{ title }}
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { title } = defineProps<{
|
||||
title: string
|
||||
}>()
|
||||
</script>
|
||||
75
src/components/custom/widget/panel/LeftSidePanel.vue
Normal file
75
src/components/custom/widget/panel/LeftSidePanel.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full w-full bg-white dark-theme:bg-zinc-800">
|
||||
<PanelHeader>
|
||||
<template #icon>
|
||||
<slot name="header-icon"></slot>
|
||||
</template>
|
||||
<slot name="header-title"></slot>
|
||||
</PanelHeader>
|
||||
|
||||
<nav class="flex-1 px-3 py-4 flex flex-col gap-2">
|
||||
<template v-for="(item, index) in navItems" :key="index">
|
||||
<div v-if="'items' in item" class="flex flex-col gap-2">
|
||||
<NavTitle :title="item.title" />
|
||||
<NavItem
|
||||
v-for="subItem in item.items"
|
||||
:key="subItem.id"
|
||||
:active="activeItem === subItem.id"
|
||||
@click="activeItem = subItem.id"
|
||||
>
|
||||
{{ subItem.label }}
|
||||
</NavItem>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<NavItem
|
||||
:active="activeItem === item.id"
|
||||
@click="activeItem = item.id"
|
||||
>
|
||||
{{ item.label }}
|
||||
</NavItem>
|
||||
</div>
|
||||
</template>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { NavGroupData, NavItemData } from '@/types/custom_components/navTypes'
|
||||
|
||||
import NavItem from '../nav/NavItem.vue'
|
||||
import NavTitle from '../nav/NavTitle.vue'
|
||||
import PanelHeader from './PanelHeader.vue'
|
||||
|
||||
const { navItems = [], modelValue } = defineProps<{
|
||||
navItems?: (NavItemData | NavGroupData)[]
|
||||
modelValue?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | null]
|
||||
}>()
|
||||
|
||||
const getFirstItemId = () => {
|
||||
if (!navItems || navItems.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const firstEntry = navItems[0]
|
||||
|
||||
if ('items' in firstEntry && firstEntry.items.length > 0) {
|
||||
return firstEntry.items[0].id
|
||||
}
|
||||
if ('id' in firstEntry) {
|
||||
return firstEntry.id
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const activeItem = computed({
|
||||
get: () => modelValue ?? getFirstItemId(),
|
||||
set: (value: string | null) => emit('update:modelValue', value)
|
||||
})
|
||||
</script>
|
||||
12
src/components/custom/widget/panel/PanelHeader.vue
Normal file
12
src/components/custom/widget/panel/PanelHeader.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<header class="flex items-center justify-between h-16 px-6">
|
||||
<div class="flex items-center gap-2 pl-1">
|
||||
<slot name="icon">
|
||||
<i-lucide:puzzle class="text-neutral text-base" />
|
||||
</slot>
|
||||
<h2 class="font-bold text-base text-neutral">
|
||||
<slot></slot>
|
||||
</h2>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
5
src/components/custom/widget/panel/RightSidePanel.vue
Normal file
5
src/components/custom/widget/panel/RightSidePanel.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="w-full h-full pl-4 pr-6 pb-8 bg-white dark-theme:bg-zinc-800">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -10,14 +10,16 @@
|
||||
:aria-labelledby="item.key"
|
||||
>
|
||||
<template #header>
|
||||
<component
|
||||
:is="item.headerComponent"
|
||||
v-if="item.headerComponent"
|
||||
:id="item.key"
|
||||
/>
|
||||
<h3 v-else :id="item.key">
|
||||
{{ item.title || ' ' }}
|
||||
</h3>
|
||||
<div v-if="!item.dialogComponentProps?.headless">
|
||||
<component
|
||||
:is="item.headerComponent"
|
||||
v-if="item.headerComponent"
|
||||
:id="item.key"
|
||||
/>
|
||||
<h3 v-else :id="item.key">
|
||||
{{ item.title || ' ' }}
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<component
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</ListBox>
|
||||
<div v-if="!isLegacyManager" class="flex justify-end py-3">
|
||||
<div v-if="isManagerInstalled" class="flex justify-end py-3">
|
||||
<PackInstallButton
|
||||
:disabled="isLoading || !!error || missingNodePacks.length === 0"
|
||||
:node-packs="missingNodePacks"
|
||||
@@ -45,12 +45,14 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
@@ -58,11 +60,22 @@ const props = defineProps<{
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
}>()
|
||||
|
||||
const aboutPanelStore = useAboutPanelStore()
|
||||
|
||||
// Get missing node packs from workflow with loading and error states
|
||||
const { missingNodePacks, isLoading, error, missingCoreNodes } =
|
||||
useMissingNodes()
|
||||
|
||||
const isLegacyManager = ref(false)
|
||||
// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
|
||||
// This allows us to conditionally show the Manager button only when the extension is available
|
||||
// TODO: Remove this check when Manager functionality is fully migrated into core
|
||||
const isManagerInstalled = computed(() => {
|
||||
return aboutPanelStore.badges.some(
|
||||
(badge) =>
|
||||
badge.label.includes('ComfyUI-Manager') ||
|
||||
badge.url.includes('ComfyUI-Manager')
|
||||
)
|
||||
})
|
||||
|
||||
const uniqueNodes = computed(() => {
|
||||
const seenTypes = new Set()
|
||||
@@ -90,13 +103,6 @@ const openManager = () => {
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const isLegacyResponse = await useComfyManagerService().isLegacyManagerUI()
|
||||
if (isLegacyResponse?.is_legacy_manager_ui) {
|
||||
isLegacyManager.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tag from 'primevue/tag'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
|
||||
import ManagerHeader from './ManagerHeader.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: enMessages
|
||||
}
|
||||
})
|
||||
|
||||
describe('ManagerHeader', () => {
|
||||
const createWrapper = () => {
|
||||
return mount(ManagerHeader, {
|
||||
global: {
|
||||
plugins: [createPinia(), PrimeVue, i18n],
|
||||
directives: {
|
||||
tooltip: Tooltip
|
||||
},
|
||||
components: {
|
||||
Tag
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('renders the component title', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('h2').text()).toBe(
|
||||
enMessages.manager.discoverCommunityContent
|
||||
)
|
||||
})
|
||||
|
||||
it('displays the legacy manager UI tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const tag = wrapper.find('[data-pc-name="tag"]')
|
||||
expect(tag.exists()).toBe(true)
|
||||
expect(tag.text()).toContain(enMessages.manager.legacyManagerUI)
|
||||
})
|
||||
|
||||
it('applies info severity to the tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const tag = wrapper.find('[data-pc-name="tag"]')
|
||||
expect(tag.classes()).toContain('p-tag-info')
|
||||
})
|
||||
|
||||
it('displays info icon in the tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const icon = wrapper.find('.pi-info-circle')
|
||||
expect(icon.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has cursor-help class on the tag', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const tag = wrapper.find('[data-pc-name="tag"]')
|
||||
expect(tag.classes()).toContain('cursor-help')
|
||||
})
|
||||
|
||||
it('has proper structure with flex container', () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const flexContainer = wrapper.find('.flex.justify-end.ml-auto.pr-4')
|
||||
expect(flexContainer.exists()).toBe(true)
|
||||
|
||||
const tag = flexContainer.find('[data-pc-name="tag"]')
|
||||
expect(tag.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -4,22 +4,6 @@
|
||||
<h2 class="text-lg font-normal text-left">
|
||||
{{ $t('manager.discoverCommunityContent') }}
|
||||
</h2>
|
||||
<div class="flex justify-end ml-auto pr-4">
|
||||
<Tag
|
||||
v-tooltip.left="$t('manager.legacyManagerUIDescription')"
|
||||
severity="info"
|
||||
icon="pi pi-info-circle"
|
||||
:value="$t('manager.legacyManagerUI')"
|
||||
class="cursor-help"
|
||||
:pt="{
|
||||
root: { class: 'text-xs' }
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag'
|
||||
</script>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
:loading="isInstalling"
|
||||
:loading-message="$t('g.installing')"
|
||||
@action="installAllPacks"
|
||||
@click="onClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -36,6 +37,10 @@ const { nodePacks, variant, label } = defineProps<{
|
||||
|
||||
const isInstalling = inject(IsInstallingKey, ref(false))
|
||||
|
||||
const onClick = (): void => {
|
||||
isInstalling.value = true
|
||||
}
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (installItem: NodePack) => {
|
||||
@@ -60,6 +65,8 @@ const installPack = (item: NodePack) =>
|
||||
const installAllPacks = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
|
||||
isInstalling.value = true
|
||||
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
|
||||
@@ -84,9 +84,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import Card from 'primevue/card'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, provide } from 'vue'
|
||||
import { computed, provide, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
|
||||
@@ -113,17 +114,18 @@ const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
|
||||
const { isPackInstalled, isPackEnabled, isPackInstalling } =
|
||||
useComfyManagerStore()
|
||||
|
||||
const isInstalling = computed(() => isPackInstalling(nodePack?.id))
|
||||
const isInstalling = ref(false)
|
||||
provide(IsInstallingKey, isInstalling)
|
||||
|
||||
const { isPackInstalled, isPackEnabled } = useComfyManagerStore()
|
||||
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
const isDisabled = computed(
|
||||
() => isInstalled.value && !isPackEnabled(nodePack?.id)
|
||||
)
|
||||
|
||||
whenever(isInstalled, () => (isInstalling.value = false))
|
||||
|
||||
const nodesCount = computed(() =>
|
||||
isMergedNodePack(nodePack) ? nodePack.comfy_nodes?.length : undefined
|
||||
)
|
||||
|
||||
@@ -1,47 +1,40 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full px-6 py-2 shadow-lg flex items-center justify-between"
|
||||
class="w-full px-6 py-4 shadow-lg flex items-center justify-between"
|
||||
:class="{
|
||||
'rounded-t-none': progressDialogContent.isExpanded,
|
||||
'rounded-lg': !progressDialogContent.isExpanded
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center text-base leading-none">
|
||||
<div class="justify-center text-sm font-bold leading-none">
|
||||
<div class="flex items-center">
|
||||
<!-- 1. Queue running (install/enable/disable etc.) -->
|
||||
<template v-if="isQueueRunning">
|
||||
<DotSpinner duration="1s" class="mr-2" />
|
||||
<template v-if="isInProgress">
|
||||
<i class="pi pi-spin pi-spinner mr-2 text-3xl" />
|
||||
<span>{{ currentTaskName }}</span>
|
||||
</template>
|
||||
<!-- 3. Restarting -->
|
||||
<template v-else-if="isRestarting">
|
||||
<DotSpinner duration="1s" class="mr-2" />
|
||||
<span>{{ $t('manager.restartingBackend') }}</span>
|
||||
</template>
|
||||
<!-- 4. Restart completed -->
|
||||
<template v-else-if="isRestartCompleted">
|
||||
<span class="mr-2">🎉</span>
|
||||
<span>{{ $t('manager.extensionsSuccessfullyInstalled') }}</span>
|
||||
</template>
|
||||
<!-- 2. Tasks completed (waiting for restart) -->
|
||||
<template v-else>
|
||||
<span class="mr-2">✅</span>
|
||||
<span>
|
||||
{{ $t('manager.clickToFinishSetup') }}
|
||||
'{{ $t('manager.applyChanges') }}'
|
||||
{{ $t('manager.toFinishSetup') }}
|
||||
</span>
|
||||
<i class="pi pi-check-circle mr-2 text-green-500" />
|
||||
<span class="leading-none">{{
|
||||
$t('manager.restartToApplyChanges')
|
||||
}}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- 1. Queue running -->
|
||||
<template v-if="isQueueRunning">
|
||||
<span class="text-sm text-neutral-700 dark-theme:text-neutral-400">
|
||||
{{ completedTasksCount }} {{ $t('g.progressCountOf') }}
|
||||
{{ taskLogs }}
|
||||
</span>
|
||||
<span v-if="isInProgress" class="text-xs font-bold text-neutral-600">
|
||||
{{ comfyManagerStore.uncompletedCount }} {{ $t('g.progressCountOf') }}
|
||||
{{ comfyManagerStore.taskLogs.length }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
v-if="!isInProgress"
|
||||
rounded
|
||||
outlined
|
||||
class="px-4 py-2 rounded-md mr-4"
|
||||
@click="handleRestart"
|
||||
>
|
||||
{{ $t('g.restart') }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="
|
||||
progressDialogContent.isExpanded
|
||||
@@ -51,46 +44,20 @@
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="font-bold"
|
||||
severity="secondary"
|
||||
:aria-label="progressDialogContent.isExpanded ? 'Collapse' : 'Expand'"
|
||||
@click.stop="progressDialogContent.toggle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 2. Tasks completed (waiting for restart) -->
|
||||
<template v-else-if="!isRestarting && !isRestartCompleted">
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
text
|
||||
rounded
|
||||
outlined
|
||||
class="rounded-md border-2 px-3 text-neutral-600 border-neutral-900 hover:bg-neutral-100 dark-theme:bg-none dark-theme:text-white dark-theme:border-white dark-theme:hover:bg-neutral-700"
|
||||
@click="handleRestart"
|
||||
>
|
||||
{{ $t('manager.applyChanges') }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<!-- 3. Restarting -->
|
||||
<template v-else-if="isRestarting">
|
||||
<!-- No buttons during restart -->
|
||||
</template>
|
||||
|
||||
<!-- 4. Restart completed -->
|
||||
<template v-else-if="isRestartCompleted">
|
||||
<!-- No buttons after restart completed (auto-close after 3 seconds) -->
|
||||
</template>
|
||||
|
||||
<!-- Common: Close button -->
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="font-bold"
|
||||
severity="secondary"
|
||||
aria-label="Close"
|
||||
@click.stop="closeDialog"
|
||||
/>
|
||||
size="small"
|
||||
severity="secondary"
|
||||
aria-label="Close"
|
||||
@click.stop="closeDialog"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -98,10 +65,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
@@ -111,34 +77,19 @@ import {
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
// State management for restart process
|
||||
const isRestarting = ref<boolean>(false)
|
||||
const isRestartCompleted = ref<boolean>(false)
|
||||
|
||||
// Computed states
|
||||
const isQueueRunning = computed(() => comfyManagerStore.uncompletedCount > 0)
|
||||
const taskLogs = computed(() => comfyManagerStore.taskLogs.length)
|
||||
|
||||
const completedTasksCount = computed(() => {
|
||||
if (isQueueRunning.value && taskLogs.value > 0) {
|
||||
return taskLogs.value - 1
|
||||
}
|
||||
return taskLogs.value
|
||||
})
|
||||
const isInProgress = computed(() => comfyManagerStore.uncompletedCount > 0)
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
|
||||
}
|
||||
|
||||
const fallbackTaskName = t('manager.installingDependencies')
|
||||
const fallbackTaskName = t('g.installing')
|
||||
const currentTaskName = computed(() => {
|
||||
if (!comfyManagerStore.taskLogs.length) return fallbackTaskName
|
||||
const task = comfyManagerStore.taskLogs.at(-1)
|
||||
@@ -146,52 +97,21 @@ const currentTaskName = computed(() => {
|
||||
})
|
||||
|
||||
const handleRestart = async () => {
|
||||
// Store original toast setting value
|
||||
const originalToastSetting = settingStore.get(
|
||||
'Comfy.Toast.DisableReconnectingToast'
|
||||
)
|
||||
const onReconnect = async () => {
|
||||
// Refresh manager state
|
||||
|
||||
try {
|
||||
await settingStore.set('Comfy.Toast.DisableReconnectingToast', true)
|
||||
comfyManagerStore.clearLogs()
|
||||
comfyManagerStore.setStale()
|
||||
|
||||
isRestarting.value = true
|
||||
// Refresh node definitions
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
|
||||
const onReconnect = async () => {
|
||||
try {
|
||||
comfyManagerStore.setStale()
|
||||
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
|
||||
await useWorkflowService().reloadCurrentWorkflow()
|
||||
} finally {
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
closeDialog()
|
||||
comfyManagerStore.clearLogs()
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
} catch (error) {
|
||||
// If restart fails, restore settings and reset state
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = false
|
||||
closeDialog() // Close dialog on error
|
||||
throw error
|
||||
// Reload workflow
|
||||
await useWorkflowService().reloadCurrentWorkflow()
|
||||
}
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
closeDialog()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
<MiniMap
|
||||
v-if="comfyAppReady && minimapEnabled"
|
||||
ref="minimapRef"
|
||||
class="pointer-events-auto"
|
||||
/>
|
||||
</template>
|
||||
@@ -71,7 +70,6 @@ import { useContextMenuTranslation } from '@/composables/useContextMenuTranslati
|
||||
import { useCopy } from '@/composables/useCopy'
|
||||
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
||||
import { useLitegraphSettings } from '@/composables/useLitegraphSettings'
|
||||
import { useMinimap } from '@/composables/useMinimap'
|
||||
import { usePaste } from '@/composables/usePaste'
|
||||
import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
@@ -119,9 +117,7 @@ const selectionToolboxEnabled = computed(() =>
|
||||
settingStore.get('Comfy.Canvas.SelectionToolbox')
|
||||
)
|
||||
|
||||
const minimapRef = ref<InstanceType<typeof MiniMap>>()
|
||||
const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))
|
||||
const minimap = useMinimap()
|
||||
|
||||
watchEffect(() => {
|
||||
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
|
||||
@@ -358,13 +354,6 @@ onMounted(async () => {
|
||||
}
|
||||
)
|
||||
|
||||
whenever(
|
||||
() => minimapRef.value,
|
||||
(ref) => {
|
||||
minimap.setMinimapRef(ref)
|
||||
}
|
||||
)
|
||||
|
||||
whenever(
|
||||
() => useCanvasStore().canvas,
|
||||
(canvas) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible && initialized"
|
||||
ref="minimapRef"
|
||||
class="minimap-main-container flex absolute bottom-[20px] right-[90px] z-[1000]"
|
||||
>
|
||||
<MiniMapPanel
|
||||
@@ -54,15 +55,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { useMinimap } from '@/composables/useMinimap'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
|
||||
|
||||
import MiniMapPanel from './MiniMapPanel.vue'
|
||||
|
||||
const minimap = useMinimap()
|
||||
const canvasStore = useCanvasStore()
|
||||
const minimapRef = ref<HTMLDivElement>()
|
||||
|
||||
const {
|
||||
initialized,
|
||||
@@ -80,13 +79,13 @@ const {
|
||||
renderBypass,
|
||||
renderError,
|
||||
updateOption,
|
||||
init,
|
||||
destroy,
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
handleWheel
|
||||
} = minimap
|
||||
handleWheel,
|
||||
setMinimapRef
|
||||
} = useMinimap()
|
||||
|
||||
const showOptionsPanel = ref(false)
|
||||
|
||||
@@ -94,19 +93,9 @@ const toggleOptionsPanel = () => {
|
||||
showOptionsPanel.value = !showOptionsPanel.value
|
||||
}
|
||||
|
||||
watch(
|
||||
() => canvasStore.canvas,
|
||||
async (canvas) => {
|
||||
if (canvas && !initialized.value) {
|
||||
await init()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
if (canvasStore.canvas) {
|
||||
await init()
|
||||
onMounted(() => {
|
||||
if (minimapRef.value) {
|
||||
setMinimapRef(minimapRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
|
||||
import { MinimapOptionKey } from '@/composables/useMinimap'
|
||||
import type { MinimapSettingsKey } from '@/renderer/extensions/minimap/types'
|
||||
|
||||
defineProps<{
|
||||
panelStyles: any
|
||||
@@ -92,6 +92,6 @@ defineProps<{
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
updateOption: [key: MinimapOptionKey, value: boolean]
|
||||
updateOption: [key: MinimapSettingsKey, value: boolean]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
usePragmaticDraggable,
|
||||
usePragmaticDroppable
|
||||
} from '@/composables/usePragmaticDragAndDrop'
|
||||
import { useWorkflowThumbnail } from '@/composables/useWorkflowThumbnail'
|
||||
import { useWorkflowThumbnail } from '@/renderer/thumbnail/composables/useWorkflowThumbnail'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { ComfyWorkflow } from '@/stores/workflowStore'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
|
||||
import {
|
||||
DEFAULT_DARK_COLOR_PALETTE,
|
||||
DEFAULT_LIGHT_COLOR_PALETTE
|
||||
@@ -15,12 +16,10 @@ import { Point } from '@/lib/litegraph/src/litegraph'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { addFluxKontextGroupNode } from '@/scripts/fluxKontextEditNode'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
@@ -34,7 +33,6 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
import {
|
||||
getAllNonIoNodesInSubgraph,
|
||||
getExecutionIdsForSelectedNodes
|
||||
@@ -713,54 +711,12 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',
|
||||
icon: 'pi pi-objects-column',
|
||||
label: 'Custom Nodes Manager',
|
||||
id: 'Comfy.Manager.CustomNodesManager',
|
||||
icon: 'pi pi-puzzle',
|
||||
label: 'Toggle the Custom Nodes Manager',
|
||||
versionAdded: '1.12.10',
|
||||
function: async () => {
|
||||
const { is_legacy_manager_ui } =
|
||||
(await useComfyManagerService().isLegacyManagerUI()) ?? {}
|
||||
|
||||
if (is_legacy_manager_ui === true) {
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.Menu.ToggleVisibility' // This command is registered by legacy manager FE extension
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
} else {
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowUpdateAvailablePacks',
|
||||
icon: 'pi pi-sync',
|
||||
label: 'Check for Custom Node Updates',
|
||||
versionAdded: '1.17.0',
|
||||
function: () => {
|
||||
dialogService.showManagerDialog({
|
||||
initialTab: ManagerTab.UpdateAvailable
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowMissingPacks',
|
||||
icon: 'pi pi-exclamation-circle',
|
||||
label: 'Install Missing Custom Nodes',
|
||||
versionAdded: '1.17.0',
|
||||
function: () => {
|
||||
dialogService.showManagerDialog({
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
dialogService.toggleManagerDialog()
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -925,81 +881,14 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.CustomNodesManager.ShowLegacyCustomNodesMenu',
|
||||
icon: 'pi pi-bars',
|
||||
label: 'Custom Nodes (Legacy)',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.CustomNodesManager.ToggleVisibility'
|
||||
)
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowLegacyManagerMenu',
|
||||
icon: 'mdi mdi-puzzle',
|
||||
label: 'Manager Menu (Legacy)',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
try {
|
||||
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.UnloadModels',
|
||||
icon: 'mdi mdi-vacuum-outline',
|
||||
label: 'Unload Models',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModels'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
await api.freeMemory({ freeExecutionCache: false })
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.UnloadModelsAndExecutionCache',
|
||||
icon: 'mdi mdi-vacuum-outline',
|
||||
label: 'Unload Models and Execution Cache',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModelsAndExecutionCache'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
await api.freeMemory({ freeExecutionCache: true })
|
||||
id: 'Comfy.Dev.ShowModelSelector',
|
||||
icon: 'pi pi-box',
|
||||
label: 'Show Model Selector (Dev)',
|
||||
versionAdded: '1.26.2',
|
||||
category: 'view-controls' as const,
|
||||
function: () => {
|
||||
const modelSelectorDialog = useModelSelectorDialog()
|
||||
modelSelectorDialog.show()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { computed, reactive, readonly } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
/**
|
||||
* Known server feature flags (top-level, not extensions)
|
||||
*/
|
||||
export enum ServerFeatureFlag {
|
||||
SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
|
||||
MAX_UPLOAD_SIZE = 'max_upload_size'
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for reactive access to feature flags
|
||||
*/
|
||||
export function useFeatureFlags() {
|
||||
// Create reactive state that tracks server feature flags
|
||||
const flags = reactive({
|
||||
get supportsPreviewMetadata() {
|
||||
return api.getServerFeature(ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA)
|
||||
},
|
||||
get maxUploadSize() {
|
||||
return api.getServerFeature(ServerFeatureFlag.MAX_UPLOAD_SIZE)
|
||||
}
|
||||
})
|
||||
|
||||
// Create a reactive computed for any feature flag
|
||||
const featureFlag = <T = unknown>(featurePath: string, defaultValue?: T) => {
|
||||
return computed(() => api.getServerFeature(featurePath, defaultValue))
|
||||
}
|
||||
|
||||
return {
|
||||
flags: readonly(flags),
|
||||
featureFlag
|
||||
}
|
||||
}
|
||||
@@ -1,849 +0,0 @@
|
||||
import { useRafFn, useThrottleFn } from '@vueuse/core'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
import { LGraphEventMode, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
|
||||
interface GraphCallbacks {
|
||||
onNodeAdded?: (node: LGraphNode) => void
|
||||
onNodeRemoved?: (node: LGraphNode) => void
|
||||
onConnectionChange?: (node: LGraphNode) => void
|
||||
}
|
||||
|
||||
export type MinimapOptionKey =
|
||||
| 'Comfy.Minimap.NodeColors'
|
||||
| 'Comfy.Minimap.ShowLinks'
|
||||
| 'Comfy.Minimap.ShowGroups'
|
||||
| 'Comfy.Minimap.RenderBypassState'
|
||||
| 'Comfy.Minimap.RenderErrorState'
|
||||
|
||||
export function useMinimap() {
|
||||
const settingStore = useSettingStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
const minimapRef = ref<any>(null)
|
||||
|
||||
const visible = ref(true)
|
||||
|
||||
const nodeColors = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.NodeColors')
|
||||
)
|
||||
const showLinks = computed(() => settingStore.get('Comfy.Minimap.ShowLinks'))
|
||||
const showGroups = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.ShowGroups')
|
||||
)
|
||||
const renderBypass = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.RenderBypassState')
|
||||
)
|
||||
const renderError = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.RenderErrorState')
|
||||
)
|
||||
|
||||
const updateOption = async (key: MinimapOptionKey, value: boolean) => {
|
||||
await settingStore.set(key, value)
|
||||
|
||||
needsFullRedraw.value = true
|
||||
updateMinimap()
|
||||
}
|
||||
|
||||
const initialized = ref(false)
|
||||
const bounds = ref({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 0,
|
||||
maxY: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
})
|
||||
const scale = ref(1)
|
||||
const isDragging = ref(false)
|
||||
const viewportTransform = ref({ x: 0, y: 0, width: 0, height: 0 })
|
||||
|
||||
const needsFullRedraw = ref(true)
|
||||
const needsBoundsUpdate = ref(true)
|
||||
const lastNodeCount = ref(0)
|
||||
const nodeStatesCache = new Map<NodeId, string>()
|
||||
const linksCache = ref<string>('')
|
||||
|
||||
const updateFlags = ref({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
|
||||
const width = 250
|
||||
const height = 200
|
||||
|
||||
// Theme-aware colors for canvas drawing
|
||||
const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
const nodeColor = computed(
|
||||
() => (isLightTheme.value ? '#3DA8E099' : '#0B8CE999') // lighter blue for light theme
|
||||
)
|
||||
const nodeColorDefault = computed(
|
||||
() => (isLightTheme.value ? '#D9D9D9' : '#353535') // this is the default node color when using nodeColors setting
|
||||
)
|
||||
const linkColor = computed(
|
||||
() => (isLightTheme.value ? '#616161' : '#B3B3B3') // lighter orange for light theme
|
||||
)
|
||||
const slotColor = computed(() => linkColor.value)
|
||||
const groupColor = computed(() =>
|
||||
isLightTheme.value ? '#A2D3EC' : '#1F547A'
|
||||
)
|
||||
const groupColorDefault = computed(
|
||||
() => (isLightTheme.value ? '#283640' : '#B3C1CB') // this is the default group color when using nodeColors setting
|
||||
)
|
||||
const bypassColor = computed(() =>
|
||||
isLightTheme.value ? '#DBDBDB' : '#4B184B'
|
||||
)
|
||||
|
||||
const containerRect = ref({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: width,
|
||||
height: height
|
||||
})
|
||||
|
||||
const canvasDimensions = ref({
|
||||
width: 0,
|
||||
height: 0
|
||||
})
|
||||
|
||||
const updateContainerRect = () => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
const rect = containerRef.value.getBoundingClientRect()
|
||||
containerRect.value = {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
}
|
||||
}
|
||||
|
||||
const updateCanvasDimensions = () => {
|
||||
const c = canvas.value
|
||||
if (!c) return
|
||||
|
||||
const canvasEl = c.canvas
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
|
||||
canvasDimensions.value = {
|
||||
width: canvasEl.clientWidth || canvasEl.width / dpr,
|
||||
height: canvasEl.clientHeight || canvasEl.height / dpr
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = computed(() => canvasStore.canvas)
|
||||
const graph = computed(() => {
|
||||
// If we're in a subgraph, use that; otherwise use the canvas graph
|
||||
const activeSubgraph = workflowStore.activeSubgraph
|
||||
return activeSubgraph || canvas.value?.graph
|
||||
})
|
||||
|
||||
const containerStyles = computed(() => ({
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
backgroundColor: isLightTheme.value ? '#FAF9F5' : '#15161C',
|
||||
border: `1px solid ${isLightTheme.value ? '#ccc' : '#333'}`,
|
||||
borderRadius: '8px'
|
||||
}))
|
||||
|
||||
const panelStyles = computed(() => ({
|
||||
width: `210px`,
|
||||
height: `${height}px`,
|
||||
backgroundColor: isLightTheme.value ? '#FAF9F5' : '#15161C',
|
||||
border: `1px solid ${isLightTheme.value ? '#ccc' : '#333'}`,
|
||||
borderRadius: '8px'
|
||||
}))
|
||||
|
||||
const viewportStyles = computed(() => ({
|
||||
transform: `translate(${viewportTransform.value.x}px, ${viewportTransform.value.y}px)`,
|
||||
width: `${viewportTransform.value.width}px`,
|
||||
height: `${viewportTransform.value.height}px`,
|
||||
border: `2px solid ${isLightTheme.value ? '#E0E0E0' : '#FFF'}`,
|
||||
backgroundColor: `#FFF33`,
|
||||
willChange: 'transform',
|
||||
backfaceVisibility: 'hidden' as const,
|
||||
perspective: '1000px',
|
||||
pointerEvents: 'none' as const
|
||||
}))
|
||||
|
||||
const calculateGraphBounds = () => {
|
||||
const g = graph.value
|
||||
if (!g || !g._nodes || g._nodes.length === 0) {
|
||||
return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }
|
||||
}
|
||||
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
let maxX = -Infinity
|
||||
let maxY = -Infinity
|
||||
|
||||
for (const node of g._nodes) {
|
||||
minX = Math.min(minX, node.pos[0])
|
||||
minY = Math.min(minY, node.pos[1])
|
||||
maxX = Math.max(maxX, node.pos[0] + node.size[0])
|
||||
maxY = Math.max(maxY, node.pos[1] + node.size[1])
|
||||
}
|
||||
|
||||
let currentWidth = maxX - minX
|
||||
let currentHeight = maxY - minY
|
||||
|
||||
// Enforce minimum viewport dimensions for better visualization
|
||||
const minViewportWidth = 2500
|
||||
const minViewportHeight = 2000
|
||||
|
||||
if (currentWidth < minViewportWidth) {
|
||||
const padding = (minViewportWidth - currentWidth) / 2
|
||||
minX -= padding
|
||||
maxX += padding
|
||||
currentWidth = minViewportWidth
|
||||
}
|
||||
|
||||
if (currentHeight < minViewportHeight) {
|
||||
const padding = (minViewportHeight - currentHeight) / 2
|
||||
minY -= padding
|
||||
maxY += padding
|
||||
currentHeight = minViewportHeight
|
||||
}
|
||||
|
||||
return {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
width: currentWidth,
|
||||
height: currentHeight
|
||||
}
|
||||
}
|
||||
|
||||
const calculateScale = () => {
|
||||
if (bounds.value.width === 0 || bounds.value.height === 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const scaleX = width / bounds.value.width
|
||||
const scaleY = height / bounds.value.height
|
||||
|
||||
// Apply 0.9 factor to provide padding/gap between nodes and minimap borders
|
||||
return Math.min(scaleX, scaleY) * 0.9
|
||||
}
|
||||
|
||||
const renderGroups = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
) => {
|
||||
const g = graph.value
|
||||
if (!g || !g._groups || g._groups.length === 0) return
|
||||
|
||||
for (const group of g._groups) {
|
||||
const x = (group.pos[0] - bounds.value.minX) * scale.value + offsetX
|
||||
const y = (group.pos[1] - bounds.value.minY) * scale.value + offsetY
|
||||
const w = group.size[0] * scale.value
|
||||
const h = group.size[1] * scale.value
|
||||
|
||||
let color = groupColor.value
|
||||
|
||||
if (nodeColors.value) {
|
||||
color = group.color ?? groupColorDefault.value
|
||||
|
||||
if (isLightTheme.value) {
|
||||
color = adjustColor(color, { opacity: 0.5 })
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = color
|
||||
ctx.fillRect(x, y, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
const renderNodes = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
) => {
|
||||
const g = graph.value
|
||||
if (!g || !g._nodes || g._nodes.length === 0) return
|
||||
|
||||
for (const node of g._nodes) {
|
||||
const x = (node.pos[0] - bounds.value.minX) * scale.value + offsetX
|
||||
const y = (node.pos[1] - bounds.value.minY) * scale.value + offsetY
|
||||
const w = node.size[0] * scale.value
|
||||
const h = node.size[1] * scale.value
|
||||
|
||||
let color = nodeColor.value
|
||||
|
||||
if (renderBypass.value && node.mode === LGraphEventMode.BYPASS) {
|
||||
color = bypassColor.value
|
||||
} else if (nodeColors.value) {
|
||||
color = nodeColorDefault.value
|
||||
|
||||
if (node.bgcolor) {
|
||||
color = isLightTheme.value
|
||||
? adjustColor(node.bgcolor, { lightness: 0.5 })
|
||||
: node.bgcolor
|
||||
}
|
||||
}
|
||||
|
||||
// Render solid node blocks
|
||||
ctx.fillStyle = color
|
||||
ctx.fillRect(x, y, w, h)
|
||||
|
||||
if (renderError.value && node.has_errors) {
|
||||
ctx.strokeStyle = '#FF0000'
|
||||
ctx.lineWidth = 0.3
|
||||
ctx.strokeRect(x, y, w, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const renderConnections = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
) => {
|
||||
const g = graph.value
|
||||
if (!g) return
|
||||
|
||||
ctx.strokeStyle = linkColor.value
|
||||
ctx.lineWidth = 0.3
|
||||
|
||||
const slotRadius = Math.max(scale.value, 0.5) // Larger slots that scale
|
||||
const connections: Array<{
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
}> = []
|
||||
|
||||
for (const node of g._nodes) {
|
||||
if (!node.outputs) continue
|
||||
|
||||
const x1 = (node.pos[0] - bounds.value.minX) * scale.value + offsetX
|
||||
const y1 = (node.pos[1] - bounds.value.minY) * scale.value + offsetY
|
||||
|
||||
for (const output of node.outputs) {
|
||||
if (!output.links) continue
|
||||
|
||||
for (const linkId of output.links) {
|
||||
const link = g.links[linkId]
|
||||
if (!link) continue
|
||||
|
||||
const targetNode = g.getNodeById(link.target_id)
|
||||
if (!targetNode) continue
|
||||
|
||||
const x2 =
|
||||
(targetNode.pos[0] - bounds.value.minX) * scale.value + offsetX
|
||||
const y2 =
|
||||
(targetNode.pos[1] - bounds.value.minY) * scale.value + offsetY
|
||||
|
||||
const outputX = x1 + node.size[0] * scale.value
|
||||
const outputY = y1 + node.size[1] * scale.value * 0.2
|
||||
const inputX = x2
|
||||
const inputY = y2 + targetNode.size[1] * scale.value * 0.2
|
||||
|
||||
// Draw connection line
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(outputX, outputY)
|
||||
ctx.lineTo(inputX, inputY)
|
||||
ctx.stroke()
|
||||
|
||||
connections.push({ x1: outputX, y1: outputY, x2: inputX, y2: inputY })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render connection slots on top
|
||||
ctx.fillStyle = slotColor.value
|
||||
for (const conn of connections) {
|
||||
// Output slot
|
||||
ctx.beginPath()
|
||||
ctx.arc(conn.x1, conn.y1, slotRadius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
// Input slot
|
||||
ctx.beginPath()
|
||||
ctx.arc(conn.x2, conn.y2, slotRadius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
const renderMinimap = () => {
|
||||
const g = graph.value
|
||||
if (!canvasRef.value || !g) return
|
||||
|
||||
const ctx = canvasRef.value.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
// Fast path for 0 nodes - just show background
|
||||
if (!g._nodes || g._nodes.length === 0) {
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
return
|
||||
}
|
||||
|
||||
const needsRedraw =
|
||||
needsFullRedraw.value ||
|
||||
updateFlags.value.nodes ||
|
||||
updateFlags.value.connections
|
||||
|
||||
if (needsRedraw) {
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
const offsetX = (width - bounds.value.width * scale.value) / 2
|
||||
const offsetY = (height - bounds.value.height * scale.value) / 2
|
||||
|
||||
if (showGroups.value) {
|
||||
renderGroups(ctx, offsetX, offsetY)
|
||||
}
|
||||
|
||||
if (showLinks.value) {
|
||||
renderConnections(ctx, offsetX, offsetY)
|
||||
}
|
||||
|
||||
renderNodes(ctx, offsetX, offsetY)
|
||||
|
||||
needsFullRedraw.value = false
|
||||
updateFlags.value.nodes = false
|
||||
updateFlags.value.connections = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateViewport = () => {
|
||||
const c = canvas.value
|
||||
if (!c) return
|
||||
|
||||
if (
|
||||
canvasDimensions.value.width === 0 ||
|
||||
canvasDimensions.value.height === 0
|
||||
) {
|
||||
updateCanvasDimensions()
|
||||
}
|
||||
|
||||
const ds = c.ds
|
||||
|
||||
const viewportWidth = canvasDimensions.value.width / ds.scale
|
||||
const viewportHeight = canvasDimensions.value.height / ds.scale
|
||||
|
||||
const worldX = -ds.offset[0]
|
||||
const worldY = -ds.offset[1]
|
||||
|
||||
const centerOffsetX = (width - bounds.value.width * scale.value) / 2
|
||||
const centerOffsetY = (height - bounds.value.height * scale.value) / 2
|
||||
|
||||
viewportTransform.value = {
|
||||
x: (worldX - bounds.value.minX) * scale.value + centerOffsetX,
|
||||
y: (worldY - bounds.value.minY) * scale.value + centerOffsetY,
|
||||
width: viewportWidth * scale.value,
|
||||
height: viewportHeight * scale.value
|
||||
}
|
||||
|
||||
updateFlags.value.viewport = false
|
||||
}
|
||||
|
||||
const updateMinimap = () => {
|
||||
if (needsBoundsUpdate.value || updateFlags.value.bounds) {
|
||||
bounds.value = calculateGraphBounds()
|
||||
scale.value = calculateScale()
|
||||
needsBoundsUpdate.value = false
|
||||
updateFlags.value.bounds = false
|
||||
needsFullRedraw.value = true
|
||||
// When bounds change, we need to update the viewport position
|
||||
updateFlags.value.viewport = true
|
||||
}
|
||||
|
||||
if (
|
||||
needsFullRedraw.value ||
|
||||
updateFlags.value.nodes ||
|
||||
updateFlags.value.connections
|
||||
) {
|
||||
renderMinimap()
|
||||
}
|
||||
|
||||
// Update viewport if needed (e.g., after bounds change)
|
||||
if (updateFlags.value.viewport) {
|
||||
updateViewport()
|
||||
}
|
||||
}
|
||||
|
||||
const checkForChanges = useThrottleFn(() => {
|
||||
const g = graph.value
|
||||
if (!g) return
|
||||
|
||||
let structureChanged = false
|
||||
let positionChanged = false
|
||||
let connectionChanged = false
|
||||
|
||||
if (g._nodes.length !== lastNodeCount.value) {
|
||||
structureChanged = true
|
||||
lastNodeCount.value = g._nodes.length
|
||||
}
|
||||
|
||||
for (const node of g._nodes) {
|
||||
const key = node.id
|
||||
const currentState = `${node.pos[0]},${node.pos[1]},${node.size[0]},${node.size[1]}`
|
||||
|
||||
if (nodeStatesCache.get(key) !== currentState) {
|
||||
positionChanged = true
|
||||
nodeStatesCache.set(key, currentState)
|
||||
}
|
||||
}
|
||||
|
||||
const currentLinks = JSON.stringify(g.links || {})
|
||||
if (currentLinks !== linksCache.value) {
|
||||
connectionChanged = true
|
||||
linksCache.value = currentLinks
|
||||
}
|
||||
|
||||
const currentNodeIds = new Set(g._nodes.map((n) => n.id))
|
||||
for (const [nodeId] of nodeStatesCache) {
|
||||
if (!currentNodeIds.has(nodeId)) {
|
||||
nodeStatesCache.delete(nodeId)
|
||||
structureChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
if (structureChanged || positionChanged) {
|
||||
updateFlags.value.bounds = true
|
||||
updateFlags.value.nodes = true
|
||||
}
|
||||
|
||||
if (connectionChanged) {
|
||||
updateFlags.value.connections = true
|
||||
}
|
||||
|
||||
if (structureChanged || positionChanged || connectionChanged) {
|
||||
updateMinimap()
|
||||
}
|
||||
}, 500)
|
||||
|
||||
const { pause: pauseChangeDetection, resume: resumeChangeDetection } =
|
||||
useRafFn(
|
||||
async () => {
|
||||
if (visible.value) {
|
||||
await checkForChanges()
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
const { startSync: startViewportSync, stopSync: stopViewportSync } =
|
||||
useCanvasTransformSync(updateViewport, { autoStart: false })
|
||||
|
||||
// Pointer event handlers for touch screen support
|
||||
const handlePointerDown = (e: PointerEvent) => {
|
||||
isDragging.value = true
|
||||
updateContainerRect()
|
||||
handlePointerMove(e)
|
||||
}
|
||||
|
||||
const handlePointerMove = (e: PointerEvent) => {
|
||||
if (!isDragging.value || !canvasRef.value || !canvas.value) return
|
||||
|
||||
const x = e.clientX - containerRect.value.left
|
||||
const y = e.clientY - containerRect.value.top
|
||||
|
||||
const offsetX = (width - bounds.value.width * scale.value) / 2
|
||||
const offsetY = (height - bounds.value.height * scale.value) / 2
|
||||
|
||||
const worldX = (x - offsetX) / scale.value + bounds.value.minX
|
||||
const worldY = (y - offsetY) / scale.value + bounds.value.minY
|
||||
|
||||
centerViewOn(worldX, worldY)
|
||||
}
|
||||
|
||||
const handlePointerUp = () => {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const c = canvas.value
|
||||
if (!c) return
|
||||
|
||||
if (
|
||||
containerRect.value.left === 0 &&
|
||||
containerRect.value.top === 0 &&
|
||||
containerRef.value
|
||||
) {
|
||||
updateContainerRect()
|
||||
}
|
||||
|
||||
const ds = c.ds
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1
|
||||
|
||||
const newScale = ds.scale * delta
|
||||
|
||||
const MIN_SCALE = 0.1
|
||||
const MAX_SCALE = 10
|
||||
|
||||
if (newScale < MIN_SCALE || newScale > MAX_SCALE) return
|
||||
|
||||
const x = e.clientX - containerRect.value.left
|
||||
const y = e.clientY - containerRect.value.top
|
||||
|
||||
const offsetX = (width - bounds.value.width * scale.value) / 2
|
||||
const offsetY = (height - bounds.value.height * scale.value) / 2
|
||||
|
||||
const worldX = (x - offsetX) / scale.value + bounds.value.minX
|
||||
const worldY = (y - offsetY) / scale.value + bounds.value.minY
|
||||
|
||||
ds.scale = newScale
|
||||
|
||||
centerViewOn(worldX, worldY)
|
||||
}
|
||||
|
||||
const centerViewOn = (worldX: number, worldY: number) => {
|
||||
const c = canvas.value
|
||||
if (!c) return
|
||||
|
||||
if (
|
||||
canvasDimensions.value.width === 0 ||
|
||||
canvasDimensions.value.height === 0
|
||||
) {
|
||||
updateCanvasDimensions()
|
||||
}
|
||||
|
||||
const ds = c.ds
|
||||
|
||||
const viewportWidth = canvasDimensions.value.width / ds.scale
|
||||
const viewportHeight = canvasDimensions.value.height / ds.scale
|
||||
|
||||
ds.offset[0] = -(worldX - viewportWidth / 2)
|
||||
ds.offset[1] = -(worldY - viewportHeight / 2)
|
||||
|
||||
updateFlags.value.viewport = true
|
||||
|
||||
c.setDirty(true, true)
|
||||
}
|
||||
|
||||
// Map to store original callbacks per graph ID
|
||||
const originalCallbacksMap = new Map<string, GraphCallbacks>()
|
||||
|
||||
const handleGraphChanged = useThrottleFn(() => {
|
||||
needsFullRedraw.value = true
|
||||
updateFlags.value.bounds = true
|
||||
updateFlags.value.nodes = true
|
||||
updateFlags.value.connections = true
|
||||
updateMinimap()
|
||||
}, 500)
|
||||
|
||||
const setupEventListeners = () => {
|
||||
const g = graph.value
|
||||
if (!g) return
|
||||
|
||||
// Check if we've already wrapped this graph's callbacks
|
||||
if (originalCallbacksMap.has(g.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Store the original callbacks for this graph
|
||||
const originalCallbacks: GraphCallbacks = {
|
||||
onNodeAdded: g.onNodeAdded,
|
||||
onNodeRemoved: g.onNodeRemoved,
|
||||
onConnectionChange: g.onConnectionChange
|
||||
}
|
||||
originalCallbacksMap.set(g.id, originalCallbacks)
|
||||
|
||||
g.onNodeAdded = function (node) {
|
||||
originalCallbacks.onNodeAdded?.call(this, node)
|
||||
|
||||
void handleGraphChanged()
|
||||
}
|
||||
|
||||
g.onNodeRemoved = function (node) {
|
||||
originalCallbacks.onNodeRemoved?.call(this, node)
|
||||
nodeStatesCache.delete(node.id)
|
||||
void handleGraphChanged()
|
||||
}
|
||||
|
||||
g.onConnectionChange = function (node) {
|
||||
originalCallbacks.onConnectionChange?.call(this, node)
|
||||
|
||||
void handleGraphChanged()
|
||||
}
|
||||
}
|
||||
|
||||
const cleanupEventListeners = () => {
|
||||
const g = graph.value
|
||||
if (!g) return
|
||||
|
||||
const originalCallbacks = originalCallbacksMap.get(g.id)
|
||||
if (!originalCallbacks) {
|
||||
console.error(
|
||||
'Attempted to cleanup event listeners for graph that was never set up'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
g.onNodeAdded = originalCallbacks.onNodeAdded
|
||||
g.onNodeRemoved = originalCallbacks.onNodeRemoved
|
||||
g.onConnectionChange = originalCallbacks.onConnectionChange
|
||||
|
||||
originalCallbacksMap.delete(g.id)
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
if (initialized.value) return
|
||||
|
||||
visible.value = settingStore.get('Comfy.Minimap.Visible')
|
||||
|
||||
if (canvas.value && graph.value) {
|
||||
setupEventListeners()
|
||||
|
||||
api.addEventListener('graphChanged', handleGraphChanged)
|
||||
|
||||
if (containerRef.value) {
|
||||
updateContainerRect()
|
||||
}
|
||||
updateCanvasDimensions()
|
||||
|
||||
window.addEventListener('resize', updateContainerRect)
|
||||
window.addEventListener('scroll', updateContainerRect)
|
||||
window.addEventListener('resize', updateCanvasDimensions)
|
||||
|
||||
needsFullRedraw.value = true
|
||||
updateFlags.value.bounds = true
|
||||
updateFlags.value.nodes = true
|
||||
updateFlags.value.connections = true
|
||||
updateFlags.value.viewport = true
|
||||
|
||||
updateMinimap()
|
||||
updateViewport()
|
||||
|
||||
if (visible.value) {
|
||||
resumeChangeDetection()
|
||||
startViewportSync()
|
||||
}
|
||||
initialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const destroy = () => {
|
||||
pauseChangeDetection()
|
||||
stopViewportSync()
|
||||
cleanupEventListeners()
|
||||
|
||||
api.removeEventListener('graphChanged', handleGraphChanged)
|
||||
|
||||
window.removeEventListener('resize', updateContainerRect)
|
||||
window.removeEventListener('scroll', updateContainerRect)
|
||||
window.removeEventListener('resize', updateCanvasDimensions)
|
||||
|
||||
nodeStatesCache.clear()
|
||||
initialized.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
canvas,
|
||||
async (newCanvas, oldCanvas) => {
|
||||
if (oldCanvas) {
|
||||
cleanupEventListeners()
|
||||
pauseChangeDetection()
|
||||
stopViewportSync()
|
||||
api.removeEventListener('graphChanged', handleGraphChanged)
|
||||
window.removeEventListener('resize', updateContainerRect)
|
||||
window.removeEventListener('scroll', updateContainerRect)
|
||||
window.removeEventListener('resize', updateCanvasDimensions)
|
||||
}
|
||||
if (newCanvas && !initialized.value) {
|
||||
await init()
|
||||
}
|
||||
},
|
||||
{ immediate: true, flush: 'post' }
|
||||
)
|
||||
|
||||
// Watch for graph changes (e.g., when navigating to/from subgraphs)
|
||||
watch(graph, (newGraph, oldGraph) => {
|
||||
if (newGraph && newGraph !== oldGraph) {
|
||||
cleanupEventListeners()
|
||||
setupEventListeners()
|
||||
needsFullRedraw.value = true
|
||||
updateFlags.value.bounds = true
|
||||
updateFlags.value.nodes = true
|
||||
updateFlags.value.connections = true
|
||||
updateMinimap()
|
||||
}
|
||||
})
|
||||
|
||||
watch(visible, async (isVisible) => {
|
||||
if (isVisible) {
|
||||
if (containerRef.value) {
|
||||
updateContainerRect()
|
||||
}
|
||||
updateCanvasDimensions()
|
||||
|
||||
needsFullRedraw.value = true
|
||||
updateFlags.value.bounds = true
|
||||
updateFlags.value.nodes = true
|
||||
updateFlags.value.connections = true
|
||||
updateFlags.value.viewport = true
|
||||
|
||||
await nextTick()
|
||||
|
||||
await nextTick()
|
||||
|
||||
updateMinimap()
|
||||
updateViewport()
|
||||
resumeChangeDetection()
|
||||
startViewportSync()
|
||||
} else {
|
||||
pauseChangeDetection()
|
||||
stopViewportSync()
|
||||
}
|
||||
})
|
||||
|
||||
const toggle = async () => {
|
||||
visible.value = !visible.value
|
||||
await settingStore.set('Comfy.Minimap.Visible', visible.value)
|
||||
}
|
||||
|
||||
const setMinimapRef = (ref: any) => {
|
||||
minimapRef.value = ref
|
||||
}
|
||||
|
||||
return {
|
||||
visible: computed(() => visible.value),
|
||||
initialized: computed(() => initialized.value),
|
||||
|
||||
containerRef,
|
||||
canvasRef,
|
||||
containerStyles,
|
||||
viewportStyles,
|
||||
panelStyles,
|
||||
width,
|
||||
height,
|
||||
|
||||
nodeColors,
|
||||
showLinks,
|
||||
showGroups,
|
||||
renderBypass,
|
||||
renderError,
|
||||
|
||||
init,
|
||||
destroy,
|
||||
toggle,
|
||||
renderMinimap,
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
handleWheel,
|
||||
setMinimapRef,
|
||||
updateOption
|
||||
}
|
||||
}
|
||||
29
src/composables/useModelSelectorDialog.ts
Normal file
29
src/composables/useModelSelectorDialog.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import ModelSelector from '@/components/custom/widget/ModelSelector.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const DIALOG_KEY = 'global-model-selector'
|
||||
|
||||
export const useModelSelectorDialog = () => {
|
||||
const dialogService = useDialogService()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function hide() {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
}
|
||||
|
||||
function show() {
|
||||
dialogService.showLayoutDialog({
|
||||
key: DIALOG_KEY,
|
||||
component: ModelSelector,
|
||||
props: {
|
||||
onClose: hide
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
hide
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,6 @@ export const CORE_MENU_COMMANDS = [
|
||||
],
|
||||
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
|
||||
[['Edit'], ['Comfy.OpenClipspace']],
|
||||
[
|
||||
['Manager'],
|
||||
[
|
||||
'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',
|
||||
'Comfy.Manager.ShowMissingPacks',
|
||||
'Comfy.Manager.ShowUpdateAvailablePacks'
|
||||
]
|
||||
],
|
||||
[
|
||||
['Help'],
|
||||
[
|
||||
|
||||
@@ -13,13 +13,6 @@ import type { SettingParams } from '@/types/settingTypes'
|
||||
* when they are no longer needed.
|
||||
*/
|
||||
export const CORE_SETTINGS: SettingParams[] = [
|
||||
{
|
||||
id: 'Comfy.Memory.AllowManualUnload',
|
||||
name: 'Allow manual unload of models and execution cache via user command',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.18.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Validation.Workflows',
|
||||
name: 'Validate workflows',
|
||||
|
||||
@@ -1574,7 +1574,10 @@ export class LGraphNode
|
||||
* remove an existing output slot
|
||||
*/
|
||||
removeOutput(slot: number): void {
|
||||
this.disconnectOutput(slot)
|
||||
// Only disconnect if node is part of a graph
|
||||
if (this.graph) {
|
||||
this.disconnectOutput(slot)
|
||||
}
|
||||
const { outputs } = this
|
||||
outputs.splice(slot, 1)
|
||||
|
||||
@@ -1582,11 +1585,12 @@ export class LGraphNode
|
||||
const output = outputs[i]
|
||||
if (!output || !output.links) continue
|
||||
|
||||
for (const linkId of output.links) {
|
||||
if (!this.graph) throw new NullGraphError()
|
||||
|
||||
const link = this.graph._links.get(linkId)
|
||||
if (link) link.origin_slot--
|
||||
// Only update link indices if node is part of a graph
|
||||
if (this.graph) {
|
||||
for (const linkId of output.links) {
|
||||
const link = this.graph._links.get(linkId)
|
||||
if (link) link.origin_slot--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1626,7 +1630,10 @@ export class LGraphNode
|
||||
* remove an existing input slot
|
||||
*/
|
||||
removeInput(slot: number): void {
|
||||
this.disconnectInput(slot, true)
|
||||
// Only disconnect if node is part of a graph
|
||||
if (this.graph) {
|
||||
this.disconnectInput(slot, true)
|
||||
}
|
||||
const { inputs } = this
|
||||
const slot_info = inputs.splice(slot, 1)
|
||||
|
||||
@@ -1634,9 +1641,11 @@ export class LGraphNode
|
||||
const input = inputs[i]
|
||||
if (!input?.link) continue
|
||||
|
||||
if (!this.graph) throw new NullGraphError()
|
||||
const link = this.graph._links.get(input.link)
|
||||
if (link) link.target_slot--
|
||||
// Only update link indices if node is part of a graph
|
||||
if (this.graph) {
|
||||
const link = this.graph._links.get(input.link)
|
||||
if (link) link.target_slot--
|
||||
}
|
||||
}
|
||||
this.onInputRemoved?.(slot, slot_info[0])
|
||||
this.setDirtyCanvas(true, true)
|
||||
|
||||
@@ -656,4 +656,119 @@ describe('LGraphNode', () => {
|
||||
spy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeInput/removeOutput on copied nodes', () => {
|
||||
beforeEach(() => {
|
||||
// Register a test node type so clone() can work
|
||||
LiteGraph.registerNodeType('TestNode', LGraphNode)
|
||||
})
|
||||
|
||||
test('should NOT throw error when calling removeInput on a copied node without graph', () => {
|
||||
// Create a node with an input
|
||||
const originalNode = new LGraphNode('Test Node')
|
||||
originalNode.type = 'TestNode'
|
||||
originalNode.addInput('input1', 'number')
|
||||
|
||||
// Clone the node (which creates a node without graph reference)
|
||||
const copiedNode = originalNode.clone()
|
||||
|
||||
// This should NOT throw anymore - we can remove inputs on nodes without graph
|
||||
expect(() => copiedNode!.removeInput(0)).not.toThrow()
|
||||
expect(copiedNode!.inputs).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('should NOT throw error when calling removeOutput on a copied node without graph', () => {
|
||||
// Create a node with an output
|
||||
const originalNode = new LGraphNode('Test Node')
|
||||
originalNode.type = 'TestNode'
|
||||
originalNode.addOutput('output1', 'number')
|
||||
|
||||
// Clone the node (which creates a node without graph reference)
|
||||
const copiedNode = originalNode.clone()
|
||||
|
||||
// This should NOT throw anymore - we can remove outputs on nodes without graph
|
||||
expect(() => copiedNode!.removeOutput(0)).not.toThrow()
|
||||
expect(copiedNode!.outputs).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('should skip disconnectInput/disconnectOutput when node has no graph', () => {
|
||||
// Create nodes with input/output
|
||||
const nodeWithInput = new LGraphNode('Test Node')
|
||||
nodeWithInput.type = 'TestNode'
|
||||
nodeWithInput.addInput('input1', 'number')
|
||||
|
||||
const nodeWithOutput = new LGraphNode('Test Node')
|
||||
nodeWithOutput.type = 'TestNode'
|
||||
nodeWithOutput.addOutput('output1', 'number')
|
||||
|
||||
// Clone nodes (no graph reference)
|
||||
const clonedInput = nodeWithInput.clone()
|
||||
const clonedOutput = nodeWithOutput.clone()
|
||||
|
||||
// Mock disconnect methods to verify they're not called
|
||||
clonedInput!.disconnectInput = vi.fn()
|
||||
clonedOutput!.disconnectOutput = vi.fn()
|
||||
|
||||
// Remove input/output - disconnect methods should NOT be called
|
||||
clonedInput!.removeInput(0)
|
||||
clonedOutput!.removeOutput(0)
|
||||
|
||||
expect(clonedInput!.disconnectInput).not.toHaveBeenCalled()
|
||||
expect(clonedOutput!.disconnectOutput).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should be able to removeInput on a copied node after adding to graph', () => {
|
||||
// Create a graph and a node with an input
|
||||
const graph = new LGraph()
|
||||
const originalNode = new LGraphNode('Test Node')
|
||||
originalNode.type = 'TestNode'
|
||||
originalNode.addInput('input1', 'number')
|
||||
|
||||
// Clone the node and add to graph
|
||||
const copiedNode = originalNode.clone()
|
||||
expect(copiedNode).not.toBeNull()
|
||||
graph.add(copiedNode!)
|
||||
|
||||
// This should work now that the node has a graph reference
|
||||
expect(() => copiedNode!.removeInput(0)).not.toThrow()
|
||||
expect(copiedNode!.inputs).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('should be able to removeOutput on a copied node after adding to graph', () => {
|
||||
// Create a graph and a node with an output
|
||||
const graph = new LGraph()
|
||||
const originalNode = new LGraphNode('Test Node')
|
||||
originalNode.type = 'TestNode'
|
||||
originalNode.addOutput('output1', 'number')
|
||||
|
||||
// Clone the node and add to graph
|
||||
const copiedNode = originalNode.clone()
|
||||
expect(copiedNode).not.toBeNull()
|
||||
graph.add(copiedNode!)
|
||||
|
||||
// This should work now that the node has a graph reference
|
||||
expect(() => copiedNode!.removeOutput(0)).not.toThrow()
|
||||
expect(copiedNode!.outputs).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('RerouteNode clone scenario - should be able to removeOutput and addOutput on cloned node', () => {
|
||||
// This simulates the RerouteNode clone method behavior
|
||||
const originalNode = new LGraphNode('Reroute')
|
||||
originalNode.type = 'TestNode'
|
||||
originalNode.addOutput('*', '*')
|
||||
|
||||
// Clone the node (simulating RerouteNode.clone)
|
||||
const clonedNode = originalNode.clone()
|
||||
expect(clonedNode).not.toBeNull()
|
||||
|
||||
// This should not throw - we should be able to modify outputs on a cloned node
|
||||
expect(() => {
|
||||
clonedNode!.removeOutput(0)
|
||||
clonedNode!.addOutput('renamed', '*')
|
||||
}).not.toThrow()
|
||||
|
||||
expect(clonedNode!.outputs).toHaveLength(1)
|
||||
expect(clonedNode!.outputs[0].name).toBe('renamed')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "الاتصال بالدعم"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "إظهار منتقي النماذج (للمطورين)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "تكرار سير العمل الحالي"
|
||||
},
|
||||
|
||||
@@ -840,6 +840,7 @@
|
||||
"Save": "حفظ",
|
||||
"Save As": "حفظ باسم",
|
||||
"Show Keybindings Dialog": "عرض مربع حوار اختصارات لوحة المفاتيح",
|
||||
"Show Model Selector (Dev)": "إظهار منتقي النماذج (للمطورين)",
|
||||
"Show Settings Dialog": "عرض نافذة الإعدادات",
|
||||
"Sign Out": "تسجيل خروج",
|
||||
"Toggle Essential Bottom Panel": "تبديل اللوحة السفلية الأساسية",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Contact Support"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "Show Model Selector (Dev)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Duplicate Current Workflow"
|
||||
},
|
||||
@@ -164,20 +167,8 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Load Default Workflow"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Custom Nodes Manager"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "Custom Nodes (Legacy)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "Manager Menu (Legacy)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Install Missing Custom Nodes"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Check for Custom Node Updates"
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Toggle the Custom Nodes Manager"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Toggle the Custom Nodes Manager Progress Bar"
|
||||
@@ -191,12 +182,6 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Open Mask Editor for Selected Node"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Unload Models"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Unload Models and Execution Cache"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "New Blank Workflow"
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"confirmed": "Confirmed",
|
||||
"reset": "Reset",
|
||||
"resetAll": "Reset All",
|
||||
"clearFilters": "Clear Filters",
|
||||
"resetAllKeybindingsTooltip": "Reset all keybindings to default",
|
||||
"customizeFolder": "Customize Folder",
|
||||
"icon": "Icon",
|
||||
@@ -98,6 +99,12 @@
|
||||
"nodes": "Nodes",
|
||||
"community": "Community",
|
||||
"all": "All",
|
||||
"versionMismatchWarning": "Version Compatibility Warning",
|
||||
"versionMismatchWarningMessage": "{warning}: {detail} Visit https://docs.comfy.org/installation/update_comfyui#common-update-issues for update instructions.",
|
||||
"frontendOutdated": "Frontend version {frontendVersion} is outdated. Backend requires {requiredVersion} or higher.",
|
||||
"frontendNewer": "Frontend version {frontendVersion} may not be compatible with backend version {backendVersion}.",
|
||||
"updateFrontend": "Update Frontend",
|
||||
"dismiss": "Dismiss",
|
||||
"update": "Update",
|
||||
"updated": "Updated",
|
||||
"resultsCount": "Found {count} Results",
|
||||
@@ -134,18 +141,15 @@
|
||||
"releaseTitle": "{package} {version} Release",
|
||||
"progressCountOf": "of",
|
||||
"keybindingAlreadyExists": "Keybinding already exists on",
|
||||
"commandProhibited": "Command {command} is prohibited. Contact an administrator for more information.",
|
||||
"startRecording": "Start Recording",
|
||||
"stopRecording": "Stop Recording",
|
||||
"micPermissionDenied": "Microphone permission denied",
|
||||
"noAudioRecorded": "No audio recorded",
|
||||
"nodesRunning": "nodes running"
|
||||
"nodesRunning": "nodes running",
|
||||
"duplicate": "Duplicate"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
"legacyMenuNotAvailable": "Legacy manager menu is not available, defaulting to the new manager menu.",
|
||||
"legacyManagerUI": "Use Legacy UI",
|
||||
"legacyManagerUIDescription": "To use the legacy Manager UI, start ComfyUI with --enable-manager-legacy-ui",
|
||||
"failed": "Failed ({count})",
|
||||
"noNodesFound": "No nodes found",
|
||||
"noNodesFoundDescription": "The pack's nodes either could not be parsed, or the pack is a frontend extension only and doesn't have any nodes.",
|
||||
@@ -155,12 +159,6 @@
|
||||
"inWorkflow": "In Workflow",
|
||||
"infoPanelEmpty": "Click an item to see the info",
|
||||
"restartToApplyChanges": "To apply changes, please restart ComfyUI",
|
||||
"clickToFinishSetup": "Click",
|
||||
"toFinishSetup": "to finish setup",
|
||||
"applyChanges": "Apply Changes",
|
||||
"restartingBackend": "Restarting backend to apply changes...",
|
||||
"extensionsSuccessfullyInstalled": "Extension(s) successfully installed and are ready to use!",
|
||||
"installingDependencies": "Installing dependencies...",
|
||||
"loadingVersions": "Loading versions...",
|
||||
"selectVersion": "Select Version",
|
||||
"downloads": "Downloads",
|
||||
@@ -434,12 +432,19 @@
|
||||
"restart": "Restart"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"themeToggle": "Toggle Theme",
|
||||
"helpCenter": "Help Center",
|
||||
"logout": "Logout",
|
||||
"queue": "Queue",
|
||||
"nodeLibrary": "Node Library",
|
||||
"workflows": "Workflows",
|
||||
"templates": "Templates",
|
||||
"labels": {
|
||||
"queue": "Queue",
|
||||
"nodes": "Nodes",
|
||||
"models": "Models",
|
||||
"workflows": "Workflows",
|
||||
"templates": "Templates"
|
||||
},
|
||||
"browseTemplates": "Browse example templates",
|
||||
"openWorkflow": "Open workflow in local file system",
|
||||
"newBlankWorkflow": "Create a new blank workflow",
|
||||
@@ -535,7 +540,14 @@
|
||||
"clipspace": "Open Clipspace",
|
||||
"resetView": "Reset canvas view",
|
||||
"clear": "Clear workflow",
|
||||
"toggleBottomPanel": "Toggle Bottom Panel"
|
||||
"toggleBottomPanel": "Toggle Bottom Panel",
|
||||
"theme": "Theme",
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"manageExtensions": "Manage Extensions",
|
||||
"settings": "Settings",
|
||||
"help": "Help",
|
||||
"queue": "Queue Panel"
|
||||
},
|
||||
"tabMenu": {
|
||||
"duplicateTab": "Duplicate Tab",
|
||||
@@ -548,6 +560,8 @@
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"title": "Get Started with a Template",
|
||||
"loadingMore": "Loading more templates...",
|
||||
"searchPlaceholder": "Search templates...",
|
||||
"category": {
|
||||
"ComfyUI Examples": "ComfyUI Examples",
|
||||
"Custom Nodes": "Custom Nodes",
|
||||
@@ -877,7 +891,8 @@
|
||||
"fitView": "Fit View",
|
||||
"selectMode": "Select Mode",
|
||||
"panMode": "Pan Mode",
|
||||
"toggleLinkVisibility": "Toggle Link Visibility"
|
||||
"toggleLinkVisibility": "Toggle Link Visibility",
|
||||
"toggleMinimap": "Toggle Minimap"
|
||||
},
|
||||
"groupNode": {
|
||||
"create": "Create group node",
|
||||
@@ -926,9 +941,8 @@
|
||||
"Image Layer": "Image Layer"
|
||||
},
|
||||
"menuLabels": {
|
||||
"Workflow": "Workflow",
|
||||
"File": "File",
|
||||
"Edit": "Edit",
|
||||
"Manager": "Manager",
|
||||
"Help": "Help",
|
||||
"Check for Updates": "Check for Updates",
|
||||
"Open Custom Nodes Folder": "Open Custom Nodes Folder",
|
||||
@@ -942,18 +956,20 @@
|
||||
"Quit": "Quit",
|
||||
"Reinstall": "Reinstall",
|
||||
"Restart": "Restart",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Open 3D Viewer (Beta) for Selected Node",
|
||||
"Browse Templates": "Browse Templates",
|
||||
"Add Edit Model Step": "Add Edit Model Step",
|
||||
"Delete Selected Items": "Delete Selected Items",
|
||||
"Fit view to selected nodes": "Fit view to selected nodes",
|
||||
"Zoom to fit": "Zoom to fit",
|
||||
"Move Selected Nodes Down": "Move Selected Nodes Down",
|
||||
"Move Selected Nodes Left": "Move Selected Nodes Left",
|
||||
"Move Selected Nodes Right": "Move Selected Nodes Right",
|
||||
"Move Selected Nodes Up": "Move Selected Nodes Up",
|
||||
"Reset View": "Reset View",
|
||||
"Resize Selected Nodes": "Resize Selected Nodes",
|
||||
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",
|
||||
"Node Links": "Node Links",
|
||||
"Canvas Toggle Lock": "Canvas Toggle Lock",
|
||||
"Minimap": "Minimap",
|
||||
"Pin/Unpin Selected Items": "Pin/Unpin Selected Items",
|
||||
"Bypass/Unbypass Selected Nodes": "Bypass/Unbypass Selected Nodes",
|
||||
"Collapse/Expand Selected Nodes": "Collapse/Expand Selected Nodes",
|
||||
@@ -964,13 +980,16 @@
|
||||
"Clear Pending Tasks": "Clear Pending Tasks",
|
||||
"Clear Workflow": "Clear Workflow",
|
||||
"Contact Support": "Contact Support",
|
||||
"Show Model Selector (Dev)": "Show Model Selector (Dev)",
|
||||
"Duplicate Current Workflow": "Duplicate Current Workflow",
|
||||
"Export": "Export",
|
||||
"Export (API)": "Export (API)",
|
||||
"Give Feedback": "Give Feedback",
|
||||
"Convert Selection to Subgraph": "Convert Selection to Subgraph",
|
||||
"Exit Subgraph": "Exit Subgraph",
|
||||
"Fit Group To Contents": "Fit Group To Contents",
|
||||
"Group Selected Nodes": "Group Selected Nodes",
|
||||
"Unpack the selected Subgraph": "Unpack the selected Subgraph",
|
||||
"Convert selected nodes to group node": "Convert selected nodes to group node",
|
||||
"Manage group nodes": "Manage group nodes",
|
||||
"Ungroup selected group nodes": "Ungroup selected group nodes",
|
||||
@@ -981,17 +1000,14 @@
|
||||
"ComfyUI Issues": "ComfyUI Issues",
|
||||
"Interrupt": "Interrupt",
|
||||
"Load Default Workflow": "Load Default Workflow",
|
||||
"Custom Nodes Manager": "Custom Nodes Manager",
|
||||
"Custom Nodes (Legacy)": "Custom Nodes (Legacy)",
|
||||
"Manager Menu (Legacy)": "Manager Menu (Legacy)",
|
||||
"Install Missing Custom Nodes": "Install Missing Custom Nodes",
|
||||
"Check for Custom Node Updates": "Check for Custom Node Updates",
|
||||
"Toggle the Custom Nodes Manager": "Toggle the Custom Nodes Manager",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Toggle the Custom Nodes Manager Progress Bar",
|
||||
"Decrease Brush Size in MaskEditor": "Decrease Brush Size in MaskEditor",
|
||||
"Increase Brush Size in MaskEditor": "Increase Brush Size in MaskEditor",
|
||||
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
|
||||
"Unload Models": "Unload Models",
|
||||
"Unload Models and Execution Cache": "Unload Models and Execution Cache",
|
||||
"New": "New",
|
||||
"Clipspace": "Clipspace",
|
||||
"Manager": "Manager",
|
||||
"Open": "Open",
|
||||
"Queue Prompt": "Queue Prompt",
|
||||
"Queue Prompt (Front)": "Queue Prompt (Front)",
|
||||
@@ -1001,6 +1017,8 @@
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Show Settings Dialog": "Show Settings Dialog",
|
||||
"Canvas Performance": "Canvas Performance",
|
||||
"Help Center": "Help Center",
|
||||
"Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)",
|
||||
"Undo": "Undo",
|
||||
"Open Sign In Dialog": "Open Sign In Dialog",
|
||||
@@ -1009,14 +1027,17 @@
|
||||
"Next Opened Workflow": "Next Opened Workflow",
|
||||
"Previous Opened Workflow": "Previous Opened Workflow",
|
||||
"Toggle Search Box": "Toggle Search Box",
|
||||
"Toggle Bottom Panel": "Toggle Bottom Panel",
|
||||
"Bottom Panel": "Bottom Panel",
|
||||
"Show Keybindings Dialog": "Show Keybindings Dialog",
|
||||
"Toggle Terminal Bottom Panel": "Toggle Terminal Bottom Panel",
|
||||
"Toggle Logs Bottom Panel": "Toggle Logs Bottom Panel",
|
||||
"Toggle Focus Mode": "Toggle Focus Mode",
|
||||
"Toggle Model Library Sidebar": "Toggle Model Library Sidebar",
|
||||
"Toggle Node Library Sidebar": "Toggle Node Library Sidebar",
|
||||
"Toggle Queue Sidebar": "Toggle Queue Sidebar",
|
||||
"Toggle Workflows Sidebar": "Toggle Workflows Sidebar"
|
||||
"Toggle Essential Bottom Panel": "Toggle Essential Bottom Panel",
|
||||
"Toggle View Controls Bottom Panel": "Toggle View Controls Bottom Panel",
|
||||
"Focus Mode": "Focus Mode",
|
||||
"Model Library": "Model Library",
|
||||
"Node Library": "Node Library",
|
||||
"Queue Panel": "Queue Panel",
|
||||
"Workflows": "Workflows"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"reinstall": "Reinstall",
|
||||
@@ -1075,7 +1096,8 @@
|
||||
"User": "User",
|
||||
"Credits": "Credits",
|
||||
"API Nodes": "API Nodes",
|
||||
"Notification Preferences": "Notification Preferences"
|
||||
"Notification Preferences": "Notification Preferences",
|
||||
"3DViewer": "3DViewer"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -1359,6 +1381,13 @@
|
||||
"outdatedVersionGeneric": "Some nodes require a newer version of ComfyUI. Please update to use all nodes.",
|
||||
"coreNodesFromVersion": "Requires ComfyUI {version}:"
|
||||
},
|
||||
"versionMismatchWarning": {
|
||||
"title": "Version Compatibility Warning",
|
||||
"frontendOutdated": "Frontend version {frontendVersion} is outdated. Backend requires version {requiredVersion} or higher.",
|
||||
"frontendNewer": "Frontend version {frontendVersion} may not be compatible with backend version {backendVersion}.",
|
||||
"updateFrontend": "Update Frontend",
|
||||
"dismiss": "Dismiss"
|
||||
},
|
||||
"errorDialog": {
|
||||
"defaultTitle": "An error occurred",
|
||||
"loadWorkflowTitle": "Loading aborted due to error reloading workflow data",
|
||||
@@ -1420,12 +1449,31 @@
|
||||
"depth": "Depth",
|
||||
"lineart": "Lineart"
|
||||
},
|
||||
"upDirections": {
|
||||
"original": "Original"
|
||||
},
|
||||
"startRecording": "Start Recording",
|
||||
"stopRecording": "Stop Recording",
|
||||
"exportRecording": "Export Recording",
|
||||
"clearRecording": "Clear Recording",
|
||||
"resizeNodeMatchOutput": "Resize Node to match output",
|
||||
"loadingBackgroundImage": "Loading Background Image"
|
||||
"loadingBackgroundImage": "Loading Background Image",
|
||||
"cameraType": {
|
||||
"perspective": "Perspective",
|
||||
"orthographic": "Orthographic"
|
||||
},
|
||||
"viewer": {
|
||||
"title": "3D Viewer (Beta)",
|
||||
"apply": "Apply",
|
||||
"cancel": "Cancel",
|
||||
"cameraType": "Camera Type",
|
||||
"sceneSettings": "Scene Settings",
|
||||
"cameraSettings": "Camera Settings",
|
||||
"lightSettings": "Light Settings",
|
||||
"exportSettings": "Export Settings",
|
||||
"modelSettings": "Model Settings"
|
||||
},
|
||||
"openIn3DViewer": "Open in 3D Viewer"
|
||||
},
|
||||
"toastMessages": {
|
||||
"nothingToQueue": "Nothing to queue",
|
||||
@@ -1463,7 +1511,8 @@
|
||||
"useApiKeyTip": "Tip: Can't access normal login? Use the Comfy API Key option.",
|
||||
"nothingSelected": "Nothing selected",
|
||||
"cannotCreateSubgraph": "Cannot create subgraph",
|
||||
"failedToConvertToSubgraph": "Failed to convert items to subgraph"
|
||||
"failedToConvertToSubgraph": "Failed to convert items to subgraph",
|
||||
"failedToInitializeLoad3dViewer": "Failed to initialize 3D Viewer"
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
@@ -1616,5 +1665,32 @@
|
||||
"whatsNewPopup": {
|
||||
"learnMore": "Learn more",
|
||||
"noReleaseNotes": "No release notes available."
|
||||
},
|
||||
"breadcrumbsMenu": {
|
||||
"duplicate": "Duplicate",
|
||||
"clearWorkflow": "Clear Workflow",
|
||||
"deleteWorkflow": "Delete Workflow",
|
||||
"enterNewName": "Enter new name"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "Essential",
|
||||
"viewControls": "View Controls",
|
||||
"manageShortcuts": "Manage Shortcuts",
|
||||
"noKeybinding": "No keybinding",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"subcategories": {
|
||||
"workflow": "Workflow",
|
||||
"node": "Node",
|
||||
"queue": "Queue",
|
||||
"view": "View",
|
||||
"panelControls": "Panel Controls"
|
||||
}
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Node Colors",
|
||||
"showLinks": "Show Links",
|
||||
"showGroups": "Show Frames/Groups",
|
||||
"renderBypassState": "Render Bypass State",
|
||||
"renderErrorState": "Render Error State"
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Contactar soporte"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "Mostrar selector de modelo (Dev)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Duplicar flujo de trabajo actual"
|
||||
},
|
||||
@@ -164,20 +167,8 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Cargar flujo de trabajo predeterminado"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Nodos personalizados (Beta)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "Nodos personalizados (heredados)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "Menú del administrador (heredado)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Instalar faltantes"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Buscar actualizaciones"
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Administrador de nodos personalizados"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Alternar diálogo de progreso del administrador"
|
||||
@@ -191,12 +182,6 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Abrir editor de máscara para el nodo seleccionado"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Descargar modelos"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Descargar modelos y caché de ejecución"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Nuevo flujo de trabajo en blanco"
|
||||
},
|
||||
|
||||
@@ -272,11 +272,11 @@
|
||||
"category": "Categoría",
|
||||
"choose_file_to_upload": "elige archivo para subir",
|
||||
"clear": "Limpiar",
|
||||
"clearFilters": "Borrar filtros",
|
||||
"close": "Cerrar",
|
||||
"color": "Color",
|
||||
"comingSoon": "Próximamente",
|
||||
"command": "Comando",
|
||||
"commandProhibited": "El comando {command} está prohibido. Contacta a un administrador para más información.",
|
||||
"community": "Comunidad",
|
||||
"completed": "Completado",
|
||||
"confirm": "Confirmar",
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "Deshabilitando",
|
||||
"dismiss": "Descartar",
|
||||
"download": "Descargar",
|
||||
"duplicate": "Duplicar",
|
||||
"edit": "Editar",
|
||||
"empty": "Vacío",
|
||||
"enableAll": "Habilitar todo",
|
||||
@@ -569,6 +570,10 @@
|
||||
"applyingTexture": "Aplicando textura...",
|
||||
"backgroundColor": "Color de fondo",
|
||||
"camera": "Cámara",
|
||||
"cameraType": {
|
||||
"orthographic": "Ortográfica",
|
||||
"perspective": "Perspectiva"
|
||||
},
|
||||
"clearRecording": "Borrar grabación",
|
||||
"edgeThreshold": "Umbral de borde",
|
||||
"export": "Exportar",
|
||||
@@ -589,6 +594,7 @@
|
||||
"wireframe": "Malla"
|
||||
},
|
||||
"model": "Modelo",
|
||||
"openIn3DViewer": "Abrir en el visor 3D",
|
||||
"previewOutput": "Vista previa de salida",
|
||||
"removeBackgroundImage": "Eliminar imagen de fondo",
|
||||
"resizeNodeMatchOutput": "Redimensionar nodo para coincidir con la salida",
|
||||
@@ -599,8 +605,22 @@
|
||||
"switchCamera": "Cambiar cámara",
|
||||
"switchingMaterialMode": "Cambiando modo de material...",
|
||||
"upDirection": "Dirección hacia arriba",
|
||||
"upDirections": {
|
||||
"original": "Original"
|
||||
},
|
||||
"uploadBackgroundImage": "Subir imagen de fondo",
|
||||
"uploadTexture": "Subir textura"
|
||||
"uploadTexture": "Subir textura",
|
||||
"viewer": {
|
||||
"apply": "Aplicar",
|
||||
"cameraSettings": "Configuración de la cámara",
|
||||
"cameraType": "Tipo de cámara",
|
||||
"cancel": "Cancelar",
|
||||
"exportSettings": "Configuración de exportación",
|
||||
"lightSettings": "Configuración de la luz",
|
||||
"modelSettings": "Configuración del modelo",
|
||||
"sceneSettings": "Configuración de la escena",
|
||||
"title": "Visor 3D (Beta)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "Requiere ComfyUI {version}:",
|
||||
@@ -647,9 +667,6 @@
|
||||
"installationQueue": "Cola de Instalación",
|
||||
"lastUpdated": "Última Actualización",
|
||||
"latestVersion": "Última",
|
||||
"legacyManagerUI": "Usar UI antigua",
|
||||
"legacyManagerUIDescription": "Para usar la UI antigua del Manager, inicia ComfyUI con --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "El menú del administrador antiguo no está disponible en esta versión de ComfyUI. Por favor, utiliza el nuevo menú del administrador en su lugar.",
|
||||
"license": "Licencia",
|
||||
"loadingVersions": "Cargando versiones...",
|
||||
"nightlyVersion": "Nocturna",
|
||||
@@ -732,6 +749,7 @@
|
||||
"manageExtensions": "Gestionar extensiones",
|
||||
"onChange": "Al cambiar",
|
||||
"onChangeTooltip": "El flujo de trabajo se encolará una vez que se haga un cambio",
|
||||
"queue": "Panel de cola",
|
||||
"refresh": "Actualizar definiciones de nodos",
|
||||
"resetView": "Restablecer vista del lienzo",
|
||||
"run": "Ejecutar",
|
||||
@@ -745,12 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "Acerca de ComfyUI",
|
||||
"Add Edit Model Step": "Agregar paso de edición de modelo",
|
||||
"Bottom Panel": "Panel inferior",
|
||||
"Browse Templates": "Explorar plantillas",
|
||||
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
|
||||
"Canvas Toggle Link Visibility": "Alternar visibilidad de enlace en lienzo",
|
||||
"Canvas Performance": "Rendimiento del lienzo",
|
||||
"Canvas Toggle Lock": "Alternar bloqueo en lienzo",
|
||||
"Canvas Toggle Minimap": "Lienzo: Alternar minimapa",
|
||||
"Check for Custom Node Updates": "Buscar actualizaciones de nodos personalizados",
|
||||
"Check for Updates": "Buscar actualizaciones",
|
||||
"Clear Pending Tasks": "Borrar tareas pendientes",
|
||||
"Clear Workflow": "Borrar flujo de trabajo",
|
||||
@@ -764,27 +781,28 @@
|
||||
"Contact Support": "Contactar soporte",
|
||||
"Convert Selection to Subgraph": "Convertir selección en subgrafo",
|
||||
"Convert selected nodes to group node": "Convertir nodos seleccionados en nodo de grupo",
|
||||
"Custom Nodes (Legacy)": "Nodos personalizados (heredado)",
|
||||
"Custom Nodes Manager": "Administrador de Nodos Personalizados",
|
||||
"Decrease Brush Size in MaskEditor": "Disminuir tamaño del pincel en MaskEditor",
|
||||
"Delete Selected Items": "Eliminar elementos seleccionados",
|
||||
"Desktop User Guide": "Guía de usuario de escritorio",
|
||||
"Duplicate Current Workflow": "Duplicar flujo de trabajo actual",
|
||||
"Edit": "Editar",
|
||||
"Exit Subgraph": "Salir de subgrafo",
|
||||
"Export": "Exportar",
|
||||
"Export (API)": "Exportar (API)",
|
||||
"File": "Archivo",
|
||||
"Fit Group To Contents": "Ajustar grupo a contenidos",
|
||||
"Fit view to selected nodes": "Ajustar vista a los nodos seleccionados",
|
||||
"Focus Mode": "Modo de enfoque",
|
||||
"Give Feedback": "Dar retroalimentación",
|
||||
"Group Selected Nodes": "Agrupar nodos seleccionados",
|
||||
"Help": "Ayuda",
|
||||
"Help Center": "Centro de ayuda",
|
||||
"Increase Brush Size in MaskEditor": "Aumentar tamaño del pincel en MaskEditor",
|
||||
"Install Missing Custom Nodes": "Instalar nodos personalizados faltantes",
|
||||
"Interrupt": "Interrumpir",
|
||||
"Load Default Workflow": "Cargar flujo de trabajo predeterminado",
|
||||
"Manage group nodes": "Gestionar nodos de grupo",
|
||||
"Manager": "Administrador",
|
||||
"Manager Menu (Legacy)": "Menú de gestión (heredado)",
|
||||
"Minimap": "Minimapa",
|
||||
"Model Library": "Biblioteca de modelos",
|
||||
"Move Selected Nodes Down": "Mover nodos seleccionados hacia abajo",
|
||||
"Move Selected Nodes Left": "Mover nodos seleccionados hacia la izquierda",
|
||||
"Move Selected Nodes Right": "Mover nodos seleccionados hacia la derecha",
|
||||
@@ -792,7 +810,10 @@
|
||||
"Mute/Unmute Selected Nodes": "Silenciar/Activar sonido de nodos seleccionados",
|
||||
"New": "Nuevo",
|
||||
"Next Opened Workflow": "Siguiente flujo de trabajo abierto",
|
||||
"Node Library": "Biblioteca de nodos",
|
||||
"Node Links": "Enlaces de nodos",
|
||||
"Open": "Abrir",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Abrir visor 3D (Beta) para el nodo seleccionado",
|
||||
"Open Custom Nodes Folder": "Abrir carpeta de nodos personalizados",
|
||||
"Open DevTools": "Abrir DevTools",
|
||||
"Open Inputs Folder": "Abrir carpeta de entradas",
|
||||
@@ -805,6 +826,7 @@
|
||||
"Pin/Unpin Selected Items": "Anclar/Desanclar elementos seleccionados",
|
||||
"Pin/Unpin Selected Nodes": "Anclar/Desanclar nodos seleccionados",
|
||||
"Previous Opened Workflow": "Flujo de trabajo abierto anterior",
|
||||
"Queue Panel": "Panel de cola",
|
||||
"Queue Prompt": "Indicador de cola",
|
||||
"Queue Prompt (Front)": "Indicador de cola (Frente)",
|
||||
"Queue Selected Output Nodes": "Encolar nodos de salida seleccionados",
|
||||
@@ -817,26 +839,32 @@
|
||||
"Restart": "Reiniciar",
|
||||
"Save": "Guardar",
|
||||
"Save As": "Guardar como",
|
||||
"Show Keybindings Dialog": "Mostrar diálogo de combinaciones de teclas",
|
||||
"Show Model Selector (Dev)": "Mostrar selector de modelo (Desarrollo)",
|
||||
"Show Settings Dialog": "Mostrar diálogo de configuración",
|
||||
"Sign Out": "Cerrar sesión",
|
||||
"Toggle Bottom Panel": "Alternar panel inferior",
|
||||
"Toggle Focus Mode": "Alternar modo de enfoque",
|
||||
"Toggle Essential Bottom Panel": "Alternar panel inferior esencial",
|
||||
"Toggle Logs Bottom Panel": "Alternar panel inferior de registros",
|
||||
"Toggle Model Library Sidebar": "Alternar barra lateral de la biblioteca de modelos",
|
||||
"Toggle Node Library Sidebar": "Alternar barra lateral de la biblioteca de nodos",
|
||||
"Toggle Queue Sidebar": "Alternar barra lateral de la cola",
|
||||
"Toggle Search Box": "Alternar caja de búsqueda",
|
||||
"Toggle Terminal Bottom Panel": "Alternar panel inferior de terminal",
|
||||
"Toggle Theme (Dark/Light)": "Alternar tema (Oscuro/Claro)",
|
||||
"Toggle Workflows Sidebar": "Alternar barra lateral de los flujos de trabajo",
|
||||
"Toggle View Controls Bottom Panel": "Alternar panel inferior de controles de vista",
|
||||
"Toggle the Custom Nodes Manager": "Alternar el Administrador de Nodos Personalizados",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
|
||||
"Undo": "Deshacer",
|
||||
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
|
||||
"Unload Models": "Descargar modelos",
|
||||
"Unload Models and Execution Cache": "Descargar modelos y caché de ejecución",
|
||||
"Workflow": "Flujo de trabajo",
|
||||
"Unpack the selected Subgraph": "Desempaquetar el Subgrafo seleccionado",
|
||||
"Workflows": "Flujos de trabajo",
|
||||
"Zoom In": "Acercar",
|
||||
"Zoom Out": "Alejar"
|
||||
"Zoom Out": "Alejar",
|
||||
"Zoom to fit": "Ajustar al tamaño"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Colores de nodos",
|
||||
"renderBypassState": "Mostrar estado de omisión",
|
||||
"renderErrorState": "Mostrar estado de error",
|
||||
"showGroups": "Mostrar marcos/grupos",
|
||||
"showLinks": "Mostrar enlaces"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "No mostrar esto de nuevo",
|
||||
@@ -1104,6 +1132,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "Visor 3D",
|
||||
"API Nodes": "Nodos API",
|
||||
"About": "Acerca de",
|
||||
"Appearance": "Apariencia",
|
||||
@@ -1155,10 +1184,31 @@
|
||||
"Window": "Ventana",
|
||||
"Workflow": "Flujo de Trabajo"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "Esenciales",
|
||||
"keyboardShortcuts": "Atajos de teclado",
|
||||
"manageShortcuts": "Gestionar atajos",
|
||||
"noKeybinding": "Sin asignación de tecla",
|
||||
"subcategories": {
|
||||
"node": "Nodo",
|
||||
"panelControls": "Controles del panel",
|
||||
"queue": "Cola",
|
||||
"view": "Vista",
|
||||
"workflow": "Flujo de trabajo"
|
||||
},
|
||||
"viewControls": "Controles de vista"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "Explorar plantillas de ejemplo",
|
||||
"downloads": "Descargas",
|
||||
"helpCenter": "Centro de ayuda",
|
||||
"labels": {
|
||||
"models": "Modelos",
|
||||
"nodes": "Nodos",
|
||||
"queue": "Cola",
|
||||
"templates": "Plantillas",
|
||||
"workflows": "Flujos de trabajo"
|
||||
},
|
||||
"logout": "Cerrar sesión",
|
||||
"modelLibrary": "Biblioteca de modelos",
|
||||
"newBlankWorkflow": "Crear un nuevo flujo de trabajo en blanco",
|
||||
@@ -1196,6 +1246,7 @@
|
||||
},
|
||||
"showFlatList": "Mostrar lista plana"
|
||||
},
|
||||
"templates": "Plantillas",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "¿Estás seguro de que quieres eliminar este flujo de trabajo?",
|
||||
"confirmDeleteTitle": "¿Eliminar flujo de trabajo?",
|
||||
@@ -1242,6 +1293,8 @@
|
||||
"Video": "Video",
|
||||
"Video API": "API de Video"
|
||||
},
|
||||
"loadingMore": "Cargando más plantillas...",
|
||||
"searchPlaceholder": "Buscar plantillas...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
|
||||
@@ -1564,6 +1617,7 @@
|
||||
"failedToExportModel": "Error al exportar modelo como {format}",
|
||||
"failedToFetchBalance": "No se pudo obtener el saldo: {error}",
|
||||
"failedToFetchLogs": "Error al obtener los registros del servidor",
|
||||
"failedToInitializeLoad3dViewer": "No se pudo inicializar el visor 3D",
|
||||
"failedToInitiateCreditPurchase": "No se pudo iniciar la compra de créditos: {error}",
|
||||
"failedToPurchaseCredits": "No se pudo comprar créditos: {error}",
|
||||
"fileLoadError": "No se puede encontrar el flujo de trabajo en {fileName}",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Contacter le support"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "Afficher le sélecteur de modèle (Dev)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Dupliquer le flux de travail actuel"
|
||||
},
|
||||
@@ -164,20 +167,8 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Charger le flux de travail par défaut"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Nœuds personnalisés (Beta)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "Nœuds personnalisés (hérités)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "Menu du gestionnaire (héritage)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Installer manquants"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Vérifier les mises à jour"
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Gestionnaire de Nœuds Personnalisés"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Basculer la boîte de dialogue de progression"
|
||||
@@ -191,12 +182,6 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Ouvrir l'éditeur de masque pour le nœud sélectionné"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Décharger les modèles"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Décharger les modèles et le cache d'exécution"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Nouveau flux de travail vierge"
|
||||
},
|
||||
|
||||
@@ -272,11 +272,11 @@
|
||||
"category": "Catégorie",
|
||||
"choose_file_to_upload": "choisissez le fichier à télécharger",
|
||||
"clear": "Effacer",
|
||||
"clearFilters": "Effacer les filtres",
|
||||
"close": "Fermer",
|
||||
"color": "Couleur",
|
||||
"comingSoon": "Bientôt disponible",
|
||||
"command": "Commande",
|
||||
"commandProhibited": "La commande {command} est interdite. Contactez un administrateur pour plus d'informations.",
|
||||
"community": "Communauté",
|
||||
"completed": "Terminé",
|
||||
"confirm": "Confirmer",
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "Désactivation",
|
||||
"dismiss": "Fermer",
|
||||
"download": "Télécharger",
|
||||
"duplicate": "Dupliquer",
|
||||
"edit": "Modifier",
|
||||
"empty": "Vide",
|
||||
"enableAll": "Activer tout",
|
||||
@@ -569,6 +570,10 @@
|
||||
"applyingTexture": "Application de la texture...",
|
||||
"backgroundColor": "Couleur de fond",
|
||||
"camera": "Caméra",
|
||||
"cameraType": {
|
||||
"orthographic": "Orthographique",
|
||||
"perspective": "Perspective"
|
||||
},
|
||||
"clearRecording": "Effacer l'enregistrement",
|
||||
"edgeThreshold": "Seuil de Bordure",
|
||||
"export": "Exportation",
|
||||
@@ -589,6 +594,7 @@
|
||||
"wireframe": "Fil de fer"
|
||||
},
|
||||
"model": "Modèle",
|
||||
"openIn3DViewer": "Ouvrir dans la visionneuse 3D",
|
||||
"previewOutput": "Aperçu de la sortie",
|
||||
"removeBackgroundImage": "Supprimer l'image de fond",
|
||||
"resizeNodeMatchOutput": "Redimensionner le nœud pour correspondre à la sortie",
|
||||
@@ -599,8 +605,22 @@
|
||||
"switchCamera": "Changer de caméra",
|
||||
"switchingMaterialMode": "Changement de mode de matériau...",
|
||||
"upDirection": "Direction Haut",
|
||||
"upDirections": {
|
||||
"original": "Original"
|
||||
},
|
||||
"uploadBackgroundImage": "Télécharger l'image de fond",
|
||||
"uploadTexture": "Télécharger Texture"
|
||||
"uploadTexture": "Télécharger Texture",
|
||||
"viewer": {
|
||||
"apply": "Appliquer",
|
||||
"cameraSettings": "Paramètres de la caméra",
|
||||
"cameraType": "Type de caméra",
|
||||
"cancel": "Annuler",
|
||||
"exportSettings": "Paramètres d’exportation",
|
||||
"lightSettings": "Paramètres de l’éclairage",
|
||||
"modelSettings": "Paramètres du modèle",
|
||||
"sceneSettings": "Paramètres de la scène",
|
||||
"title": "Visionneuse 3D (Bêta)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "Nécessite ComfyUI {version} :",
|
||||
@@ -647,9 +667,6 @@
|
||||
"installationQueue": "File d'attente d'installation",
|
||||
"lastUpdated": "Dernière mise à jour",
|
||||
"latestVersion": "Dernière",
|
||||
"legacyManagerUI": "Utiliser l'interface utilisateur héritée",
|
||||
"legacyManagerUIDescription": "Pour utiliser l'interface utilisateur de gestion héritée, démarrez ComfyUI avec --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "Le menu du gestionnaire de l'ancienne version n'est pas disponible dans cette version de ComfyUI. Veuillez utiliser le nouveau menu du gestionnaire à la place.",
|
||||
"license": "Licence",
|
||||
"loadingVersions": "Chargement des versions...",
|
||||
"nightlyVersion": "Nocturne",
|
||||
@@ -732,6 +749,7 @@
|
||||
"manageExtensions": "Gérer les extensions",
|
||||
"onChange": "Sur modification",
|
||||
"onChangeTooltip": "Le flux de travail sera mis en file d'attente une fois une modification effectuée",
|
||||
"queue": "Panneau de file d’attente",
|
||||
"refresh": "Actualiser les définitions des nœuds",
|
||||
"resetView": "Réinitialiser la vue du canevas",
|
||||
"run": "Exécuter",
|
||||
@@ -745,12 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "À propos de ComfyUI",
|
||||
"Add Edit Model Step": "Ajouter une étape d’édition de modèle",
|
||||
"Bottom Panel": "Panneau inférieur",
|
||||
"Browse Templates": "Parcourir les modèles",
|
||||
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
|
||||
"Canvas Toggle Link Visibility": "Basculer la visibilité du lien de la toile",
|
||||
"Canvas Performance": "Performance du canevas",
|
||||
"Canvas Toggle Lock": "Basculer le verrouillage de la toile",
|
||||
"Canvas Toggle Minimap": "Basculer la mini-carte du canevas",
|
||||
"Check for Custom Node Updates": "Vérifier les mises à jour des nœuds personnalisés",
|
||||
"Check for Updates": "Vérifier les mises à jour",
|
||||
"Clear Pending Tasks": "Effacer les tâches en attente",
|
||||
"Clear Workflow": "Effacer le flux de travail",
|
||||
@@ -764,27 +781,28 @@
|
||||
"Contact Support": "Contacter le support",
|
||||
"Convert Selection to Subgraph": "Convertir la sélection en sous-graphe",
|
||||
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
||||
"Custom Nodes (Legacy)": "Nœuds personnalisés (héritage)",
|
||||
"Custom Nodes Manager": "Gestionnaire de Nœuds Personnalisés",
|
||||
"Decrease Brush Size in MaskEditor": "Réduire la taille du pinceau dans MaskEditor",
|
||||
"Delete Selected Items": "Supprimer les éléments sélectionnés",
|
||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
|
||||
"Edit": "Éditer",
|
||||
"Exit Subgraph": "Quitter le sous-graphe",
|
||||
"Export": "Exporter",
|
||||
"Export (API)": "Exporter (API)",
|
||||
"File": "Fichier",
|
||||
"Fit Group To Contents": "Ajuster le groupe au contenu",
|
||||
"Fit view to selected nodes": "Ajuster la vue aux nœuds sélectionnés",
|
||||
"Focus Mode": "Mode focus",
|
||||
"Give Feedback": "Donnez votre avis",
|
||||
"Group Selected Nodes": "Grouper les nœuds sélectionnés",
|
||||
"Help": "Aide",
|
||||
"Help Center": "Centre d’aide",
|
||||
"Increase Brush Size in MaskEditor": "Augmenter la taille du pinceau dans MaskEditor",
|
||||
"Install Missing Custom Nodes": "Installer les nœuds personnalisés manquants",
|
||||
"Interrupt": "Interrompre",
|
||||
"Load Default Workflow": "Charger le flux de travail par défaut",
|
||||
"Manage group nodes": "Gérer les nœuds de groupe",
|
||||
"Manager": "Gestionnaire",
|
||||
"Manager Menu (Legacy)": "Menu du gestionnaire (héritage)",
|
||||
"Minimap": "Minicarte",
|
||||
"Model Library": "Bibliothèque de modèles",
|
||||
"Move Selected Nodes Down": "Déplacer les nœuds sélectionnés vers le bas",
|
||||
"Move Selected Nodes Left": "Déplacer les nœuds sélectionnés vers la gauche",
|
||||
"Move Selected Nodes Right": "Déplacer les nœuds sélectionnés vers la droite",
|
||||
@@ -792,7 +810,10 @@
|
||||
"Mute/Unmute Selected Nodes": "Mettre en sourdine/Activer le son des nœuds sélectionnés",
|
||||
"New": "Nouveau",
|
||||
"Next Opened Workflow": "Prochain flux de travail ouvert",
|
||||
"Node Library": "Bibliothèque de nœuds",
|
||||
"Node Links": "Liens de nœuds",
|
||||
"Open": "Ouvrir",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Ouvrir le visualiseur 3D (bêta) pour le nœud sélectionné",
|
||||
"Open Custom Nodes Folder": "Ouvrir le dossier des nœuds personnalisés",
|
||||
"Open DevTools": "Ouvrir DevTools",
|
||||
"Open Inputs Folder": "Ouvrir le dossier des entrées",
|
||||
@@ -805,6 +826,7 @@
|
||||
"Pin/Unpin Selected Items": "Épingler/Désépingler les éléments sélectionnés",
|
||||
"Pin/Unpin Selected Nodes": "Épingler/Désépingler les nœuds sélectionnés",
|
||||
"Previous Opened Workflow": "Flux de travail ouvert précédent",
|
||||
"Queue Panel": "Panneau de file d’attente",
|
||||
"Queue Prompt": "Invite de file d'attente",
|
||||
"Queue Prompt (Front)": "Invite de file d'attente (Front)",
|
||||
"Queue Selected Output Nodes": "Mettre en file d’attente les nœuds de sortie sélectionnés",
|
||||
@@ -817,26 +839,32 @@
|
||||
"Restart": "Redémarrer",
|
||||
"Save": "Enregistrer",
|
||||
"Save As": "Enregistrer sous",
|
||||
"Show Keybindings Dialog": "Afficher la boîte de dialogue des raccourcis clavier",
|
||||
"Show Model Selector (Dev)": "Afficher le sélecteur de modèle (Dev)",
|
||||
"Show Settings Dialog": "Afficher la boîte de dialogue des paramètres",
|
||||
"Sign Out": "Se déconnecter",
|
||||
"Toggle Bottom Panel": "Basculer le panneau inférieur",
|
||||
"Toggle Focus Mode": "Basculer le mode focus",
|
||||
"Toggle Essential Bottom Panel": "Afficher/Masquer le panneau inférieur essentiel",
|
||||
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
|
||||
"Toggle Model Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de modèles",
|
||||
"Toggle Node Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de nœuds",
|
||||
"Toggle Queue Sidebar": "Afficher/Masquer la barre latérale de la file d’attente",
|
||||
"Toggle Search Box": "Basculer la boîte de recherche",
|
||||
"Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal",
|
||||
"Toggle Theme (Dark/Light)": "Basculer le thème (Sombre/Clair)",
|
||||
"Toggle Workflows Sidebar": "Afficher/Masquer la barre latérale des workflows",
|
||||
"Toggle View Controls Bottom Panel": "Afficher/Masquer le panneau inférieur des contrôles de vue",
|
||||
"Toggle the Custom Nodes Manager": "Basculer le gestionnaire de nœuds personnalisés",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
|
||||
"Undo": "Annuler",
|
||||
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
|
||||
"Unload Models": "Décharger les modèles",
|
||||
"Unload Models and Execution Cache": "Décharger les modèles et le cache d'exécution",
|
||||
"Workflow": "Flux de travail",
|
||||
"Unpack the selected Subgraph": "Décompresser le Subgraph sélectionné",
|
||||
"Workflows": "Flux de travail",
|
||||
"Zoom In": "Zoom avant",
|
||||
"Zoom Out": "Zoom arrière"
|
||||
"Zoom Out": "Zoom arrière",
|
||||
"Zoom to fit": "Ajuster à l’écran"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Couleurs des nœuds",
|
||||
"renderBypassState": "Afficher l’état de contournement",
|
||||
"renderErrorState": "Afficher l’état d’erreur",
|
||||
"showGroups": "Afficher les cadres/groupes",
|
||||
"showLinks": "Afficher les liens"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Ne plus afficher ce message",
|
||||
@@ -1104,6 +1132,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "Visionneuse 3D",
|
||||
"API Nodes": "Nœuds API",
|
||||
"About": "À Propos",
|
||||
"Appearance": "Apparence",
|
||||
@@ -1155,10 +1184,31 @@
|
||||
"Window": "Fenêtre",
|
||||
"Workflow": "Flux de Travail"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "Essentiel",
|
||||
"keyboardShortcuts": "Raccourcis clavier",
|
||||
"manageShortcuts": "Gérer les raccourcis",
|
||||
"noKeybinding": "Aucun raccourci",
|
||||
"subcategories": {
|
||||
"node": "Nœud",
|
||||
"panelControls": "Contrôles du panneau",
|
||||
"queue": "File d’attente",
|
||||
"view": "Vue",
|
||||
"workflow": "Flux de travail"
|
||||
},
|
||||
"viewControls": "Contrôles d’affichage"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "Parcourir les modèles d'exemple",
|
||||
"downloads": "Téléchargements",
|
||||
"helpCenter": "Centre d'aide",
|
||||
"labels": {
|
||||
"models": "Modèles",
|
||||
"nodes": "Nœuds",
|
||||
"queue": "File d’attente",
|
||||
"templates": "Modèles",
|
||||
"workflows": "Flux de travail"
|
||||
},
|
||||
"logout": "Déconnexion",
|
||||
"modelLibrary": "Bibliothèque de modèles",
|
||||
"newBlankWorkflow": "Créer un nouveau flux de travail vierge",
|
||||
@@ -1196,6 +1246,7 @@
|
||||
},
|
||||
"showFlatList": "Afficher la liste plate"
|
||||
},
|
||||
"templates": "Modèles",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "Êtes-vous sûr de vouloir supprimer ce flux de travail ?",
|
||||
"confirmDeleteTitle": "Supprimer le flux de travail ?",
|
||||
@@ -1242,6 +1293,8 @@
|
||||
"Video": "Vidéo",
|
||||
"Video API": "API vidéo"
|
||||
},
|
||||
"loadingMore": "Chargement de plus de modèles...",
|
||||
"searchPlaceholder": "Rechercher des modèles...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D",
|
||||
@@ -1564,6 +1617,7 @@
|
||||
"failedToExportModel": "Échec de l'exportation du modèle en {format}",
|
||||
"failedToFetchBalance": "Échec de la récupération du solde : {error}",
|
||||
"failedToFetchLogs": "Échec de la récupération des journaux du serveur",
|
||||
"failedToInitializeLoad3dViewer": "Échec de l'initialisation du visualiseur 3D",
|
||||
"failedToInitiateCreditPurchase": "Échec de l'initiation de l'achat de crédits : {error}",
|
||||
"failedToPurchaseCredits": "Échec de l'achat de crédits : {error}",
|
||||
"fileLoadError": "Impossible de trouver le flux de travail dans {fileName}",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "サポートに連絡"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "モデルセレクターを表示(開発用)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "現在のワークフローを複製"
|
||||
},
|
||||
@@ -164,20 +167,8 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "デフォルトのワークフローを読み込む"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "カスタムノード(ベータ版)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "カスタムノード(レガシー)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "マネージャーメニュー(レガシー)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "不足しているパックをインストール"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "更新を確認"
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "カスタムノードマネージャ"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "プログレスダイアログの切り替え"
|
||||
@@ -191,12 +182,6 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "選択したノードのマスクエディタを開く"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "モデルのアンロード"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "モデルと実行キャッシュのアンロード"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新しい空のワークフロー"
|
||||
},
|
||||
|
||||
@@ -272,11 +272,11 @@
|
||||
"category": "カテゴリ",
|
||||
"choose_file_to_upload": "アップロードするファイルを選択",
|
||||
"clear": "クリア",
|
||||
"clearFilters": "フィルターをクリア",
|
||||
"close": "閉じる",
|
||||
"color": "色",
|
||||
"comingSoon": "近日公開",
|
||||
"command": "コマンド",
|
||||
"commandProhibited": "コマンド {command} は禁止されています。詳細は管理者にお問い合わせください。",
|
||||
"community": "コミュニティ",
|
||||
"completed": "完了",
|
||||
"confirm": "確認",
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "無効化",
|
||||
"dismiss": "閉じる",
|
||||
"download": "ダウンロード",
|
||||
"duplicate": "複製",
|
||||
"edit": "編集",
|
||||
"empty": "空",
|
||||
"enableAll": "すべて有効にする",
|
||||
@@ -569,6 +570,10 @@
|
||||
"applyingTexture": "テクスチャを適用中...",
|
||||
"backgroundColor": "背景色",
|
||||
"camera": "カメラ",
|
||||
"cameraType": {
|
||||
"orthographic": "オルソグラフィック",
|
||||
"perspective": "パースペクティブ"
|
||||
},
|
||||
"clearRecording": "録画をクリア",
|
||||
"edgeThreshold": "エッジ閾値",
|
||||
"export": "エクスポート",
|
||||
@@ -589,6 +594,7 @@
|
||||
"wireframe": "ワイヤーフレーム"
|
||||
},
|
||||
"model": "モデル",
|
||||
"openIn3DViewer": "3Dビューアで開く",
|
||||
"previewOutput": "出力のプレビュー",
|
||||
"removeBackgroundImage": "背景画像を削除",
|
||||
"resizeNodeMatchOutput": "ノードを出力に合わせてリサイズ",
|
||||
@@ -599,8 +605,22 @@
|
||||
"switchCamera": "カメラを切り替える",
|
||||
"switchingMaterialMode": "マテリアルモードの切り替え中...",
|
||||
"upDirection": "上方向",
|
||||
"upDirections": {
|
||||
"original": "オリジナル"
|
||||
},
|
||||
"uploadBackgroundImage": "背景画像をアップロード",
|
||||
"uploadTexture": "テクスチャをアップロード"
|
||||
"uploadTexture": "テクスチャをアップロード",
|
||||
"viewer": {
|
||||
"apply": "適用",
|
||||
"cameraSettings": "カメラ設定",
|
||||
"cameraType": "カメラタイプ",
|
||||
"cancel": "キャンセル",
|
||||
"exportSettings": "エクスポート設定",
|
||||
"lightSettings": "ライト設定",
|
||||
"modelSettings": "モデル設定",
|
||||
"sceneSettings": "シーン設定",
|
||||
"title": "3Dビューア(ベータ)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "ComfyUI {version} が必要です:",
|
||||
@@ -647,9 +667,6 @@
|
||||
"installationQueue": "インストールキュー",
|
||||
"lastUpdated": "最終更新日",
|
||||
"latestVersion": "最新",
|
||||
"legacyManagerUI": "レガシーUIを使用する",
|
||||
"legacyManagerUIDescription": "レガシーManager UIを使用するには、--enable-manager-legacy-uiを付けてComfyUIを起動してください",
|
||||
"legacyMenuNotAvailable": "このバージョンのComfyUIでは、レガシーマネージャーメニューは利用できません。新しいマネージャーメニューを使用してください。",
|
||||
"license": "ライセンス",
|
||||
"loadingVersions": "バージョンを読み込んでいます...",
|
||||
"nightlyVersion": "ナイトリー",
|
||||
@@ -732,6 +749,7 @@
|
||||
"manageExtensions": "拡張機能の管理",
|
||||
"onChange": "変更時",
|
||||
"onChangeTooltip": "変更が行われるとワークフローがキューに追加されます",
|
||||
"queue": "キューパネル",
|
||||
"refresh": "ノードを更新",
|
||||
"resetView": "ビューをリセット",
|
||||
"run": "実行する",
|
||||
@@ -745,12 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "ComfyUIについて",
|
||||
"Add Edit Model Step": "モデル編集ステップを追加",
|
||||
"Bottom Panel": "下部パネル",
|
||||
"Browse Templates": "テンプレートを参照",
|
||||
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
|
||||
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
|
||||
"Canvas Performance": "キャンバスパフォーマンス",
|
||||
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
|
||||
"Canvas Toggle Minimap": "キャンバス ミニマップの切り替え",
|
||||
"Check for Custom Node Updates": "カスタムノードのアップデートを確認",
|
||||
"Check for Updates": "更新を確認する",
|
||||
"Clear Pending Tasks": "保留中のタスクをクリア",
|
||||
"Clear Workflow": "ワークフローをクリア",
|
||||
@@ -764,27 +781,28 @@
|
||||
"Contact Support": "サポートに連絡",
|
||||
"Convert Selection to Subgraph": "選択範囲をサブグラフに変換",
|
||||
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
|
||||
"Custom Nodes (Legacy)": "カスタムノード(レガシー)",
|
||||
"Custom Nodes Manager": "カスタムノードマネージャ",
|
||||
"Decrease Brush Size in MaskEditor": "マスクエディタでブラシサイズを小さくする",
|
||||
"Delete Selected Items": "選択したアイテムを削除",
|
||||
"Desktop User Guide": "デスクトップユーザーガイド",
|
||||
"Duplicate Current Workflow": "現在のワークフローを複製",
|
||||
"Edit": "編集",
|
||||
"Exit Subgraph": "サブグラフを終了",
|
||||
"Export": "エクスポート",
|
||||
"Export (API)": "エクスポート (API)",
|
||||
"File": "ファイル",
|
||||
"Fit Group To Contents": "グループを内容に合わせる",
|
||||
"Fit view to selected nodes": "選択したノードにビューを合わせる",
|
||||
"Focus Mode": "フォーカスモード",
|
||||
"Give Feedback": "フィードバックを送る",
|
||||
"Group Selected Nodes": "選択したノードをグループ化",
|
||||
"Help": "ヘルプ",
|
||||
"Help Center": "ヘルプセンター",
|
||||
"Increase Brush Size in MaskEditor": "マスクエディタでブラシサイズを大きくする",
|
||||
"Install Missing Custom Nodes": "不足しているカスタムノードをインストール",
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "デフォルトワークフローを読み込む",
|
||||
"Manage group nodes": "グループノードを管理",
|
||||
"Manager": "マネージャー",
|
||||
"Manager Menu (Legacy)": "マネージャーメニュー(レガシー)",
|
||||
"Minimap": "ミニマップ",
|
||||
"Model Library": "モデルライブラリ",
|
||||
"Move Selected Nodes Down": "選択したノードを下へ移動",
|
||||
"Move Selected Nodes Left": "選択したノードを左へ移動",
|
||||
"Move Selected Nodes Right": "選択したノードを右へ移動",
|
||||
@@ -792,7 +810,10 @@
|
||||
"Mute/Unmute Selected Nodes": "選択したノードのミュート/ミュート解除",
|
||||
"New": "新規",
|
||||
"Next Opened Workflow": "次に開いたワークフロー",
|
||||
"Node Library": "ノードライブラリ",
|
||||
"Node Links": "ノードリンク",
|
||||
"Open": "開く",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "選択したノードの3Dビューアー(ベータ)を開く",
|
||||
"Open Custom Nodes Folder": "カスタムノードフォルダを開く",
|
||||
"Open DevTools": "DevToolsを開く",
|
||||
"Open Inputs Folder": "入力フォルダを開く",
|
||||
@@ -805,6 +826,7 @@
|
||||
"Pin/Unpin Selected Items": "選択したアイテムのピン留め/ピン留め解除",
|
||||
"Pin/Unpin Selected Nodes": "選択したノードのピン留め/ピン留め解除",
|
||||
"Previous Opened Workflow": "前に開いたワークフロー",
|
||||
"Queue Panel": "キューパネル",
|
||||
"Queue Prompt": "キューのプロンプト",
|
||||
"Queue Prompt (Front)": "キューのプロンプト (前面)",
|
||||
"Queue Selected Output Nodes": "選択した出力ノードをキューに追加",
|
||||
@@ -817,26 +839,32 @@
|
||||
"Restart": "再起動",
|
||||
"Save": "保存",
|
||||
"Save As": "名前を付けて保存",
|
||||
"Show Keybindings Dialog": "キーバインドダイアログを表示",
|
||||
"Show Model Selector (Dev)": "モデルセレクターを表示(開発用)",
|
||||
"Show Settings Dialog": "設定ダイアログを表示",
|
||||
"Sign Out": "サインアウト",
|
||||
"Toggle Bottom Panel": "下部パネルの切り替え",
|
||||
"Toggle Focus Mode": "フォーカスモードの切り替え",
|
||||
"Toggle Essential Bottom Panel": "エッセンシャル下部パネルの切り替え",
|
||||
"Toggle Logs Bottom Panel": "ログパネル下部を切り替え",
|
||||
"Toggle Model Library Sidebar": "モデルライブラリサイドバーを切り替え",
|
||||
"Toggle Node Library Sidebar": "ノードライブラリサイドバーを切り替え",
|
||||
"Toggle Queue Sidebar": "キューサイドバーを切り替え",
|
||||
"Toggle Search Box": "検索ボックスの切り替え",
|
||||
"Toggle Terminal Bottom Panel": "ターミナルパネル下部を切り替え",
|
||||
"Toggle Theme (Dark/Light)": "テーマを切り替え(ダーク/ライト)",
|
||||
"Toggle Workflows Sidebar": "ワークフローサイドバーを切り替え",
|
||||
"Toggle View Controls Bottom Panel": "ビューコントロール下部パネルの切り替え",
|
||||
"Toggle the Custom Nodes Manager": "カスタムノードマネージャーを切り替え",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え",
|
||||
"Undo": "元に戻す",
|
||||
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
|
||||
"Unload Models": "モデルのアンロード",
|
||||
"Unload Models and Execution Cache": "モデルと実行キャッシュのアンロード",
|
||||
"Workflow": "ワークフロー",
|
||||
"Unpack the selected Subgraph": "選択したサブグラフを展開",
|
||||
"Workflows": "ワークフロー",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト"
|
||||
"Zoom Out": "ズームアウト",
|
||||
"Zoom to fit": "全体表示にズーム"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "ノードの色",
|
||||
"renderBypassState": "バイパス状態を表示",
|
||||
"renderErrorState": "エラー状態を表示",
|
||||
"showGroups": "フレーム/グループを表示",
|
||||
"showLinks": "リンクを表示"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "再度表示しない",
|
||||
@@ -1104,6 +1132,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3Dビューア",
|
||||
"API Nodes": "APIノード",
|
||||
"About": "情報",
|
||||
"Appearance": "外観",
|
||||
@@ -1155,10 +1184,31 @@
|
||||
"Window": "ウィンドウ",
|
||||
"Workflow": "ワークフロー"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "基本",
|
||||
"keyboardShortcuts": "キーボードショートカット",
|
||||
"manageShortcuts": "ショートカットの管理",
|
||||
"noKeybinding": "キー割り当てなし",
|
||||
"subcategories": {
|
||||
"node": "ノード",
|
||||
"panelControls": "パネルコントロール",
|
||||
"queue": "キュー",
|
||||
"view": "ビュー",
|
||||
"workflow": "ワークフロー"
|
||||
},
|
||||
"viewControls": "表示コントロール"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "サンプルテンプレートを表示",
|
||||
"downloads": "ダウンロード",
|
||||
"helpCenter": "ヘルプセンター",
|
||||
"labels": {
|
||||
"models": "モデル",
|
||||
"nodes": "ノード",
|
||||
"queue": "キュー",
|
||||
"templates": "テンプレート",
|
||||
"workflows": "ワークフロー"
|
||||
},
|
||||
"logout": "ログアウト",
|
||||
"modelLibrary": "モデルライブラリ",
|
||||
"newBlankWorkflow": "新しい空のワークフローを作成",
|
||||
@@ -1196,6 +1246,7 @@
|
||||
},
|
||||
"showFlatList": "フラットリストを表示"
|
||||
},
|
||||
"templates": "テンプレート",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "このワークフローを削除してもよろしいですか?",
|
||||
"confirmDeleteTitle": "ワークフローを削除しますか?",
|
||||
@@ -1242,6 +1293,8 @@
|
||||
"Video": "ビデオ",
|
||||
"Video API": "動画API"
|
||||
},
|
||||
"loadingMore": "さらにテンプレートを読み込み中...",
|
||||
"searchPlaceholder": "テンプレートを検索...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D",
|
||||
@@ -1564,6 +1617,7 @@
|
||||
"failedToExportModel": "{format}としてモデルのエクスポートに失敗しました",
|
||||
"failedToFetchBalance": "残高の取得に失敗しました: {error}",
|
||||
"failedToFetchLogs": "サーバーログの取得に失敗しました",
|
||||
"failedToInitializeLoad3dViewer": "3Dビューアの初期化に失敗しました",
|
||||
"failedToInitiateCreditPurchase": "クレジット購入の開始に失敗しました: {error}",
|
||||
"failedToPurchaseCredits": "クレジットの購入に失敗しました: {error}",
|
||||
"fileLoadError": "{fileName}でワークフローが見つかりません",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "지원팀에 문의하기"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "모델 선택기 표시 (개발자용)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "현재 워크플로 복제"
|
||||
},
|
||||
@@ -164,20 +167,8 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "기본 워크플로 로드"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "사용자 정의 노드 (베타)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "커스텀 노드 (레거시)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "매니저 메뉴 (레거시)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "누락된 팩 설치"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "업데이트 확인"
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "사용자 정의 노드 관리자"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "진행 상황 대화 상자 전환"
|
||||
@@ -191,12 +182,6 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "선택한 노드 마스크 편집기 열기"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "모델 언로드"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "모델 및 실행 캐시 언로드"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "새로운 빈 워크플로"
|
||||
},
|
||||
|
||||
@@ -272,11 +272,11 @@
|
||||
"category": "카테고리",
|
||||
"choose_file_to_upload": "업로드할 파일 선택",
|
||||
"clear": "지우기",
|
||||
"clearFilters": "필터 지우기",
|
||||
"close": "닫기",
|
||||
"color": "색상",
|
||||
"comingSoon": "곧 출시 예정",
|
||||
"command": "명령",
|
||||
"commandProhibited": "명령 {command}은 금지되었습니다. 자세한 정보는 관리자에게 문의하십시오.",
|
||||
"community": "커뮤니티",
|
||||
"completed": "완료됨",
|
||||
"confirm": "확인",
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "비활성화 중",
|
||||
"dismiss": "닫기",
|
||||
"download": "다운로드",
|
||||
"duplicate": "복제",
|
||||
"edit": "편집",
|
||||
"empty": "비어 있음",
|
||||
"enableAll": "모두 활성화",
|
||||
@@ -569,6 +570,10 @@
|
||||
"applyingTexture": "텍스처 적용 중...",
|
||||
"backgroundColor": "배경색",
|
||||
"camera": "카메라",
|
||||
"cameraType": {
|
||||
"orthographic": "직교",
|
||||
"perspective": "원근"
|
||||
},
|
||||
"clearRecording": "녹화 지우기",
|
||||
"edgeThreshold": "엣지 임계값",
|
||||
"export": "내보내기",
|
||||
@@ -589,6 +594,7 @@
|
||||
"wireframe": "와이어프레임"
|
||||
},
|
||||
"model": "모델",
|
||||
"openIn3DViewer": "3D 뷰어에서 열기",
|
||||
"previewOutput": "출력 미리보기",
|
||||
"removeBackgroundImage": "배경 이미지 제거",
|
||||
"resizeNodeMatchOutput": "노드 크기를 출력에 맞추기",
|
||||
@@ -599,8 +605,22 @@
|
||||
"switchCamera": "카메라 전환",
|
||||
"switchingMaterialMode": "재질 모드 전환 중...",
|
||||
"upDirection": "위 방향",
|
||||
"upDirections": {
|
||||
"original": "원본"
|
||||
},
|
||||
"uploadBackgroundImage": "배경 이미지 업로드",
|
||||
"uploadTexture": "텍스처 업로드"
|
||||
"uploadTexture": "텍스처 업로드",
|
||||
"viewer": {
|
||||
"apply": "적용",
|
||||
"cameraSettings": "카메라 설정",
|
||||
"cameraType": "카메라 유형",
|
||||
"cancel": "취소",
|
||||
"exportSettings": "내보내기 설정",
|
||||
"lightSettings": "조명 설정",
|
||||
"modelSettings": "모델 설정",
|
||||
"sceneSettings": "씬 설정",
|
||||
"title": "3D 뷰어 (베타)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "ComfyUI {version} 이상 필요:",
|
||||
@@ -647,9 +667,6 @@
|
||||
"installationQueue": "설치 대기열",
|
||||
"lastUpdated": "마지막 업데이트",
|
||||
"latestVersion": "최신",
|
||||
"legacyManagerUI": "레거시 UI 사용",
|
||||
"legacyManagerUIDescription": "레거시 매니저 UI를 사용하려면, ComfyUI를 --enable-manager-legacy-ui로 시작하세요",
|
||||
"legacyMenuNotAvailable": "이 버전의 ComfyUI에서는 레거시 매니저 메뉴를 사용할 수 없습니다. 대신 새로운 매니저 메뉴를 사용하십시오.",
|
||||
"license": "라이선스",
|
||||
"loadingVersions": "버전 로딩 중...",
|
||||
"nightlyVersion": "최신 테스트 버전(nightly)",
|
||||
@@ -732,6 +749,7 @@
|
||||
"manageExtensions": "확장 프로그램 관리",
|
||||
"onChange": "변경 시",
|
||||
"onChangeTooltip": "변경이 있는 경우에만 워크플로를 실행 대기열에 추가합니다.",
|
||||
"queue": "대기열 패널",
|
||||
"refresh": "노드 정의 새로 고침",
|
||||
"resetView": "캔버스 보기 재설정",
|
||||
"run": "실행",
|
||||
@@ -745,12 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "ComfyUI에 대하여",
|
||||
"Add Edit Model Step": "모델 편집 단계 추가",
|
||||
"Bottom Panel": "하단 패널",
|
||||
"Browse Templates": "템플릿 탐색",
|
||||
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
|
||||
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
|
||||
"Canvas Performance": "캔버스 성능",
|
||||
"Canvas Toggle Lock": "캔버스 토글 잠금",
|
||||
"Canvas Toggle Minimap": "캔버스 미니맵 전환",
|
||||
"Check for Custom Node Updates": "커스텀 노드 업데이트 확인",
|
||||
"Check for Updates": "업데이트 확인",
|
||||
"Clear Pending Tasks": "보류 중인 작업 제거하기",
|
||||
"Clear Workflow": "워크플로 지우기",
|
||||
@@ -764,27 +781,28 @@
|
||||
"Contact Support": "고객 지원 문의",
|
||||
"Convert Selection to Subgraph": "선택 영역을 서브그래프로 변환",
|
||||
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
|
||||
"Custom Nodes (Legacy)": "커스텀 노드(레거시)",
|
||||
"Custom Nodes Manager": "사용자 정의 노드 관리자",
|
||||
"Decrease Brush Size in MaskEditor": "마스크 편집기에서 브러시 크기 줄이기",
|
||||
"Delete Selected Items": "선택한 항목 삭제",
|
||||
"Desktop User Guide": "데스크톱 사용자 가이드",
|
||||
"Duplicate Current Workflow": "현재 워크플로 복제",
|
||||
"Edit": "편집",
|
||||
"Exit Subgraph": "서브그래프 종료",
|
||||
"Export": "내보내기",
|
||||
"Export (API)": "내보내기 (API)",
|
||||
"File": "파일",
|
||||
"Fit Group To Contents": "그룹을 내용에 맞게 조정",
|
||||
"Fit view to selected nodes": "선택한 노드에 맞게 보기 조정",
|
||||
"Focus Mode": "포커스 모드",
|
||||
"Give Feedback": "피드백 제공",
|
||||
"Group Selected Nodes": "선택한 노드 그룹화",
|
||||
"Help": "도움말",
|
||||
"Help Center": "도움말 센터",
|
||||
"Increase Brush Size in MaskEditor": "마스크 편집기에서 브러시 크기 늘리기",
|
||||
"Install Missing Custom Nodes": "누락된 커스텀 노드 설치",
|
||||
"Interrupt": "중단",
|
||||
"Load Default Workflow": "기본 워크플로 불러오기",
|
||||
"Manage group nodes": "그룹 노드 관리",
|
||||
"Manager": "매니저",
|
||||
"Manager Menu (Legacy)": "매니저 메뉴(레거시)",
|
||||
"Minimap": "미니맵",
|
||||
"Model Library": "모델 라이브러리",
|
||||
"Move Selected Nodes Down": "선택한 노드 아래로 이동",
|
||||
"Move Selected Nodes Left": "선택한 노드 왼쪽으로 이동",
|
||||
"Move Selected Nodes Right": "선택한 노드 오른쪽으로 이동",
|
||||
@@ -792,7 +810,10 @@
|
||||
"Mute/Unmute Selected Nodes": "선택한 노드 활성화/비활성화",
|
||||
"New": "새로 만들기",
|
||||
"Next Opened Workflow": "다음 열린 워크플로",
|
||||
"Node Library": "노드 라이브러리",
|
||||
"Node Links": "노드 링크",
|
||||
"Open": "열기",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "선택한 노드에 대해 3D 뷰어(베타) 열기",
|
||||
"Open Custom Nodes Folder": "사용자 정의 노드 폴더 열기",
|
||||
"Open DevTools": "개발자 도구 열기",
|
||||
"Open Inputs Folder": "입력 폴더 열기",
|
||||
@@ -805,6 +826,7 @@
|
||||
"Pin/Unpin Selected Items": "선택한 항목 고정/고정 해제",
|
||||
"Pin/Unpin Selected Nodes": "선택한 노드 고정/고정 해제",
|
||||
"Previous Opened Workflow": "이전 열린 워크플로",
|
||||
"Queue Panel": "대기열 패널",
|
||||
"Queue Prompt": "실행 대기열에 프롬프트 추가",
|
||||
"Queue Prompt (Front)": "실행 대기열 맨 앞에 프롬프트 추가",
|
||||
"Queue Selected Output Nodes": "선택한 출력 노드 대기열에 추가",
|
||||
@@ -817,26 +839,32 @@
|
||||
"Restart": "재시작",
|
||||
"Save": "저장",
|
||||
"Save As": "다른 이름으로 저장",
|
||||
"Show Keybindings Dialog": "단축키 대화상자 표시",
|
||||
"Show Model Selector (Dev)": "모델 선택기 표시 (개발자용)",
|
||||
"Show Settings Dialog": "설정 대화상자 표시",
|
||||
"Sign Out": "로그아웃",
|
||||
"Toggle Bottom Panel": "하단 패널 전환",
|
||||
"Toggle Focus Mode": "포커스 모드 전환",
|
||||
"Toggle Essential Bottom Panel": "필수 하단 패널 전환",
|
||||
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
|
||||
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
|
||||
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
|
||||
"Toggle Queue Sidebar": "실행 대기열 사이드바 전환",
|
||||
"Toggle Search Box": "검색 상자 전환",
|
||||
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
|
||||
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
|
||||
"Toggle Workflows Sidebar": "워크플로우 사이드바 전환",
|
||||
"Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환",
|
||||
"Toggle the Custom Nodes Manager": "커스텀 노드 매니저 전환",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
|
||||
"Undo": "실행 취소",
|
||||
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
|
||||
"Unload Models": "모델 언로드",
|
||||
"Unload Models and Execution Cache": "모델 및 실행 캐시 언로드",
|
||||
"Workflow": "워크플로",
|
||||
"Unpack the selected Subgraph": "선택한 서브그래프 풀기",
|
||||
"Workflows": "워크플로우",
|
||||
"Zoom In": "확대",
|
||||
"Zoom Out": "축소"
|
||||
"Zoom Out": "축소",
|
||||
"Zoom to fit": "화면에 맞추기"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "노드 색상",
|
||||
"renderBypassState": "바이패스 상태 렌더링",
|
||||
"renderErrorState": "에러 상태 렌더링",
|
||||
"showGroups": "프레임/그룹 표시",
|
||||
"showLinks": "링크 표시"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "다시 보지 않기",
|
||||
@@ -1104,6 +1132,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D뷰어",
|
||||
"API Nodes": "API 노드",
|
||||
"About": "정보",
|
||||
"Appearance": "모양",
|
||||
@@ -1155,10 +1184,31 @@
|
||||
"Window": "창",
|
||||
"Workflow": "워크플로"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "필수",
|
||||
"keyboardShortcuts": "키보드 단축키",
|
||||
"manageShortcuts": "단축키 관리",
|
||||
"noKeybinding": "단축키 없음",
|
||||
"subcategories": {
|
||||
"node": "노드",
|
||||
"panelControls": "패널 컨트롤",
|
||||
"queue": "대기열",
|
||||
"view": "보기",
|
||||
"workflow": "워크플로우"
|
||||
},
|
||||
"viewControls": "보기 컨트롤"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "예제 템플릿 탐색",
|
||||
"downloads": "다운로드",
|
||||
"helpCenter": "도움말 센터",
|
||||
"labels": {
|
||||
"models": "모델",
|
||||
"nodes": "노드",
|
||||
"queue": "대기열",
|
||||
"templates": "템플릿",
|
||||
"workflows": "워크플로우"
|
||||
},
|
||||
"logout": "로그아웃",
|
||||
"modelLibrary": "모델 라이브러리",
|
||||
"newBlankWorkflow": "새 빈 워크플로 만들기",
|
||||
@@ -1196,6 +1246,7 @@
|
||||
},
|
||||
"showFlatList": "평면 목록 표시"
|
||||
},
|
||||
"templates": "템플릿",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "정말로 이 워크플로를 삭제하시겠습니까?",
|
||||
"confirmDeleteTitle": "워크플로 삭제",
|
||||
@@ -1242,6 +1293,8 @@
|
||||
"Video": "비디오",
|
||||
"Video API": "비디오 API"
|
||||
},
|
||||
"loadingMore": "템플릿을 더 불러오는 중...",
|
||||
"searchPlaceholder": "템플릿 검색...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
|
||||
@@ -1564,6 +1617,7 @@
|
||||
"failedToExportModel": "{format} 형식으로 모델 내보내기에 실패했습니다",
|
||||
"failedToFetchBalance": "잔액을 가져오지 못했습니다: {error}",
|
||||
"failedToFetchLogs": "서버 로그를 가져오는 데 실패했습니다",
|
||||
"failedToInitializeLoad3dViewer": "3D 뷰어 초기화에 실패했습니다",
|
||||
"failedToInitiateCreditPurchase": "크레딧 구매를 시작하지 못했습니다: {error}",
|
||||
"failedToPurchaseCredits": "크레딧 구매에 실패했습니다: {error}",
|
||||
"fileLoadError": "{fileName}에서 워크플로를 찾을 수 없습니다",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Связаться с поддержкой"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "Показать выбор модели (Dev)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Дублировать текущий рабочий процесс"
|
||||
},
|
||||
@@ -164,20 +167,8 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Загрузить стандартный рабочий процесс"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Пользовательские узлы (Бета)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "Пользовательские узлы (устаревшие)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "Меню менеджера (устаревшее)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Установить отсутствующие"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Проверить наличие обновлений"
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Менеджер Пользовательских Узлов"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Переключить диалоговое окно прогресса"
|
||||
@@ -191,12 +182,6 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Открыть редактор масок для выбранной ноды"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Выгрузить модели"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Выгрузить модели и кэш выполнения"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Новый пустой рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -272,11 +272,11 @@
|
||||
"category": "Категория",
|
||||
"choose_file_to_upload": "выберите файл для загрузки",
|
||||
"clear": "Очистить",
|
||||
"clearFilters": "Сбросить фильтры",
|
||||
"close": "Закрыть",
|
||||
"color": "Цвет",
|
||||
"comingSoon": "Скоро будет",
|
||||
"command": "Команда",
|
||||
"commandProhibited": "Команда {command} запрещена. Свяжитесь с администратором для получения дополнительной информации.",
|
||||
"community": "Сообщество",
|
||||
"completed": "Завершено",
|
||||
"confirm": "Подтвердить",
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "Отключение",
|
||||
"dismiss": "Закрыть",
|
||||
"download": "Скачать",
|
||||
"duplicate": "Дублировать",
|
||||
"edit": "Редактировать",
|
||||
"empty": "Пусто",
|
||||
"enableAll": "Включить все",
|
||||
@@ -569,6 +570,10 @@
|
||||
"applyingTexture": "Применение текстуры...",
|
||||
"backgroundColor": "Цвет фона",
|
||||
"camera": "Камера",
|
||||
"cameraType": {
|
||||
"orthographic": "Ортографическая",
|
||||
"perspective": "Перспективная"
|
||||
},
|
||||
"clearRecording": "Очистить запись",
|
||||
"edgeThreshold": "Пороговое значение края",
|
||||
"export": "Экспорт",
|
||||
@@ -589,6 +594,7 @@
|
||||
"wireframe": "Каркас"
|
||||
},
|
||||
"model": "Модель",
|
||||
"openIn3DViewer": "Открыть в 3D просмотрщике",
|
||||
"previewOutput": "Предварительный просмотр",
|
||||
"removeBackgroundImage": "Удалить фоновое изображение",
|
||||
"resizeNodeMatchOutput": "Изменить размер узла под вывод",
|
||||
@@ -599,8 +605,22 @@
|
||||
"switchCamera": "Переключить камеру",
|
||||
"switchingMaterialMode": "Переключение режима материала...",
|
||||
"upDirection": "Направление Вверх",
|
||||
"upDirections": {
|
||||
"original": "Оригинал"
|
||||
},
|
||||
"uploadBackgroundImage": "Загрузить фоновое изображение",
|
||||
"uploadTexture": "Загрузить текстуру"
|
||||
"uploadTexture": "Загрузить текстуру",
|
||||
"viewer": {
|
||||
"apply": "Применить",
|
||||
"cameraSettings": "Настройки камеры",
|
||||
"cameraType": "Тип камеры",
|
||||
"cancel": "Отмена",
|
||||
"exportSettings": "Настройки экспорта",
|
||||
"lightSettings": "Настройки освещения",
|
||||
"modelSettings": "Настройки модели",
|
||||
"sceneSettings": "Настройки сцены",
|
||||
"title": "3D Просмотрщик (Бета)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "Требуется ComfyUI {version}:",
|
||||
@@ -647,9 +667,6 @@
|
||||
"installationQueue": "Очередь установки",
|
||||
"lastUpdated": "Последнее обновление",
|
||||
"latestVersion": "Последняя",
|
||||
"legacyManagerUI": "Использовать устаревший UI",
|
||||
"legacyManagerUIDescription": "Чтобы использовать устаревший UI менеджера, запустите ComfyUI с --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "Устаревшее меню менеджера недоступно в этой версии ComfyUI. Пожалуйста, используйте новое меню менеджера.",
|
||||
"license": "Лицензия",
|
||||
"loadingVersions": "Загрузка версий...",
|
||||
"nightlyVersion": "Ночная",
|
||||
@@ -732,6 +749,7 @@
|
||||
"manageExtensions": "Управление расширениями",
|
||||
"onChange": "При изменении",
|
||||
"onChangeTooltip": "Рабочий процесс будет поставлен в очередь после внесения изменений",
|
||||
"queue": "Панель очереди",
|
||||
"refresh": "Обновить определения нод",
|
||||
"resetView": "Сбросить вид холста",
|
||||
"run": "Запустить",
|
||||
@@ -745,12 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "О ComfyUI",
|
||||
"Add Edit Model Step": "Добавить или изменить шаг модели",
|
||||
"Bottom Panel": "Нижняя панель",
|
||||
"Browse Templates": "Просмотреть шаблоны",
|
||||
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
|
||||
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
|
||||
"Canvas Performance": "Производительность холста",
|
||||
"Canvas Toggle Lock": "Переключение блокировки холста",
|
||||
"Canvas Toggle Minimap": "Показать/скрыть миникарту на холсте",
|
||||
"Check for Custom Node Updates": "Проверить обновления пользовательских узлов",
|
||||
"Check for Updates": "Проверить наличие обновлений",
|
||||
"Clear Pending Tasks": "Очистить ожидающие задачи",
|
||||
"Clear Workflow": "Очистить рабочий процесс",
|
||||
@@ -764,27 +781,28 @@
|
||||
"Contact Support": "Связаться с поддержкой",
|
||||
"Convert Selection to Subgraph": "Преобразовать выделенное в подграф",
|
||||
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
|
||||
"Custom Nodes (Legacy)": "Пользовательские узлы (устаревшие)",
|
||||
"Custom Nodes Manager": "Менеджер Пользовательских Узлов",
|
||||
"Decrease Brush Size in MaskEditor": "Уменьшить размер кисти в MaskEditor",
|
||||
"Delete Selected Items": "Удалить выбранные элементы",
|
||||
"Desktop User Guide": "Руководство пользователя для настольных ПК",
|
||||
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
|
||||
"Edit": "Редактировать",
|
||||
"Exit Subgraph": "Выйти из подграфа",
|
||||
"Export": "Экспортировать",
|
||||
"Export (API)": "Экспорт (API)",
|
||||
"File": "Файл",
|
||||
"Fit Group To Contents": "Подогнать группу под содержимое",
|
||||
"Fit view to selected nodes": "Подогнать вид под выбранные ноды",
|
||||
"Focus Mode": "Режим фокуса",
|
||||
"Give Feedback": "Оставить отзыв",
|
||||
"Group Selected Nodes": "Сгруппировать выбранные ноды",
|
||||
"Help": "Помощь",
|
||||
"Help Center": "Центр поддержки",
|
||||
"Increase Brush Size in MaskEditor": "Увеличить размер кисти в MaskEditor",
|
||||
"Install Missing Custom Nodes": "Установить отсутствующие пользовательские узлы",
|
||||
"Interrupt": "Прервать",
|
||||
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
|
||||
"Manage group nodes": "Управление групповыми нодами",
|
||||
"Manager": "Менеджер",
|
||||
"Manager Menu (Legacy)": "Меню управления (устаревшее)",
|
||||
"Minimap": "Мини-карта",
|
||||
"Model Library": "Библиотека моделей",
|
||||
"Move Selected Nodes Down": "Переместить выбранные узлы вниз",
|
||||
"Move Selected Nodes Left": "Переместить выбранные узлы влево",
|
||||
"Move Selected Nodes Right": "Переместить выбранные узлы вправо",
|
||||
@@ -792,7 +810,10 @@
|
||||
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных нод",
|
||||
"New": "Новый",
|
||||
"Next Opened Workflow": "Следующий открытый рабочий процесс",
|
||||
"Node Library": "Библиотека узлов",
|
||||
"Node Links": "Связи узлов",
|
||||
"Open": "Открыть",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Открыть 3D-просмотрщик (бета) для выбранного узла",
|
||||
"Open Custom Nodes Folder": "Открыть папку пользовательских нод",
|
||||
"Open DevTools": "Открыть инструменты разработчика",
|
||||
"Open Inputs Folder": "Открыть папку входных данных",
|
||||
@@ -805,6 +826,7 @@
|
||||
"Pin/Unpin Selected Items": "Закрепить/открепить выбранные элементы",
|
||||
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные ноды",
|
||||
"Previous Opened Workflow": "Предыдущий открытый рабочий процесс",
|
||||
"Queue Panel": "Панель очереди",
|
||||
"Queue Prompt": "Запрос в очереди",
|
||||
"Queue Prompt (Front)": "Запрос в очереди (спереди)",
|
||||
"Queue Selected Output Nodes": "Добавить выбранные выходные узлы в очередь",
|
||||
@@ -817,26 +839,32 @@
|
||||
"Restart": "Перезапустить",
|
||||
"Save": "Сохранить",
|
||||
"Save As": "Сохранить как",
|
||||
"Show Keybindings Dialog": "Показать диалог клавиш быстрого доступа",
|
||||
"Show Model Selector (Dev)": "Показать выбор модели (Dev)",
|
||||
"Show Settings Dialog": "Показать диалог настроек",
|
||||
"Sign Out": "Выйти",
|
||||
"Toggle Bottom Panel": "Переключить нижнюю панель",
|
||||
"Toggle Focus Mode": "Переключить режим фокуса",
|
||||
"Toggle Essential Bottom Panel": "Показать/скрыть основную нижнюю панель",
|
||||
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
|
||||
"Toggle Model Library Sidebar": "Показать/скрыть боковую панель библиотеки моделей",
|
||||
"Toggle Node Library Sidebar": "Показать/скрыть боковую панель библиотеки узлов",
|
||||
"Toggle Queue Sidebar": "Показать/скрыть боковую панель очереди",
|
||||
"Toggle Search Box": "Переключить поисковую панель",
|
||||
"Toggle Terminal Bottom Panel": "Переключение нижней панели терминала",
|
||||
"Toggle Theme (Dark/Light)": "Переключение темы (Тёмная/Светлая)",
|
||||
"Toggle Workflows Sidebar": "Показать/скрыть боковую панель рабочих процессов",
|
||||
"Toggle View Controls Bottom Panel": "Показать/скрыть панель управления просмотром",
|
||||
"Toggle the Custom Nodes Manager": "Переключить менеджер пользовательских узлов",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
|
||||
"Undo": "Отменить",
|
||||
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
|
||||
"Unload Models": "Выгрузить модели",
|
||||
"Unload Models and Execution Cache": "Выгрузить модели и кэш выполнения",
|
||||
"Workflow": "Рабочий процесс",
|
||||
"Unpack the selected Subgraph": "Распаковать выбранный подграф",
|
||||
"Workflows": "Рабочие процессы",
|
||||
"Zoom In": "Увеличить",
|
||||
"Zoom Out": "Уменьшить"
|
||||
"Zoom Out": "Уменьшить",
|
||||
"Zoom to fit": "Масштабировать по размеру"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Цвета узлов",
|
||||
"renderBypassState": "Отображать состояние обхода",
|
||||
"renderErrorState": "Отображать состояние ошибки",
|
||||
"showGroups": "Показать фреймы/группы",
|
||||
"showLinks": "Показать связи"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Больше не показывать это",
|
||||
@@ -1104,6 +1132,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D-просмотрщик",
|
||||
"API Nodes": "API-узлы",
|
||||
"About": "О программе",
|
||||
"Appearance": "Внешний вид",
|
||||
@@ -1155,10 +1184,31 @@
|
||||
"Window": "Окно",
|
||||
"Workflow": "Рабочий процесс"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "Основные",
|
||||
"keyboardShortcuts": "Горячие клавиши",
|
||||
"manageShortcuts": "Управление горячими клавишами",
|
||||
"noKeybinding": "Нет сочетания клавиш",
|
||||
"subcategories": {
|
||||
"node": "Узел",
|
||||
"panelControls": "Управление панелью",
|
||||
"queue": "Очередь",
|
||||
"view": "Просмотр",
|
||||
"workflow": "Рабочий процесс"
|
||||
},
|
||||
"viewControls": "Управление просмотром"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "Просмотреть примеры шаблонов",
|
||||
"downloads": "Загрузки",
|
||||
"helpCenter": "Центр поддержки",
|
||||
"labels": {
|
||||
"models": "Модели",
|
||||
"nodes": "Узлы",
|
||||
"queue": "Очередь",
|
||||
"templates": "Шаблоны",
|
||||
"workflows": "Воркфлоу"
|
||||
},
|
||||
"logout": "Выйти",
|
||||
"modelLibrary": "Библиотека моделей",
|
||||
"newBlankWorkflow": "Создайте новый пустой рабочий процесс",
|
||||
@@ -1196,6 +1246,7 @@
|
||||
},
|
||||
"showFlatList": "Показать плоский список"
|
||||
},
|
||||
"templates": "Шаблоны",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "Вы уверены, что хотите удалить этот рабочий процесс?",
|
||||
"confirmDeleteTitle": "Удалить рабочий процесс?",
|
||||
@@ -1242,6 +1293,8 @@
|
||||
"Video": "Видео",
|
||||
"Video API": "Video API"
|
||||
},
|
||||
"loadingMore": "Загрузка дополнительных шаблонов...",
|
||||
"searchPlaceholder": "Поиск шаблонов...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D",
|
||||
@@ -1564,6 +1617,7 @@
|
||||
"failedToExportModel": "Не удалось экспортировать модель как {format}",
|
||||
"failedToFetchBalance": "Не удалось получить баланс: {error}",
|
||||
"failedToFetchLogs": "Не удалось получить серверные логи",
|
||||
"failedToInitializeLoad3dViewer": "Не удалось инициализировать 3D просмотрщик",
|
||||
"failedToInitiateCreditPurchase": "Не удалось начать покупку кредитов: {error}",
|
||||
"failedToPurchaseCredits": "Не удалось купить кредиты: {error}",
|
||||
"fileLoadError": "Не удалось найти рабочий процесс в {fileName}",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "聯絡支援"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "顯示模型選擇器(開發)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "複製目前工作流程"
|
||||
},
|
||||
@@ -164,20 +167,8 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "載入預設工作流程"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "自訂節點管理器"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "自訂節點(舊版)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "管理選單(舊版)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "安裝缺少的自訂節點"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "檢查自訂節點更新"
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "切換自訂節點管理器"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切換自訂節點管理器進度條"
|
||||
@@ -191,12 +182,6 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "為選取的節點開啟 Mask 編輯器"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "卸載模型"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "卸載模型與執行快取"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新增空白工作流程"
|
||||
},
|
||||
|
||||
@@ -272,11 +272,11 @@
|
||||
"category": "分類",
|
||||
"choose_file_to_upload": "選擇要上傳的檔案",
|
||||
"clear": "清除",
|
||||
"clearFilters": "清除篩選",
|
||||
"close": "關閉",
|
||||
"color": "顏色",
|
||||
"comingSoon": "即將推出",
|
||||
"command": "指令",
|
||||
"commandProhibited": "指令 {command} 已被禁止。如需更多資訊,請聯絡管理員。",
|
||||
"community": "社群",
|
||||
"completed": "已完成",
|
||||
"confirm": "確認",
|
||||
@@ -299,6 +299,7 @@
|
||||
"disabling": "停用中",
|
||||
"dismiss": "關閉",
|
||||
"download": "下載",
|
||||
"duplicate": "複製",
|
||||
"edit": "編輯",
|
||||
"empty": "空",
|
||||
"enableAll": "全部啟用",
|
||||
@@ -569,6 +570,10 @@
|
||||
"applyingTexture": "正在套用材質貼圖...",
|
||||
"backgroundColor": "背景顏色",
|
||||
"camera": "相機",
|
||||
"cameraType": {
|
||||
"orthographic": "正交",
|
||||
"perspective": "透視"
|
||||
},
|
||||
"clearRecording": "清除錄影",
|
||||
"edgeThreshold": "邊緣閾值",
|
||||
"export": "匯出",
|
||||
@@ -589,6 +594,7 @@
|
||||
"wireframe": "線框"
|
||||
},
|
||||
"model": "模型",
|
||||
"openIn3DViewer": "在 3D 檢視器中開啟",
|
||||
"previewOutput": "預覽輸出",
|
||||
"removeBackgroundImage": "移除背景圖片",
|
||||
"resizeNodeMatchOutput": "調整節點以符合輸出",
|
||||
@@ -599,8 +605,22 @@
|
||||
"switchCamera": "切換相機",
|
||||
"switchingMaterialMode": "正在切換材質模式...",
|
||||
"upDirection": "上方方向",
|
||||
"upDirections": {
|
||||
"original": "原始"
|
||||
},
|
||||
"uploadBackgroundImage": "上傳背景圖片",
|
||||
"uploadTexture": "上傳材質貼圖"
|
||||
"uploadTexture": "上傳材質貼圖",
|
||||
"viewer": {
|
||||
"apply": "套用",
|
||||
"cameraSettings": "相機設定",
|
||||
"cameraType": "相機類型",
|
||||
"cancel": "取消",
|
||||
"exportSettings": "匯出設定",
|
||||
"lightSettings": "燈光設定",
|
||||
"modelSettings": "模型設定",
|
||||
"sceneSettings": "場景設定",
|
||||
"title": "3D 檢視器(測試版)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "需要 ComfyUI {version}:",
|
||||
@@ -647,9 +667,6 @@
|
||||
"installationQueue": "安裝佇列",
|
||||
"lastUpdated": "最後更新",
|
||||
"latestVersion": "最新版本",
|
||||
"legacyManagerUI": "使用舊版介面",
|
||||
"legacyManagerUIDescription": "若要使用舊版管理介面,請以 --enable-manager-legacy-ui 啟動 ComfyUI",
|
||||
"legacyMenuNotAvailable": "舊版管理選單不可用,已預設切換至新版管理選單。",
|
||||
"license": "授權條款",
|
||||
"loadingVersions": "正在載入版本...",
|
||||
"nightlyVersion": "每夜建置版",
|
||||
@@ -732,6 +749,7 @@
|
||||
"manageExtensions": "管理擴充功能",
|
||||
"onChange": "變更時",
|
||||
"onChangeTooltip": "每當有變更時,工作流程會排入佇列",
|
||||
"queue": "佇列面板",
|
||||
"refresh": "重新整理節點定義",
|
||||
"resetView": "重設畫布視圖",
|
||||
"run": "執行",
|
||||
@@ -745,12 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "關於 ComfyUI",
|
||||
"Add Edit Model Step": "新增編輯模型步驟",
|
||||
"Bottom Panel": "底部面板",
|
||||
"Browse Templates": "瀏覽範本",
|
||||
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
|
||||
"Canvas Toggle Link Visibility": "切換連結可見性",
|
||||
"Canvas Performance": "畫布效能",
|
||||
"Canvas Toggle Lock": "切換畫布鎖定",
|
||||
"Canvas Toggle Minimap": "畫布切換小地圖",
|
||||
"Check for Custom Node Updates": "檢查自訂節點更新",
|
||||
"Check for Updates": "檢查更新",
|
||||
"Clear Pending Tasks": "清除待處理任務",
|
||||
"Clear Workflow": "清除工作流程",
|
||||
@@ -764,27 +781,28 @@
|
||||
"Contact Support": "聯絡支援",
|
||||
"Convert Selection to Subgraph": "將選取內容轉為子圖",
|
||||
"Convert selected nodes to group node": "將選取節點轉為群組節點",
|
||||
"Custom Nodes (Legacy)": "自訂節點(舊版)",
|
||||
"Custom Nodes Manager": "自訂節點管理員",
|
||||
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中減小筆刷大小",
|
||||
"Delete Selected Items": "刪除選取項目",
|
||||
"Desktop User Guide": "桌面應用程式使用指南",
|
||||
"Duplicate Current Workflow": "複製目前工作流程",
|
||||
"Edit": "編輯",
|
||||
"Exit Subgraph": "離開子圖",
|
||||
"Export": "匯出",
|
||||
"Export (API)": "匯出(API)",
|
||||
"File": "檔案",
|
||||
"Fit Group To Contents": "群組貼合內容",
|
||||
"Fit view to selected nodes": "視圖貼合選取節點",
|
||||
"Focus Mode": "專注模式",
|
||||
"Give Feedback": "提供意見回饋",
|
||||
"Group Selected Nodes": "群組選取節點",
|
||||
"Help": "說明",
|
||||
"Help Center": "說明中心",
|
||||
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
|
||||
"Install Missing Custom Nodes": "安裝缺少的自訂節點",
|
||||
"Interrupt": "中斷",
|
||||
"Load Default Workflow": "載入預設工作流程",
|
||||
"Manage group nodes": "管理群組節點",
|
||||
"Manager": "管理員",
|
||||
"Manager Menu (Legacy)": "管理員選單(舊版)",
|
||||
"Minimap": "縮圖地圖",
|
||||
"Model Library": "模型庫",
|
||||
"Move Selected Nodes Down": "選取節點下移",
|
||||
"Move Selected Nodes Left": "選取節點左移",
|
||||
"Move Selected Nodes Right": "選取節點右移",
|
||||
@@ -792,7 +810,10 @@
|
||||
"Mute/Unmute Selected Nodes": "靜音/取消靜音選取節點",
|
||||
"New": "新增",
|
||||
"Next Opened Workflow": "下一個已開啟的工作流程",
|
||||
"Node Library": "節點庫",
|
||||
"Node Links": "節點連結",
|
||||
"Open": "開啟",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "為選取的節點開啟 3D 檢視器(Beta 版)",
|
||||
"Open Custom Nodes Folder": "開啟自訂節點資料夾",
|
||||
"Open DevTools": "開啟開發者工具",
|
||||
"Open Inputs Folder": "開啟輸入資料夾",
|
||||
@@ -805,6 +826,7 @@
|
||||
"Pin/Unpin Selected Items": "釘選/取消釘選選取項目",
|
||||
"Pin/Unpin Selected Nodes": "釘選/取消釘選選取節點",
|
||||
"Previous Opened Workflow": "上一個已開啟的工作流程",
|
||||
"Queue Panel": "佇列面板",
|
||||
"Queue Prompt": "加入提示至佇列",
|
||||
"Queue Prompt (Front)": "將提示加入佇列前端",
|
||||
"Queue Selected Output Nodes": "將選取的輸出節點加入佇列",
|
||||
@@ -817,26 +839,32 @@
|
||||
"Restart": "重新啟動",
|
||||
"Save": "儲存",
|
||||
"Save As": "另存新檔",
|
||||
"Show Keybindings Dialog": "顯示快捷鍵對話框",
|
||||
"Show Model Selector (Dev)": "顯示模型選擇器(開發用)",
|
||||
"Show Settings Dialog": "顯示設定對話框",
|
||||
"Sign Out": "登出",
|
||||
"Toggle Bottom Panel": "切換下方面板",
|
||||
"Toggle Focus Mode": "切換專注模式",
|
||||
"Toggle Essential Bottom Panel": "切換基本下方面板",
|
||||
"Toggle Logs Bottom Panel": "切換日誌下方面板",
|
||||
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
|
||||
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
|
||||
"Toggle Queue Sidebar": "切換佇列側邊欄",
|
||||
"Toggle Search Box": "切換搜尋框",
|
||||
"Toggle Terminal Bottom Panel": "切換終端機底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切換主題(深色/淺色)",
|
||||
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
|
||||
"Toggle View Controls Bottom Panel": "切換檢視控制下方面板",
|
||||
"Toggle the Custom Nodes Manager": "切換自訂節點管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切換自訂節點管理器進度條",
|
||||
"Undo": "復原",
|
||||
"Ungroup selected group nodes": "取消群組選取的群組節點",
|
||||
"Unload Models": "卸載模型",
|
||||
"Unload Models and Execution Cache": "卸載模型與執行快取",
|
||||
"Workflow": "工作流程",
|
||||
"Unpack the selected Subgraph": "解包所選子圖",
|
||||
"Workflows": "工作流程",
|
||||
"Zoom In": "放大",
|
||||
"Zoom Out": "縮小"
|
||||
"Zoom Out": "縮小",
|
||||
"Zoom to fit": "縮放至適合大小"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "節點顏色",
|
||||
"renderBypassState": "顯示繞過狀態",
|
||||
"renderErrorState": "顯示錯誤狀態",
|
||||
"showGroups": "顯示框架/群組",
|
||||
"showLinks": "顯示連結"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不要再顯示此訊息",
|
||||
@@ -1104,6 +1132,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D 檢視器",
|
||||
"API Nodes": "API 節點",
|
||||
"About": "關於",
|
||||
"Appearance": "外觀",
|
||||
@@ -1155,10 +1184,31 @@
|
||||
"Window": "視窗",
|
||||
"Workflow": "工作流程"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "基本",
|
||||
"keyboardShortcuts": "鍵盤快捷鍵",
|
||||
"manageShortcuts": "管理快捷鍵",
|
||||
"noKeybinding": "無快捷鍵",
|
||||
"subcategories": {
|
||||
"node": "節點",
|
||||
"panelControls": "面板控制",
|
||||
"queue": "佇列",
|
||||
"view": "檢視",
|
||||
"workflow": "工作流程"
|
||||
},
|
||||
"viewControls": "檢視控制"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "瀏覽範例模板",
|
||||
"downloads": "下載",
|
||||
"helpCenter": "說明中心",
|
||||
"labels": {
|
||||
"models": "模型",
|
||||
"nodes": "節點",
|
||||
"queue": "佇列",
|
||||
"templates": "範本",
|
||||
"workflows": "工作流程"
|
||||
},
|
||||
"logout": "登出",
|
||||
"modelLibrary": "模型庫",
|
||||
"newBlankWorkflow": "建立新的空白工作流程",
|
||||
@@ -1196,6 +1246,7 @@
|
||||
},
|
||||
"showFlatList": "顯示平面清單"
|
||||
},
|
||||
"templates": "範本",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "您確定要刪除這個工作流程嗎?",
|
||||
"confirmDeleteTitle": "刪除工作流程?",
|
||||
@@ -1242,6 +1293,8 @@
|
||||
"Video": "影片",
|
||||
"Video API": "影片 API"
|
||||
},
|
||||
"loadingMore": "正在載入更多範本...",
|
||||
"searchPlaceholder": "搜尋範本...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
|
||||
@@ -1564,6 +1617,7 @@
|
||||
"failedToExportModel": "無法將模型匯出為 {format}",
|
||||
"failedToFetchBalance": "取得餘額失敗:{error}",
|
||||
"failedToFetchLogs": "無法取得伺服器日誌",
|
||||
"failedToInitializeLoad3dViewer": "初始化 3D 檢視器失敗",
|
||||
"failedToInitiateCreditPurchase": "啟動點數購買失敗:{error}",
|
||||
"failedToPurchaseCredits": "購買點數失敗:{error}",
|
||||
"fileLoadError": "無法在 {fileName} 中找到工作流程",
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "联系支持"
|
||||
},
|
||||
"Comfy_Dev_ShowModelSelector": {
|
||||
"label": "顯示模型選擇器(開發)"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "复制当前工作流"
|
||||
},
|
||||
@@ -164,20 +167,8 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "加载默认工作流"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "自定义节点(测试版)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "自訂節點(舊版)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "管理員選單(舊版)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "安装缺失的包"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "检查更新"
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "自定义节点管理器"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切换进度对话框"
|
||||
@@ -191,12 +182,6 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "打开选中节点的遮罩编辑器"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "卸载模型"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "卸载模型和执行缓存"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新建空白工作流"
|
||||
},
|
||||
|
||||
@@ -84,9 +84,9 @@
|
||||
},
|
||||
"breadcrumbsMenu": {
|
||||
"clearWorkflow": "清除工作流程",
|
||||
"deleteWorkflow": "刪除工作流程",
|
||||
"duplicate": "複製",
|
||||
"enterNewName": "輸入新名稱"
|
||||
"deleteWorkflow": "删除工作流程",
|
||||
"duplicate": "复制",
|
||||
"enterNewName": "输入新名称"
|
||||
},
|
||||
"chatHistory": {
|
||||
"cancelEdit": "取消",
|
||||
@@ -272,11 +272,11 @@
|
||||
"category": "类别",
|
||||
"choose_file_to_upload": "选择要上传的文件",
|
||||
"clear": "清除",
|
||||
"clearFilters": "清除筛选",
|
||||
"close": "关闭",
|
||||
"color": "颜色",
|
||||
"comingSoon": "即将推出",
|
||||
"command": "指令",
|
||||
"commandProhibited": "命令 {command} 被禁止。请联系管理员获取更多信息。",
|
||||
"community": "社区",
|
||||
"completed": "已完成",
|
||||
"confirm": "确认",
|
||||
@@ -297,8 +297,9 @@
|
||||
"devices": "设备",
|
||||
"disableAll": "禁用全部",
|
||||
"disabling": "禁用中",
|
||||
"dismiss": "關閉",
|
||||
"dismiss": "关闭",
|
||||
"download": "下载",
|
||||
"duplicate": "复制",
|
||||
"edit": "编辑",
|
||||
"empty": "空",
|
||||
"enableAll": "启用全部",
|
||||
@@ -313,7 +314,7 @@
|
||||
"findIssues": "查找问题",
|
||||
"firstTimeUIMessage": "这是您第一次使用新界面。选择 \"菜单 > 使用新菜单 > 禁用\" 来恢复旧界面。",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 或更高版本。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已过时。后端需要 {requiredVersion} 或更高版本。",
|
||||
"goToNode": "转到节点",
|
||||
"help": "帮助",
|
||||
"icon": "图标",
|
||||
@@ -400,7 +401,7 @@
|
||||
"usageHint": "使用提示",
|
||||
"user": "用户",
|
||||
"versionMismatchWarning": "版本相容性警告",
|
||||
"versionMismatchWarningMessage": "{warning}:{detail} 請參閱 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新說明。",
|
||||
"versionMismatchWarningMessage": "{warning}:{detail} 请参阅 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新说明。",
|
||||
"videoFailedToLoad": "视频加载失败",
|
||||
"workflow": "工作流"
|
||||
},
|
||||
@@ -410,7 +411,7 @@
|
||||
"resetView": "重置视图",
|
||||
"selectMode": "选择模式",
|
||||
"toggleLinkVisibility": "切换连线可见性",
|
||||
"toggleMinimap": "切換小地圖",
|
||||
"toggleMinimap": "切换小地图",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小"
|
||||
},
|
||||
@@ -569,6 +570,10 @@
|
||||
"applyingTexture": "应用纹理中...",
|
||||
"backgroundColor": "背景颜色",
|
||||
"camera": "摄影机",
|
||||
"cameraType": {
|
||||
"orthographic": "正交",
|
||||
"perspective": "透视"
|
||||
},
|
||||
"clearRecording": "清除录制",
|
||||
"edgeThreshold": "边缘阈值",
|
||||
"export": "导出",
|
||||
@@ -589,6 +594,7 @@
|
||||
"wireframe": "线框"
|
||||
},
|
||||
"model": "模型",
|
||||
"openIn3DViewer": "在 3D 查看器中打开",
|
||||
"previewOutput": "预览输出",
|
||||
"removeBackgroundImage": "移除背景图片",
|
||||
"resizeNodeMatchOutput": "调整节点以匹配输出",
|
||||
@@ -599,8 +605,22 @@
|
||||
"switchCamera": "切换摄影机类型",
|
||||
"switchingMaterialMode": "切换材质模式中...",
|
||||
"upDirection": "上方向",
|
||||
"upDirections": {
|
||||
"original": "原始"
|
||||
},
|
||||
"uploadBackgroundImage": "上传背景图片",
|
||||
"uploadTexture": "上传纹理"
|
||||
"uploadTexture": "上传纹理",
|
||||
"viewer": {
|
||||
"apply": "应用",
|
||||
"cameraSettings": "相机设置",
|
||||
"cameraType": "相机类型",
|
||||
"cancel": "取消",
|
||||
"exportSettings": "导出设置",
|
||||
"lightSettings": "灯光设置",
|
||||
"modelSettings": "模型设置",
|
||||
"sceneSettings": "场景设置",
|
||||
"title": "3D 查看器(测试版)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "需要 ComfyUI {version}:",
|
||||
@@ -647,9 +667,6 @@
|
||||
"installationQueue": "安装队列",
|
||||
"lastUpdated": "最后更新",
|
||||
"latestVersion": "最新",
|
||||
"legacyManagerUI": "使用旧版UI",
|
||||
"legacyManagerUIDescription": "要使用旧版的管理器UI,请启动ComfyUI并使用 --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "在此版本的ComfyUI中,不提供旧版的管理器菜单。请使用新的管理器菜单。",
|
||||
"license": "许可证",
|
||||
"loadingVersions": "正在加载版本...",
|
||||
"nightlyVersion": "每夜",
|
||||
@@ -723,7 +740,7 @@
|
||||
"disabled": "禁用",
|
||||
"disabledTooltip": "工作流将不会自动执行",
|
||||
"execute": "执行",
|
||||
"help": "說明",
|
||||
"help": "说明",
|
||||
"hideMenu": "隐藏菜单",
|
||||
"instant": "实时",
|
||||
"instantTooltip": "工作流将会在生成完成后立即执行",
|
||||
@@ -732,25 +749,25 @@
|
||||
"manageExtensions": "管理擴充功能",
|
||||
"onChange": "更改时",
|
||||
"onChangeTooltip": "一旦进行更改,工作流将添加到执行队列",
|
||||
"queue": "队列面板",
|
||||
"refresh": "刷新节点",
|
||||
"resetView": "重置视图",
|
||||
"run": "运行",
|
||||
"runWorkflow": "运行工作流程(Shift排在前面)",
|
||||
"runWorkflowFront": "运行工作流程(排在前面)",
|
||||
"settings": "設定",
|
||||
"settings": "设定",
|
||||
"showMenu": "显示菜单",
|
||||
"theme": "主題",
|
||||
"theme": "主题",
|
||||
"toggleBottomPanel": "底部面板"
|
||||
},
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "关于ComfyUI",
|
||||
"Add Edit Model Step": "添加编辑模型步骤",
|
||||
"Bottom Panel": "底部面板",
|
||||
"Browse Templates": "浏览模板",
|
||||
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",
|
||||
"Canvas Toggle Link Visibility": "切换连线可见性",
|
||||
"Canvas Performance": "画布性能",
|
||||
"Canvas Toggle Lock": "切换视图锁定",
|
||||
"Canvas Toggle Minimap": "畫布切換小地圖",
|
||||
"Check for Custom Node Updates": "檢查自訂節點更新",
|
||||
"Check for Updates": "检查更新",
|
||||
"Clear Pending Tasks": "清除待处理任务",
|
||||
"Clear Workflow": "清除工作流",
|
||||
@@ -764,27 +781,28 @@
|
||||
"Contact Support": "联系支持",
|
||||
"Convert Selection to Subgraph": "将选中内容转换为子图",
|
||||
"Convert selected nodes to group node": "将选中节点转换为组节点",
|
||||
"Custom Nodes (Legacy)": "自訂節點(舊版)",
|
||||
"Custom Nodes Manager": "自定义节点管理器",
|
||||
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中減小筆刷大小",
|
||||
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中减小笔刷大小",
|
||||
"Delete Selected Items": "删除选定的项目",
|
||||
"Desktop User Guide": "桌面端用户指南",
|
||||
"Duplicate Current Workflow": "复制当前工作流",
|
||||
"Edit": "编辑",
|
||||
"Exit Subgraph": "退出子图",
|
||||
"Export": "导出",
|
||||
"Export (API)": "导出 (API)",
|
||||
"File": "文件",
|
||||
"Fit Group To Contents": "适应组内容",
|
||||
"Fit view to selected nodes": "适应视图到选中节点",
|
||||
"Focus Mode": "专注模式",
|
||||
"Give Feedback": "提供反馈",
|
||||
"Group Selected Nodes": "将选中节点转换为组节点",
|
||||
"Help": "帮助",
|
||||
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
|
||||
"Install Missing Custom Nodes": "安裝缺少的自訂節點",
|
||||
"Help Center": "帮助中心",
|
||||
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大笔刷大小",
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "加载默认工作流",
|
||||
"Manage group nodes": "管理组节点",
|
||||
"Manager": "管理器",
|
||||
"Manager Menu (Legacy)": "管理選單(舊版)",
|
||||
"Minimap": "小地图",
|
||||
"Model Library": "模型库",
|
||||
"Move Selected Nodes Down": "下移所选节点",
|
||||
"Move Selected Nodes Left": "左移所选节点",
|
||||
"Move Selected Nodes Right": "右移所选节点",
|
||||
@@ -792,7 +810,10 @@
|
||||
"Mute/Unmute Selected Nodes": "静音/取消静音选定节点",
|
||||
"New": "新建",
|
||||
"Next Opened Workflow": "下一个打开的工作流",
|
||||
"Node Library": "节点库",
|
||||
"Node Links": "节点连接",
|
||||
"Open": "打开",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "为选中节点打开3D查看器(测试版)",
|
||||
"Open Custom Nodes Folder": "打开自定义节点文件夹",
|
||||
"Open DevTools": "打开开发者工具",
|
||||
"Open Inputs Folder": "打开输入文件夹",
|
||||
@@ -805,6 +826,7 @@
|
||||
"Pin/Unpin Selected Items": "固定/取消固定选定项目",
|
||||
"Pin/Unpin Selected Nodes": "固定/取消固定选定节点",
|
||||
"Previous Opened Workflow": "上一个打开的工作流",
|
||||
"Queue Panel": "队列面板",
|
||||
"Queue Prompt": "执行提示词",
|
||||
"Queue Prompt (Front)": "执行提示词 (优先执行)",
|
||||
"Queue Selected Output Nodes": "将所选输出节点加入队列",
|
||||
@@ -817,26 +839,32 @@
|
||||
"Restart": "重启",
|
||||
"Save": "保存",
|
||||
"Save As": "另存为",
|
||||
"Show Keybindings Dialog": "顯示快捷鍵對話框",
|
||||
"Show Model Selector (Dev)": "顯示模型選擇器(開發用)",
|
||||
"Show Settings Dialog": "显示设置对话框",
|
||||
"Sign Out": "退出登录",
|
||||
"Toggle Bottom Panel": "切换底部面板",
|
||||
"Toggle Focus Mode": "切换专注模式",
|
||||
"Toggle Essential Bottom Panel": "切换基础底部面板",
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
|
||||
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
|
||||
"Toggle Queue Sidebar": "切換佇列側邊欄",
|
||||
"Toggle Search Box": "切换搜索框",
|
||||
"Toggle Terminal Bottom Panel": "切换终端底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
|
||||
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
|
||||
"Toggle View Controls Bottom Panel": "切换视图控制底部面板",
|
||||
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
|
||||
"Undo": "撤销",
|
||||
"Ungroup selected group nodes": "解散选中组节点",
|
||||
"Unload Models": "卸载模型",
|
||||
"Unload Models and Execution Cache": "卸载模型和执行缓存",
|
||||
"Workflow": "工作流",
|
||||
"Unpack the selected Subgraph": "解包选中子图",
|
||||
"Workflows": "工作流",
|
||||
"Zoom In": "放大画面",
|
||||
"Zoom Out": "缩小画面"
|
||||
"Zoom Out": "缩小画面",
|
||||
"Zoom to fit": "缩放以适应"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "节点颜色",
|
||||
"renderBypassState": "渲染绕过状态",
|
||||
"renderErrorState": "渲染错误状态",
|
||||
"showGroups": "显示框架/分组",
|
||||
"showLinks": "显示连接"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不再显示此消息",
|
||||
@@ -1104,6 +1132,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D查看器",
|
||||
"API Nodes": "API 节点",
|
||||
"About": "关于",
|
||||
"Appearance": "外观",
|
||||
@@ -1155,10 +1184,31 @@
|
||||
"Window": "窗口",
|
||||
"Workflow": "工作流"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "常用",
|
||||
"keyboardShortcuts": "键盘快捷键",
|
||||
"manageShortcuts": "管理快捷键",
|
||||
"noKeybinding": "无快捷键",
|
||||
"subcategories": {
|
||||
"node": "节点",
|
||||
"panelControls": "面板控制",
|
||||
"queue": "队列",
|
||||
"view": "视图",
|
||||
"workflow": "工作流"
|
||||
},
|
||||
"viewControls": "视图控制"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "浏览示例模板",
|
||||
"downloads": "下载",
|
||||
"helpCenter": "帮助中心",
|
||||
"labels": {
|
||||
"models": "模型",
|
||||
"nodes": "节点",
|
||||
"queue": "队列",
|
||||
"templates": "模板",
|
||||
"workflows": "工作流"
|
||||
},
|
||||
"logout": "登出",
|
||||
"modelLibrary": "模型库",
|
||||
"newBlankWorkflow": "创建空白工作流",
|
||||
@@ -1196,6 +1246,7 @@
|
||||
},
|
||||
"showFlatList": "平铺结果"
|
||||
},
|
||||
"templates": "模板",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "您确定要删除此工作流吗?",
|
||||
"confirmDeleteTitle": "删除工作流?",
|
||||
@@ -1242,6 +1293,8 @@
|
||||
"Video": "视频生成",
|
||||
"Video API": "视频 API"
|
||||
},
|
||||
"loadingMore": "正在加载更多模板...",
|
||||
"searchPlaceholder": "搜索模板...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "混元3D 2.0 图生模型",
|
||||
@@ -1564,6 +1617,7 @@
|
||||
"failedToExportModel": "无法将模型导出为 {format}",
|
||||
"failedToFetchBalance": "获取余额失败:{error}",
|
||||
"failedToFetchLogs": "无法获取服务器日志",
|
||||
"failedToInitializeLoad3dViewer": "初始化3D查看器失败",
|
||||
"failedToInitiateCreditPurchase": "发起积分购买失败:{error}",
|
||||
"failedToPurchaseCredits": "购买积分失败:{error}",
|
||||
"fileLoadError": "无法在 {fileName} 中找到工作流",
|
||||
@@ -1620,9 +1674,9 @@
|
||||
"required": "必填"
|
||||
},
|
||||
"versionMismatchWarning": {
|
||||
"dismiss": "關閉",
|
||||
"dismiss": "关闭",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 版或更高版本。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已过时。後端需要 {requiredVersion} 版或更高版本。",
|
||||
"title": "版本相容性警告",
|
||||
"updateFrontend": "更新前端"
|
||||
},
|
||||
|
||||
100
src/renderer/core/spatial/boundsCalculator.ts
Normal file
100
src/renderer/core/spatial/boundsCalculator.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Spatial bounds calculations for node layouts
|
||||
*/
|
||||
|
||||
export interface SpatialBounds {
|
||||
minX: number
|
||||
minY: number
|
||||
maxX: number
|
||||
maxY: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface PositionedNode {
|
||||
pos: ArrayLike<number>
|
||||
size: ArrayLike<number>
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the spatial bounding box of positioned nodes
|
||||
*/
|
||||
export function calculateNodeBounds(
|
||||
nodes: PositionedNode[]
|
||||
): SpatialBounds | null {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
let maxX = -Infinity
|
||||
let maxY = -Infinity
|
||||
|
||||
for (const node of nodes) {
|
||||
const x = node.pos[0]
|
||||
const y = node.pos[1]
|
||||
const width = node.size[0]
|
||||
const height = node.size[1]
|
||||
|
||||
minX = Math.min(minX, x)
|
||||
minY = Math.min(minY, y)
|
||||
maxX = Math.max(maxX, x + width)
|
||||
maxY = Math.max(maxY, y + height)
|
||||
}
|
||||
|
||||
return {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce minimum viewport dimensions for better visualization
|
||||
*/
|
||||
export function enforceMinimumBounds(
|
||||
bounds: SpatialBounds,
|
||||
minWidth: number = 2500,
|
||||
minHeight: number = 2000
|
||||
): SpatialBounds {
|
||||
let { minX, minY, maxX, maxY, width, height } = bounds
|
||||
|
||||
if (width < minWidth) {
|
||||
const padding = (minWidth - width) / 2
|
||||
minX -= padding
|
||||
maxX += padding
|
||||
width = minWidth
|
||||
}
|
||||
|
||||
if (height < minHeight) {
|
||||
const padding = (minHeight - height) / 2
|
||||
minY -= padding
|
||||
maxY += padding
|
||||
height = minHeight
|
||||
}
|
||||
|
||||
return { minX, minY, maxX, maxY, width, height }
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the scale factor to fit bounds within a viewport
|
||||
*/
|
||||
export function calculateMinimapScale(
|
||||
bounds: SpatialBounds,
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
padding: number = 0.9
|
||||
): number {
|
||||
if (bounds.width === 0 || bounds.height === 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const scaleX = viewportWidth / bounds.width
|
||||
const scaleY = viewportHeight / bounds.height
|
||||
|
||||
return Math.min(scaleX, scaleY) * padding
|
||||
}
|
||||
251
src/renderer/extensions/minimap/composables/useMinimap.ts
Normal file
251
src/renderer/extensions/minimap/composables/useMinimap.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { useRafFn } from '@vueuse/core'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
import type { MinimapCanvas, MinimapSettingsKey } from '../types'
|
||||
import { useMinimapGraph } from './useMinimapGraph'
|
||||
import { useMinimapInteraction } from './useMinimapInteraction'
|
||||
import { useMinimapRenderer } from './useMinimapRenderer'
|
||||
import { useMinimapSettings } from './useMinimapSettings'
|
||||
import { useMinimapViewport } from './useMinimapViewport'
|
||||
|
||||
export function useMinimap() {
|
||||
const canvasStore = useCanvasStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
const minimapRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const visible = ref(true)
|
||||
const initialized = ref(false)
|
||||
|
||||
const width = 250
|
||||
const height = 200
|
||||
|
||||
const canvas = computed(() => canvasStore.canvas as MinimapCanvas | null)
|
||||
const graph = computed(() => {
|
||||
// If we're in a subgraph, use that; otherwise use the canvas graph
|
||||
const activeSubgraph = workflowStore.activeSubgraph
|
||||
return (activeSubgraph || canvas.value?.graph) as LGraph | null
|
||||
})
|
||||
|
||||
// Settings
|
||||
const settings = useMinimapSettings()
|
||||
const {
|
||||
nodeColors,
|
||||
showLinks,
|
||||
showGroups,
|
||||
renderBypass,
|
||||
renderError,
|
||||
containerStyles,
|
||||
panelStyles
|
||||
} = settings
|
||||
|
||||
const updateOption = async (key: MinimapSettingsKey, value: boolean) => {
|
||||
await settingStore.set(key, value)
|
||||
renderer.forceFullRedraw()
|
||||
renderer.updateMinimap(viewport.updateBounds, viewport.updateViewport)
|
||||
}
|
||||
|
||||
// Viewport management
|
||||
const viewport = useMinimapViewport(canvas, graph, width, height)
|
||||
|
||||
// Interaction handling
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
viewport.bounds,
|
||||
viewport.scale,
|
||||
width,
|
||||
height,
|
||||
viewport.centerViewOn,
|
||||
canvas
|
||||
)
|
||||
|
||||
// Graph event management
|
||||
const graphManager = useMinimapGraph(graph, () => {
|
||||
renderer.forceFullRedraw()
|
||||
renderer.updateMinimap(viewport.updateBounds, viewport.updateViewport)
|
||||
})
|
||||
|
||||
// Rendering
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graph,
|
||||
viewport.bounds,
|
||||
viewport.scale,
|
||||
graphManager.updateFlags,
|
||||
settings,
|
||||
width,
|
||||
height
|
||||
)
|
||||
|
||||
// RAF loop for continuous updates
|
||||
const { pause: pauseChangeDetection, resume: resumeChangeDetection } =
|
||||
useRafFn(
|
||||
async () => {
|
||||
if (visible.value) {
|
||||
const hasChanges = await graphManager.checkForChanges()
|
||||
if (hasChanges) {
|
||||
renderer.updateMinimap(
|
||||
viewport.updateBounds,
|
||||
viewport.updateViewport
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
const init = async () => {
|
||||
if (initialized.value) return
|
||||
|
||||
visible.value = settingStore.get('Comfy.Minimap.Visible')
|
||||
|
||||
if (canvas.value && graph.value) {
|
||||
graphManager.init()
|
||||
|
||||
if (containerRef.value) {
|
||||
interaction.updateContainerRect()
|
||||
}
|
||||
viewport.updateCanvasDimensions()
|
||||
|
||||
window.addEventListener('resize', interaction.updateContainerRect)
|
||||
window.addEventListener('scroll', interaction.updateContainerRect)
|
||||
window.addEventListener('resize', viewport.updateCanvasDimensions)
|
||||
|
||||
renderer.forceFullRedraw()
|
||||
renderer.updateMinimap(viewport.updateBounds, viewport.updateViewport)
|
||||
viewport.updateViewport()
|
||||
|
||||
if (visible.value) {
|
||||
resumeChangeDetection()
|
||||
viewport.startViewportSync()
|
||||
}
|
||||
initialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const destroy = () => {
|
||||
pauseChangeDetection()
|
||||
viewport.stopViewportSync()
|
||||
graphManager.destroy()
|
||||
|
||||
window.removeEventListener('resize', interaction.updateContainerRect)
|
||||
window.removeEventListener('scroll', interaction.updateContainerRect)
|
||||
window.removeEventListener('resize', viewport.updateCanvasDimensions)
|
||||
|
||||
initialized.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
canvas,
|
||||
async (newCanvas, oldCanvas) => {
|
||||
if (oldCanvas) {
|
||||
graphManager.cleanupEventListeners()
|
||||
pauseChangeDetection()
|
||||
viewport.stopViewportSync()
|
||||
graphManager.destroy()
|
||||
window.removeEventListener('resize', interaction.updateContainerRect)
|
||||
window.removeEventListener('scroll', interaction.updateContainerRect)
|
||||
window.removeEventListener('resize', viewport.updateCanvasDimensions)
|
||||
}
|
||||
if (newCanvas && !initialized.value) {
|
||||
await init()
|
||||
}
|
||||
},
|
||||
{ immediate: true, flush: 'post' }
|
||||
)
|
||||
|
||||
// Watch for graph changes (e.g., when navigating to/from subgraphs)
|
||||
watch(graph, (newGraph, oldGraph) => {
|
||||
if (newGraph && newGraph !== oldGraph) {
|
||||
graphManager.cleanupEventListeners(oldGraph || undefined)
|
||||
graphManager.setupEventListeners()
|
||||
renderer.forceFullRedraw()
|
||||
renderer.updateMinimap(viewport.updateBounds, viewport.updateViewport)
|
||||
}
|
||||
})
|
||||
|
||||
watch(visible, async (isVisible) => {
|
||||
if (isVisible) {
|
||||
if (containerRef.value) {
|
||||
interaction.updateContainerRect()
|
||||
}
|
||||
viewport.updateCanvasDimensions()
|
||||
|
||||
renderer.forceFullRedraw()
|
||||
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
renderer.updateMinimap(viewport.updateBounds, viewport.updateViewport)
|
||||
viewport.updateViewport()
|
||||
resumeChangeDetection()
|
||||
viewport.startViewportSync()
|
||||
} else {
|
||||
pauseChangeDetection()
|
||||
viewport.stopViewportSync()
|
||||
}
|
||||
})
|
||||
|
||||
const toggle = async () => {
|
||||
visible.value = !visible.value
|
||||
await settingStore.set('Comfy.Minimap.Visible', visible.value)
|
||||
}
|
||||
|
||||
const setMinimapRef = (ref: HTMLElement | null) => {
|
||||
minimapRef.value = ref
|
||||
}
|
||||
|
||||
// Dynamic viewport styles based on actual viewport transform
|
||||
const viewportStyles = computed(() => {
|
||||
const transform = viewport.viewportTransform.value
|
||||
return {
|
||||
transform: `translate(${transform.x}px, ${transform.y}px)`,
|
||||
width: `${transform.width}px`,
|
||||
height: `${transform.height}px`,
|
||||
border: `2px solid ${settings.isLightTheme.value ? '#E0E0E0' : '#FFF'}`,
|
||||
backgroundColor: `rgba(255, 255, 255, 0.2)`,
|
||||
willChange: 'transform',
|
||||
backfaceVisibility: 'hidden' as const,
|
||||
perspective: '1000px',
|
||||
pointerEvents: 'none' as const
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
visible: computed(() => visible.value),
|
||||
initialized: computed(() => initialized.value),
|
||||
|
||||
containerRef,
|
||||
canvasRef,
|
||||
containerStyles,
|
||||
viewportStyles,
|
||||
panelStyles,
|
||||
width,
|
||||
height,
|
||||
|
||||
nodeColors,
|
||||
showLinks,
|
||||
showGroups,
|
||||
renderBypass,
|
||||
renderError,
|
||||
|
||||
init,
|
||||
destroy,
|
||||
toggle,
|
||||
renderMinimap: renderer.renderMinimap,
|
||||
handlePointerDown: interaction.handlePointerDown,
|
||||
handlePointerMove: interaction.handlePointerMove,
|
||||
handlePointerUp: interaction.handlePointerUp,
|
||||
handleWheel: interaction.handleWheel,
|
||||
setMinimapRef,
|
||||
updateOption
|
||||
}
|
||||
}
|
||||
166
src/renderer/extensions/minimap/composables/useMinimapGraph.ts
Normal file
166
src/renderer/extensions/minimap/composables/useMinimapGraph.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
import type { UpdateFlags } from '../types'
|
||||
|
||||
interface GraphCallbacks {
|
||||
onNodeAdded?: (node: LGraphNode) => void
|
||||
onNodeRemoved?: (node: LGraphNode) => void
|
||||
onConnectionChange?: (node: LGraphNode) => void
|
||||
}
|
||||
|
||||
export function useMinimapGraph(
|
||||
graph: Ref<LGraph | null>,
|
||||
onGraphChanged: () => void
|
||||
) {
|
||||
const nodeStatesCache = new Map<NodeId, string>()
|
||||
const linksCache = ref<string>('')
|
||||
const lastNodeCount = ref(0)
|
||||
const updateFlags = ref<UpdateFlags>({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
|
||||
// Map to store original callbacks per graph ID
|
||||
const originalCallbacksMap = new Map<string, GraphCallbacks>()
|
||||
|
||||
const handleGraphChangedThrottled = useThrottleFn(() => {
|
||||
onGraphChanged()
|
||||
}, 500)
|
||||
|
||||
const setupEventListeners = () => {
|
||||
const g = graph.value
|
||||
if (!g) return
|
||||
|
||||
// Check if we've already wrapped this graph's callbacks
|
||||
if (originalCallbacksMap.has(g.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Store the original callbacks for this graph
|
||||
const originalCallbacks: GraphCallbacks = {
|
||||
onNodeAdded: g.onNodeAdded,
|
||||
onNodeRemoved: g.onNodeRemoved,
|
||||
onConnectionChange: g.onConnectionChange
|
||||
}
|
||||
originalCallbacksMap.set(g.id, originalCallbacks)
|
||||
|
||||
g.onNodeAdded = function (node: LGraphNode) {
|
||||
originalCallbacks.onNodeAdded?.call(this, node)
|
||||
void handleGraphChangedThrottled()
|
||||
}
|
||||
|
||||
g.onNodeRemoved = function (node: LGraphNode) {
|
||||
originalCallbacks.onNodeRemoved?.call(this, node)
|
||||
nodeStatesCache.delete(node.id)
|
||||
void handleGraphChangedThrottled()
|
||||
}
|
||||
|
||||
g.onConnectionChange = function (node: LGraphNode) {
|
||||
originalCallbacks.onConnectionChange?.call(this, node)
|
||||
void handleGraphChangedThrottled()
|
||||
}
|
||||
}
|
||||
|
||||
const cleanupEventListeners = (oldGraph?: LGraph) => {
|
||||
const g = oldGraph || graph.value
|
||||
if (!g) return
|
||||
|
||||
const originalCallbacks = originalCallbacksMap.get(g.id)
|
||||
if (!originalCallbacks) {
|
||||
console.error(
|
||||
'Attempted to cleanup event listeners for graph that was never set up'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
g.onNodeAdded = originalCallbacks.onNodeAdded
|
||||
g.onNodeRemoved = originalCallbacks.onNodeRemoved
|
||||
g.onConnectionChange = originalCallbacks.onConnectionChange
|
||||
|
||||
originalCallbacksMap.delete(g.id)
|
||||
}
|
||||
|
||||
const checkForChangesInternal = () => {
|
||||
const g = graph.value
|
||||
if (!g) return false
|
||||
|
||||
let structureChanged = false
|
||||
let positionChanged = false
|
||||
let connectionChanged = false
|
||||
|
||||
if (g._nodes.length !== lastNodeCount.value) {
|
||||
structureChanged = true
|
||||
lastNodeCount.value = g._nodes.length
|
||||
}
|
||||
|
||||
for (const node of g._nodes) {
|
||||
const key = node.id
|
||||
const currentState = `${node.pos[0]},${node.pos[1]},${node.size[0]},${node.size[1]}`
|
||||
|
||||
if (nodeStatesCache.get(key) !== currentState) {
|
||||
positionChanged = true
|
||||
nodeStatesCache.set(key, currentState)
|
||||
}
|
||||
}
|
||||
|
||||
const currentLinks = JSON.stringify(g.links || {})
|
||||
if (currentLinks !== linksCache.value) {
|
||||
connectionChanged = true
|
||||
linksCache.value = currentLinks
|
||||
}
|
||||
|
||||
const currentNodeIds = new Set(g._nodes.map((n: LGraphNode) => n.id))
|
||||
for (const [nodeId] of nodeStatesCache) {
|
||||
if (!currentNodeIds.has(nodeId)) {
|
||||
nodeStatesCache.delete(nodeId)
|
||||
structureChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
if (structureChanged || positionChanged) {
|
||||
updateFlags.value.bounds = true
|
||||
updateFlags.value.nodes = true
|
||||
}
|
||||
|
||||
if (connectionChanged) {
|
||||
updateFlags.value.connections = true
|
||||
}
|
||||
|
||||
return structureChanged || positionChanged || connectionChanged
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
setupEventListeners()
|
||||
api.addEventListener('graphChanged', handleGraphChangedThrottled)
|
||||
}
|
||||
|
||||
const destroy = () => {
|
||||
cleanupEventListeners()
|
||||
api.removeEventListener('graphChanged', handleGraphChangedThrottled)
|
||||
nodeStatesCache.clear()
|
||||
}
|
||||
|
||||
const clearCache = () => {
|
||||
nodeStatesCache.clear()
|
||||
linksCache.value = ''
|
||||
lastNodeCount.value = 0
|
||||
}
|
||||
|
||||
return {
|
||||
updateFlags,
|
||||
setupEventListeners,
|
||||
cleanupEventListeners,
|
||||
checkForChanges: checkForChangesInternal,
|
||||
init,
|
||||
destroy,
|
||||
clearCache
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { MinimapCanvas } from '../types'
|
||||
|
||||
export function useMinimapInteraction(
|
||||
containerRef: Ref<HTMLDivElement | undefined>,
|
||||
bounds: Ref<{ minX: number; minY: number; width: number; height: number }>,
|
||||
scale: Ref<number>,
|
||||
width: number,
|
||||
height: number,
|
||||
centerViewOn: (worldX: number, worldY: number) => void,
|
||||
canvas: Ref<MinimapCanvas | null>
|
||||
) {
|
||||
const isDragging = ref(false)
|
||||
const containerRect = ref({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: width,
|
||||
height: height
|
||||
})
|
||||
|
||||
const updateContainerRect = () => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
const rect = containerRef.value.getBoundingClientRect()
|
||||
containerRect.value = {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointerDown = (e: PointerEvent) => {
|
||||
isDragging.value = true
|
||||
updateContainerRect()
|
||||
handlePointerMove(e)
|
||||
}
|
||||
|
||||
const handlePointerMove = (e: PointerEvent) => {
|
||||
if (!isDragging.value || !canvas.value) return
|
||||
|
||||
const x = e.clientX - containerRect.value.left
|
||||
const y = e.clientY - containerRect.value.top
|
||||
|
||||
const offsetX = (width - bounds.value.width * scale.value) / 2
|
||||
const offsetY = (height - bounds.value.height * scale.value) / 2
|
||||
|
||||
const worldX = (x - offsetX) / scale.value + bounds.value.minX
|
||||
const worldY = (y - offsetY) / scale.value + bounds.value.minY
|
||||
|
||||
centerViewOn(worldX, worldY)
|
||||
}
|
||||
|
||||
const handlePointerUp = () => {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const c = canvas.value
|
||||
if (!c) return
|
||||
|
||||
if (
|
||||
containerRect.value.left === 0 &&
|
||||
containerRect.value.top === 0 &&
|
||||
containerRef.value
|
||||
) {
|
||||
updateContainerRect()
|
||||
}
|
||||
|
||||
const ds = c.ds
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1
|
||||
|
||||
const newScale = ds.scale * delta
|
||||
|
||||
const MIN_SCALE = 0.1
|
||||
const MAX_SCALE = 10
|
||||
|
||||
if (newScale < MIN_SCALE || newScale > MAX_SCALE) return
|
||||
|
||||
const x = e.clientX - containerRect.value.left
|
||||
const y = e.clientY - containerRect.value.top
|
||||
|
||||
const offsetX = (width - bounds.value.width * scale.value) / 2
|
||||
const offsetY = (height - bounds.value.height * scale.value) / 2
|
||||
|
||||
const worldX = (x - offsetX) / scale.value + bounds.value.minX
|
||||
const worldY = (y - offsetY) / scale.value + bounds.value.minY
|
||||
|
||||
ds.scale = newScale
|
||||
|
||||
centerViewOn(worldX, worldY)
|
||||
}
|
||||
|
||||
return {
|
||||
isDragging,
|
||||
containerRect,
|
||||
updateContainerRect,
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
handleWheel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { renderMinimapToCanvas } from '../minimapCanvasRenderer'
|
||||
import type { UpdateFlags } from '../types'
|
||||
|
||||
export function useMinimapRenderer(
|
||||
canvasRef: Ref<HTMLCanvasElement | undefined>,
|
||||
graph: Ref<LGraph | null>,
|
||||
bounds: Ref<{ minX: number; minY: number; width: number; height: number }>,
|
||||
scale: Ref<number>,
|
||||
updateFlags: Ref<UpdateFlags>,
|
||||
settings: {
|
||||
nodeColors: Ref<boolean>
|
||||
showLinks: Ref<boolean>
|
||||
showGroups: Ref<boolean>
|
||||
renderBypass: Ref<boolean>
|
||||
renderError: Ref<boolean>
|
||||
},
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
const needsFullRedraw = ref(true)
|
||||
const needsBoundsUpdate = ref(true)
|
||||
|
||||
const renderMinimap = () => {
|
||||
const g = graph.value
|
||||
if (!canvasRef.value || !g) return
|
||||
|
||||
const ctx = canvasRef.value.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
// Fast path for 0 nodes - just show background
|
||||
if (!g._nodes || g._nodes.length === 0) {
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
return
|
||||
}
|
||||
|
||||
const needsRedraw =
|
||||
needsFullRedraw.value ||
|
||||
updateFlags.value.nodes ||
|
||||
updateFlags.value.connections
|
||||
|
||||
if (needsRedraw) {
|
||||
renderMinimapToCanvas(canvasRef.value, g, {
|
||||
bounds: bounds.value,
|
||||
scale: scale.value,
|
||||
settings: {
|
||||
nodeColors: settings.nodeColors.value,
|
||||
showLinks: settings.showLinks.value,
|
||||
showGroups: settings.showGroups.value,
|
||||
renderBypass: settings.renderBypass.value,
|
||||
renderError: settings.renderError.value
|
||||
},
|
||||
width,
|
||||
height
|
||||
})
|
||||
|
||||
needsFullRedraw.value = false
|
||||
updateFlags.value.nodes = false
|
||||
updateFlags.value.connections = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateMinimap = (
|
||||
updateBounds: () => void,
|
||||
updateViewport: () => void
|
||||
) => {
|
||||
if (needsBoundsUpdate.value || updateFlags.value.bounds) {
|
||||
updateBounds()
|
||||
needsBoundsUpdate.value = false
|
||||
updateFlags.value.bounds = false
|
||||
needsFullRedraw.value = true
|
||||
// When bounds change, we need to update the viewport position
|
||||
updateFlags.value.viewport = true
|
||||
}
|
||||
|
||||
if (
|
||||
needsFullRedraw.value ||
|
||||
updateFlags.value.nodes ||
|
||||
updateFlags.value.connections
|
||||
) {
|
||||
renderMinimap()
|
||||
}
|
||||
|
||||
// Update viewport if needed (e.g., after bounds change)
|
||||
if (updateFlags.value.viewport) {
|
||||
updateViewport()
|
||||
updateFlags.value.viewport = false
|
||||
}
|
||||
}
|
||||
|
||||
const forceFullRedraw = () => {
|
||||
needsFullRedraw.value = true
|
||||
updateFlags.value.bounds = true
|
||||
updateFlags.value.nodes = true
|
||||
updateFlags.value.connections = true
|
||||
updateFlags.value.viewport = true
|
||||
}
|
||||
|
||||
return {
|
||||
needsFullRedraw,
|
||||
needsBoundsUpdate,
|
||||
renderMinimap,
|
||||
updateMinimap,
|
||||
forceFullRedraw
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
/**
|
||||
* Composable for minimap configuration options that are set by the user in the
|
||||
* settings. Provides reactive computed properties for the settings.
|
||||
*/
|
||||
export function useMinimapSettings() {
|
||||
const settingStore = useSettingStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const nodeColors = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.NodeColors')
|
||||
)
|
||||
const showLinks = computed(() => settingStore.get('Comfy.Minimap.ShowLinks'))
|
||||
const showGroups = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.ShowGroups')
|
||||
)
|
||||
const renderBypass = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.RenderBypassState')
|
||||
)
|
||||
const renderError = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.RenderErrorState')
|
||||
)
|
||||
|
||||
const width = 250
|
||||
const height = 200
|
||||
|
||||
// Theme-aware colors
|
||||
const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
|
||||
const containerStyles = computed(() => ({
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
backgroundColor: isLightTheme.value ? '#FAF9F5' : '#15161C',
|
||||
border: `1px solid ${isLightTheme.value ? '#ccc' : '#333'}`,
|
||||
borderRadius: '8px'
|
||||
}))
|
||||
|
||||
const panelStyles = computed(() => ({
|
||||
width: `210px`,
|
||||
height: `${height}px`,
|
||||
backgroundColor: isLightTheme.value ? '#FAF9F5' : '#15161C',
|
||||
border: `1px solid ${isLightTheme.value ? '#ccc' : '#333'}`,
|
||||
borderRadius: '8px'
|
||||
}))
|
||||
|
||||
return {
|
||||
nodeColors,
|
||||
showLinks,
|
||||
showGroups,
|
||||
renderBypass,
|
||||
renderError,
|
||||
containerStyles,
|
||||
panelStyles,
|
||||
isLightTheme
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
calculateMinimapScale,
|
||||
calculateNodeBounds,
|
||||
enforceMinimumBounds
|
||||
} from '@/renderer/core/spatial/boundsCalculator'
|
||||
|
||||
import type { MinimapBounds, MinimapCanvas, ViewportTransform } from '../types'
|
||||
|
||||
export function useMinimapViewport(
|
||||
canvas: Ref<MinimapCanvas | null>,
|
||||
graph: Ref<LGraph | null>,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
const bounds = ref<MinimapBounds>({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 0,
|
||||
maxY: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
})
|
||||
|
||||
const scale = ref(1)
|
||||
const viewportTransform = ref<ViewportTransform>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
})
|
||||
|
||||
const canvasDimensions = ref({
|
||||
width: 0,
|
||||
height: 0
|
||||
})
|
||||
|
||||
const updateCanvasDimensions = () => {
|
||||
const c = canvas.value
|
||||
if (!c) return
|
||||
|
||||
const canvasEl = c.canvas
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
|
||||
canvasDimensions.value = {
|
||||
width: canvasEl.clientWidth || canvasEl.width / dpr,
|
||||
height: canvasEl.clientHeight || canvasEl.height / dpr
|
||||
}
|
||||
}
|
||||
|
||||
const calculateGraphBounds = (): MinimapBounds => {
|
||||
const g = graph.value
|
||||
if (!g || !g._nodes || g._nodes.length === 0) {
|
||||
return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }
|
||||
}
|
||||
|
||||
const bounds = calculateNodeBounds(g._nodes)
|
||||
if (!bounds) {
|
||||
return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }
|
||||
}
|
||||
|
||||
return enforceMinimumBounds(bounds)
|
||||
}
|
||||
|
||||
const calculateScale = () => {
|
||||
return calculateMinimapScale(bounds.value, width, height)
|
||||
}
|
||||
|
||||
const updateViewport = () => {
|
||||
const c = canvas.value
|
||||
if (!c) return
|
||||
|
||||
if (
|
||||
canvasDimensions.value.width === 0 ||
|
||||
canvasDimensions.value.height === 0
|
||||
) {
|
||||
updateCanvasDimensions()
|
||||
}
|
||||
|
||||
const ds = c.ds
|
||||
|
||||
const viewportWidth = canvasDimensions.value.width / ds.scale
|
||||
const viewportHeight = canvasDimensions.value.height / ds.scale
|
||||
|
||||
const worldX = -ds.offset[0]
|
||||
const worldY = -ds.offset[1]
|
||||
|
||||
const centerOffsetX = (width - bounds.value.width * scale.value) / 2
|
||||
const centerOffsetY = (height - bounds.value.height * scale.value) / 2
|
||||
|
||||
viewportTransform.value = {
|
||||
x: (worldX - bounds.value.minX) * scale.value + centerOffsetX,
|
||||
y: (worldY - bounds.value.minY) * scale.value + centerOffsetY,
|
||||
width: viewportWidth * scale.value,
|
||||
height: viewportHeight * scale.value
|
||||
}
|
||||
}
|
||||
|
||||
const updateBounds = () => {
|
||||
bounds.value = calculateGraphBounds()
|
||||
scale.value = calculateScale()
|
||||
}
|
||||
|
||||
const centerViewOn = (worldX: number, worldY: number) => {
|
||||
const c = canvas.value
|
||||
if (!c) return
|
||||
|
||||
if (
|
||||
canvasDimensions.value.width === 0 ||
|
||||
canvasDimensions.value.height === 0
|
||||
) {
|
||||
updateCanvasDimensions()
|
||||
}
|
||||
|
||||
const ds = c.ds
|
||||
|
||||
const viewportWidth = canvasDimensions.value.width / ds.scale
|
||||
const viewportHeight = canvasDimensions.value.height / ds.scale
|
||||
|
||||
ds.offset[0] = -(worldX - viewportWidth / 2)
|
||||
ds.offset[1] = -(worldY - viewportHeight / 2)
|
||||
|
||||
c.setDirty(true, true)
|
||||
}
|
||||
|
||||
const { startSync: startViewportSync, stopSync: stopViewportSync } =
|
||||
useCanvasTransformSync(updateViewport, { autoStart: false })
|
||||
|
||||
return {
|
||||
bounds: computed(() => bounds.value),
|
||||
scale: computed(() => scale.value),
|
||||
viewportTransform: computed(() => viewportTransform.value),
|
||||
canvasDimensions: computed(() => canvasDimensions.value),
|
||||
updateCanvasDimensions,
|
||||
updateViewport,
|
||||
updateBounds,
|
||||
centerViewOn,
|
||||
startViewportSync,
|
||||
stopViewportSync
|
||||
}
|
||||
}
|
||||
238
src/renderer/extensions/minimap/minimapCanvasRenderer.ts
Normal file
238
src/renderer/extensions/minimap/minimapCanvasRenderer.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { LGraph, LGraphEventMode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
|
||||
import type { MinimapRenderContext } from './types'
|
||||
|
||||
/**
|
||||
* Get theme-aware colors for the minimap
|
||||
*/
|
||||
function getMinimapColors() {
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const isLightTheme = colorPaletteStore.completedActivePalette.light_theme
|
||||
|
||||
return {
|
||||
nodeColor: isLightTheme ? '#3DA8E099' : '#0B8CE999',
|
||||
nodeColorDefault: isLightTheme ? '#D9D9D9' : '#353535',
|
||||
linkColor: isLightTheme ? '#616161' : '#B3B3B3',
|
||||
slotColor: isLightTheme ? '#616161' : '#B3B3B3',
|
||||
groupColor: isLightTheme ? '#A2D3EC' : '#1F547A',
|
||||
groupColorDefault: isLightTheme ? '#283640' : '#B3C1CB',
|
||||
bypassColor: isLightTheme ? '#DBDBDB' : '#4B184B',
|
||||
errorColor: '#FF0000',
|
||||
isLightTheme
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render groups on the minimap
|
||||
*/
|
||||
function renderGroups(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
graph: LGraph,
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
context: MinimapRenderContext,
|
||||
colors: ReturnType<typeof getMinimapColors>
|
||||
) {
|
||||
if (!graph._groups || graph._groups.length === 0) return
|
||||
|
||||
for (const group of graph._groups) {
|
||||
const x = (group.pos[0] - context.bounds.minX) * context.scale + offsetX
|
||||
const y = (group.pos[1] - context.bounds.minY) * context.scale + offsetY
|
||||
const w = group.size[0] * context.scale
|
||||
const h = group.size[1] * context.scale
|
||||
|
||||
let color = colors.groupColor
|
||||
|
||||
if (context.settings.nodeColors) {
|
||||
color = group.color ?? colors.groupColorDefault
|
||||
|
||||
if (colors.isLightTheme) {
|
||||
color = adjustColor(color, { opacity: 0.5 })
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = color
|
||||
ctx.fillRect(x, y, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render nodes on the minimap with performance optimizations
|
||||
*/
|
||||
function renderNodes(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
graph: LGraph,
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
context: MinimapRenderContext,
|
||||
colors: ReturnType<typeof getMinimapColors>
|
||||
) {
|
||||
if (!graph._nodes || graph._nodes.length === 0) return
|
||||
|
||||
// Group nodes by color for batch rendering
|
||||
const nodesByColor = new Map<
|
||||
string,
|
||||
Array<{ x: number; y: number; w: number; h: number; hasErrors?: boolean }>
|
||||
>()
|
||||
|
||||
for (const node of graph._nodes) {
|
||||
const x = (node.pos[0] - context.bounds.minX) * context.scale + offsetX
|
||||
const y = (node.pos[1] - context.bounds.minY) * context.scale + offsetY
|
||||
const w = node.size[0] * context.scale
|
||||
const h = node.size[1] * context.scale
|
||||
|
||||
let color = colors.nodeColor
|
||||
|
||||
if (context.settings.renderBypass && node.mode === LGraphEventMode.BYPASS) {
|
||||
color = colors.bypassColor
|
||||
} else if (context.settings.nodeColors) {
|
||||
color = colors.nodeColorDefault
|
||||
|
||||
if (node.bgcolor) {
|
||||
color = colors.isLightTheme
|
||||
? adjustColor(node.bgcolor, { lightness: 0.5 })
|
||||
: node.bgcolor
|
||||
}
|
||||
}
|
||||
|
||||
if (!nodesByColor.has(color)) {
|
||||
nodesByColor.set(color, [])
|
||||
}
|
||||
|
||||
nodesByColor.get(color)!.push({ x, y, w, h, hasErrors: node.has_errors })
|
||||
}
|
||||
|
||||
// Batch render nodes by color
|
||||
for (const [color, nodes] of nodesByColor) {
|
||||
ctx.fillStyle = color
|
||||
for (const node of nodes) {
|
||||
ctx.fillRect(node.x, node.y, node.w, node.h)
|
||||
}
|
||||
}
|
||||
|
||||
// Render error outlines if needed
|
||||
if (context.settings.renderError) {
|
||||
ctx.strokeStyle = colors.errorColor
|
||||
ctx.lineWidth = 0.3
|
||||
for (const nodes of nodesByColor.values()) {
|
||||
for (const node of nodes) {
|
||||
if (node.hasErrors) {
|
||||
ctx.strokeRect(node.x, node.y, node.w, node.h)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render connections on the minimap
|
||||
*/
|
||||
function renderConnections(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
graph: LGraph,
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
context: MinimapRenderContext,
|
||||
colors: ReturnType<typeof getMinimapColors>
|
||||
) {
|
||||
if (!graph || !graph._nodes) return
|
||||
|
||||
ctx.strokeStyle = colors.linkColor
|
||||
ctx.lineWidth = 0.3
|
||||
|
||||
const slotRadius = Math.max(context.scale, 0.5)
|
||||
const connections: Array<{
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
}> = []
|
||||
|
||||
for (const node of graph._nodes) {
|
||||
if (!node.outputs) continue
|
||||
|
||||
const x1 = (node.pos[0] - context.bounds.minX) * context.scale + offsetX
|
||||
const y1 = (node.pos[1] - context.bounds.minY) * context.scale + offsetY
|
||||
|
||||
for (const output of node.outputs) {
|
||||
if (!output.links) continue
|
||||
|
||||
for (const linkId of output.links) {
|
||||
const link = graph.links[linkId]
|
||||
if (!link) continue
|
||||
|
||||
const targetNode = graph.getNodeById(link.target_id)
|
||||
if (!targetNode) continue
|
||||
|
||||
const x2 =
|
||||
(targetNode.pos[0] - context.bounds.minX) * context.scale + offsetX
|
||||
const y2 =
|
||||
(targetNode.pos[1] - context.bounds.minY) * context.scale + offsetY
|
||||
|
||||
const outputX = x1 + node.size[0] * context.scale
|
||||
const outputY = y1 + node.size[1] * context.scale * 0.2
|
||||
const inputX = x2
|
||||
const inputY = y2 + targetNode.size[1] * context.scale * 0.2
|
||||
|
||||
// Draw connection line
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(outputX, outputY)
|
||||
ctx.lineTo(inputX, inputY)
|
||||
ctx.stroke()
|
||||
|
||||
connections.push({ x1: outputX, y1: outputY, x2: inputX, y2: inputY })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render connection slots on top
|
||||
ctx.fillStyle = colors.slotColor
|
||||
for (const conn of connections) {
|
||||
// Output slot
|
||||
ctx.beginPath()
|
||||
ctx.arc(conn.x1, conn.y1, slotRadius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
// Input slot
|
||||
ctx.beginPath()
|
||||
ctx.arc(conn.x2, conn.y2, slotRadius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a graph to a minimap canvas
|
||||
*/
|
||||
export function renderMinimapToCanvas(
|
||||
canvas: HTMLCanvasElement,
|
||||
graph: LGraph,
|
||||
context: MinimapRenderContext
|
||||
) {
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, context.width, context.height)
|
||||
|
||||
// Fast path for empty graph
|
||||
if (!graph || !graph._nodes || graph._nodes.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const colors = getMinimapColors()
|
||||
const offsetX = (context.width - context.bounds.width * context.scale) / 2
|
||||
const offsetY = (context.height - context.bounds.height * context.scale) / 2
|
||||
|
||||
// Render in correct order: groups -> links -> nodes
|
||||
if (context.settings.showGroups) {
|
||||
renderGroups(ctx, graph, offsetX, offsetY, context, colors)
|
||||
}
|
||||
|
||||
if (context.settings.showLinks) {
|
||||
renderConnections(ctx, graph, offsetX, offsetY, context, colors)
|
||||
}
|
||||
|
||||
renderNodes(ctx, graph, offsetX, offsetY, context, colors)
|
||||
}
|
||||
68
src/renderer/extensions/minimap/types.ts
Normal file
68
src/renderer/extensions/minimap/types.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Minimap-specific type definitions
|
||||
*/
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
/**
|
||||
* Minimal interface for what the minimap needs from the canvas
|
||||
*/
|
||||
export interface MinimapCanvas {
|
||||
canvas: HTMLCanvasElement
|
||||
ds: {
|
||||
scale: number
|
||||
offset: [number, number]
|
||||
}
|
||||
graph?: LGraph | null
|
||||
setDirty: (fg?: boolean, bg?: boolean) => void
|
||||
}
|
||||
|
||||
export interface MinimapRenderContext {
|
||||
bounds: {
|
||||
minX: number
|
||||
minY: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
scale: number
|
||||
settings: MinimapRenderSettings
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface MinimapRenderSettings {
|
||||
nodeColors: boolean
|
||||
showLinks: boolean
|
||||
showGroups: boolean
|
||||
renderBypass: boolean
|
||||
renderError: boolean
|
||||
}
|
||||
|
||||
export interface MinimapBounds {
|
||||
minX: number
|
||||
minY: number
|
||||
maxX: number
|
||||
maxY: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface ViewportTransform {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface UpdateFlags {
|
||||
bounds: boolean
|
||||
nodes: boolean
|
||||
connections: boolean
|
||||
viewport: boolean
|
||||
}
|
||||
|
||||
export type MinimapSettingsKey =
|
||||
| 'Comfy.Minimap.NodeColors'
|
||||
| 'Comfy.Minimap.ShowLinks'
|
||||
| 'Comfy.Minimap.ShowGroups'
|
||||
| 'Comfy.Minimap.RenderBypassState'
|
||||
| 'Comfy.Minimap.RenderErrorState'
|
||||
@@ -1,38 +1,19 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { createGraphThumbnail } from '@/renderer/thumbnail/graphThumbnailRenderer'
|
||||
import { ComfyWorkflow } from '@/stores/workflowStore'
|
||||
|
||||
import { useMinimap } from './useMinimap'
|
||||
|
||||
// Store thumbnails for each workflow
|
||||
const workflowThumbnails = ref<Map<string, string>>(new Map())
|
||||
|
||||
// Shared minimap instance
|
||||
let minimap: ReturnType<typeof useMinimap> | null = null
|
||||
|
||||
export const useWorkflowThumbnail = () => {
|
||||
/**
|
||||
* Capture a thumbnail of the canvas
|
||||
*/
|
||||
const createMinimapPreview = (): Promise<string | null> => {
|
||||
try {
|
||||
if (!minimap) {
|
||||
minimap = useMinimap()
|
||||
minimap.canvasRef.value = document.createElement('canvas')
|
||||
minimap.canvasRef.value.width = minimap.width
|
||||
minimap.canvasRef.value.height = minimap.height
|
||||
}
|
||||
minimap.renderMinimap()
|
||||
|
||||
return new Promise((resolve) => {
|
||||
minimap!.canvasRef.value!.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(URL.createObjectURL(blob))
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
const thumbnailDataUrl = createGraphThumbnail()
|
||||
return Promise.resolve(thumbnailDataUrl)
|
||||
} catch (error) {
|
||||
console.error('Failed to capture canvas thumbnail:', error)
|
||||
return Promise.resolve(null)
|
||||
64
src/renderer/thumbnail/graphThumbnailRenderer.ts
Normal file
64
src/renderer/thumbnail/graphThumbnailRenderer.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
calculateMinimapScale,
|
||||
calculateNodeBounds
|
||||
} from '@/renderer/core/spatial/boundsCalculator'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
import { renderMinimapToCanvas } from '../extensions/minimap/minimapCanvasRenderer'
|
||||
|
||||
/**
|
||||
* Create a thumbnail of the current canvas's active graph.
|
||||
* Used by workflow thumbnail generation.
|
||||
*/
|
||||
export function createGraphThumbnail(): string | null {
|
||||
const canvasStore = useCanvasStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const graph = workflowStore.activeSubgraph || canvasStore.canvas?.graph
|
||||
if (!graph || !graph._nodes || graph._nodes.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const width = 250
|
||||
const height = 200
|
||||
|
||||
// Calculate bounds using spatial calculator
|
||||
const bounds = calculateNodeBounds(graph._nodes)
|
||||
if (!bounds) {
|
||||
return null
|
||||
}
|
||||
|
||||
const scale = calculateMinimapScale(bounds, width, height)
|
||||
|
||||
// Create detached canvas
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
// Render the minimap
|
||||
renderMinimapToCanvas(canvas, graph as LGraph, {
|
||||
bounds,
|
||||
scale,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: true,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width,
|
||||
height
|
||||
})
|
||||
|
||||
const dataUrl = canvas.toDataURL()
|
||||
|
||||
// Explicit cleanup (optional but good practice)
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
}
|
||||
|
||||
return dataUrl
|
||||
}
|
||||
@@ -495,7 +495,6 @@ const zSettings = z.object({
|
||||
'Comfy.Load3D.LightAdjustmentIncrement': z.number(),
|
||||
'Comfy.Load3D.CameraType': z.enum(['perspective', 'orthographic']),
|
||||
'Comfy.Load3D.3DViewerEnable': z.boolean(),
|
||||
'Comfy.Memory.AllowManualUnload': z.boolean(),
|
||||
'pysssss.SnapToGrid': z.boolean(),
|
||||
/** VHS setting is used for queue video preview support. */
|
||||
'VHS.AdvancedPreviews': z.string(),
|
||||
|
||||
@@ -35,7 +35,6 @@ import type {
|
||||
NodeId
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import { WorkflowTemplates } from '@/types/workflowTemplateTypes'
|
||||
|
||||
@@ -1021,56 +1020,6 @@ export class ComfyApi extends EventTarget {
|
||||
return (await axios.get(this.internalURL('/folder_paths'))).data
|
||||
}
|
||||
|
||||
/* Frees memory by unloading models and optionally freeing execution cache
|
||||
* @param {Object} options - The options object
|
||||
* @param {boolean} options.freeExecutionCache - If true, also frees execution cache
|
||||
*/
|
||||
async freeMemory(options: { freeExecutionCache: boolean }) {
|
||||
try {
|
||||
let mode = ''
|
||||
if (options.freeExecutionCache) {
|
||||
mode = '{"unload_models": true, "free_memory": true}'
|
||||
} else {
|
||||
mode = '{"unload_models": true}'
|
||||
}
|
||||
|
||||
const res = await this.fetchApi(`/free`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: mode
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
if (options.freeExecutionCache) {
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: 'Models and Execution Cache have been cleared.',
|
||||
life: 3000
|
||||
})
|
||||
} else {
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: 'Models have been unloaded.',
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
} else {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary:
|
||||
'Unloading of models failed. Installed ComfyUI may be an outdated version.',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: 'An error occurred while trying to unload models.',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom nodes i18n data from the server.
|
||||
*
|
||||
@@ -1082,7 +1031,7 @@ export class ComfyApi extends EventTarget {
|
||||
|
||||
/**
|
||||
* Checks if the server supports a specific feature.
|
||||
* @param featureName The name of the feature to check (supports dot notation for nested values)
|
||||
* @param featureName The name of the feature to check
|
||||
* @returns true if the feature is supported, false otherwise
|
||||
*/
|
||||
serverSupportsFeature(featureName: string): boolean {
|
||||
@@ -1091,7 +1040,7 @@ export class ComfyApi extends EventTarget {
|
||||
|
||||
/**
|
||||
* Gets a server feature flag value.
|
||||
* @param featureName The name of the feature to get (supports dot notation for nested values)
|
||||
* @param featureName The name of the feature to get
|
||||
* @param defaultValue The default value if the feature is not found
|
||||
* @returns The feature value or default
|
||||
*/
|
||||
|
||||
@@ -27,16 +27,16 @@ enum ManagerRoute {
|
||||
UPDATE_ALL = 'manager/queue/update_all',
|
||||
UNINSTALL = 'manager/queue/uninstall',
|
||||
DISABLE = 'manager/queue/disable',
|
||||
// FIX_NODE is currently unused but kept for potential future implementation
|
||||
FIX_NODE = 'manager/queue/fix',
|
||||
LIST_INSTALLED = 'customnode/installed',
|
||||
GET_NODES = 'customnode/getmappings',
|
||||
GET_PACKS = 'customnode/getlist',
|
||||
IMPORT_FAIL_INFO = 'customnode/import_fail_info',
|
||||
REBOOT = 'manager/reboot',
|
||||
IS_LEGACY_MANAGER_UI = 'manager/is_legacy_manager_ui'
|
||||
REBOOT = 'manager/reboot'
|
||||
}
|
||||
|
||||
const managerApiClient = axios.create({
|
||||
baseURL: api.apiURL('/v2/'),
|
||||
baseURL: api.apiURL(''),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
@@ -247,15 +247,6 @@ export const useComfyManagerService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const isLegacyManagerUI = async (signal?: AbortSignal) => {
|
||||
const errorContext = 'Checking if user set Manager to use the legacy UI'
|
||||
|
||||
return executeRequest<{ is_legacy_manager_ui: boolean }>(
|
||||
() => managerApiClient.get(ManagerRoute.IS_LEGACY_MANAGER_UI, { signal }),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading,
|
||||
@@ -277,7 +268,6 @@ export const useComfyManagerService = () => {
|
||||
updateAllPacks,
|
||||
|
||||
// System operations
|
||||
rebootComfyUI,
|
||||
isLegacyManagerUI
|
||||
rebootComfyUI
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { merge } from 'es-toolkit/compat'
|
||||
import { Component } from 'vue'
|
||||
|
||||
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
|
||||
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
|
||||
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
|
||||
@@ -20,7 +23,11 @@ import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsCo
|
||||
import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkflowsDialogHeader.vue'
|
||||
import { t } from '@/i18n'
|
||||
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
|
||||
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
type DialogComponentProps,
|
||||
type ShowDialogOptions,
|
||||
useDialogStore
|
||||
} from '@/stores/dialogStore'
|
||||
|
||||
export type ConfirmationDialogType =
|
||||
| 'default'
|
||||
@@ -424,6 +431,33 @@ export const useDialogService = () => {
|
||||
}
|
||||
}
|
||||
|
||||
function showLayoutDialog(options: {
|
||||
key: string
|
||||
component: Component
|
||||
props: { onClose: () => void }
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
}) {
|
||||
const layoutDefaultProps: DialogComponentProps = {
|
||||
headless: true,
|
||||
unstyled: true,
|
||||
modal: true,
|
||||
closable: false,
|
||||
pt: {
|
||||
mask: {
|
||||
class: 'bg-black bg-opacity-40'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dialogStore.showDialog({
|
||||
...options,
|
||||
dialogComponentProps: merge(
|
||||
layoutDefaultProps,
|
||||
options.dialogComponentProps || {}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
showLoadWorkflowWarning,
|
||||
showMissingModelsWarning,
|
||||
@@ -443,6 +477,7 @@ export const useDialogService = () => {
|
||||
prompt,
|
||||
confirm,
|
||||
toggleManagerDialog,
|
||||
toggleManagerProgressDialog
|
||||
toggleManagerProgressDialog,
|
||||
showLayoutDialog
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { toRaw } from 'vue'
|
||||
|
||||
import { useWorkflowThumbnail } from '@/composables/useWorkflowThumbnail'
|
||||
import { t } from '@/i18n'
|
||||
import { LGraph, LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import type { SerialisableGraph, Vector2 } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowThumbnail } from '@/renderer/thumbnail/composables/useWorkflowThumbnail'
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
|
||||
|
||||
@@ -29,7 +29,6 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const enabledPacksIds = ref<Set<string>>(new Set())
|
||||
const disabledPacksIds = ref<Set<string>>(new Set())
|
||||
const installedPacksIds = ref<Set<string>>(new Set())
|
||||
const installingPacksIds = ref<Set<string>>(new Set())
|
||||
const isStale = ref(true)
|
||||
const taskLogs = ref<TaskLog[]>([])
|
||||
|
||||
@@ -50,9 +49,6 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
isInstalledPackId(packName) &&
|
||||
enabledPacksIds.value.has(packName)
|
||||
|
||||
const isInstallingPackId = (packName: string | undefined): boolean =>
|
||||
!!packName && installingPacksIds.value.has(packName)
|
||||
|
||||
const packsToIdSet = (packs: ManagerPackInstalled[]) =>
|
||||
packs.reduce((acc, pack) => {
|
||||
const id = pack.cnr_id || pack.aux_id
|
||||
@@ -121,11 +117,7 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
whenever(isStale, refreshInstalledList, { immediate: true })
|
||||
whenever(uncompletedCount, () => showManagerProgressDialog())
|
||||
|
||||
const withLogs = (
|
||||
task: () => Promise<null>,
|
||||
taskName: string,
|
||||
packId?: string
|
||||
) => {
|
||||
const withLogs = (task: () => Promise<null>, taskName: string) => {
|
||||
const { startListening, stopListening, logs } = useServerLogs()
|
||||
|
||||
const loggedTask = async () => {
|
||||
@@ -136,9 +128,6 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
|
||||
const onComplete = async () => {
|
||||
await stopListening()
|
||||
if (packId) {
|
||||
installingPacksIds.value.delete(packId)
|
||||
}
|
||||
setStale()
|
||||
}
|
||||
|
||||
@@ -163,11 +152,8 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
}
|
||||
}
|
||||
|
||||
installingPacksIds.value.add(params.id)
|
||||
const task = () => managerService.installPack(params, signal)
|
||||
enqueueTask(
|
||||
withLogs(task, `${actionDescription} ${params.id}`, params.id)
|
||||
)
|
||||
enqueueTask(withLogs(task, `${actionDescription} ${params.id}`))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
@@ -176,16 +162,14 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
installPack.clear()
|
||||
installPack.cancel()
|
||||
const task = () => managerService.uninstallPack(params, signal)
|
||||
enqueueTask(
|
||||
withLogs(task, t('manager.uninstalling', { id: params.id }), params.id)
|
||||
)
|
||||
enqueueTask(withLogs(task, t('manager.uninstalling', { id: params.id })))
|
||||
}
|
||||
|
||||
const updatePack = useCachedRequest<ManagerPackInfo, void>(
|
||||
async (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
updateAllPacks.cancel()
|
||||
const task = () => managerService.updatePack(params, signal)
|
||||
enqueueTask(withLogs(task, t('g.updating', { id: params.id }), params.id))
|
||||
enqueueTask(withLogs(task, t('g.updating', { id: params.id })))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
@@ -200,7 +184,7 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
|
||||
const disablePack = (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const task = () => managerService.disablePack(params, signal)
|
||||
enqueueTask(withLogs(task, t('g.disabling', { id: params.id }), params.id))
|
||||
enqueueTask(withLogs(task, t('g.disabling', { id: params.id })))
|
||||
}
|
||||
|
||||
const getInstalledPackVersion = (packId: string) => {
|
||||
@@ -228,7 +212,6 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
installedPacksIds,
|
||||
isPackInstalled: isInstalledPackId,
|
||||
isPackEnabled: isEnabledPackId,
|
||||
isPackInstalling: isInstallingPackId,
|
||||
getInstalledPackVersion,
|
||||
refreshInstalledList,
|
||||
|
||||
|
||||
@@ -28,9 +28,11 @@ interface CustomDialogComponentProps {
|
||||
pt?: DialogPassThroughOptions
|
||||
closeOnEscape?: boolean
|
||||
dismissableMask?: boolean
|
||||
unstyled?: boolean
|
||||
headless?: boolean
|
||||
}
|
||||
|
||||
type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
|
||||
export type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
|
||||
CustomDialogComponentProps
|
||||
|
||||
interface DialogInstance {
|
||||
|
||||
@@ -2,8 +2,8 @@ import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import { useWorkflowThumbnail } from '@/composables/useWorkflowThumbnail'
|
||||
import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowThumbnail } from '@/renderer/thumbnail/composables/useWorkflowThumbnail'
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
@@ -19,7 +19,7 @@ export const IsInstallingKey: InjectionKey<Ref<boolean>> =
|
||||
Symbol('isInstalling')
|
||||
|
||||
export enum ManagerWsQueueStatus {
|
||||
DONE = 'all-done',
|
||||
DONE = 'done',
|
||||
IN_PROGRESS = 'in_progress'
|
||||
}
|
||||
|
||||
|
||||
2
src/types/custom_components/index.d.ts
vendored
Normal file
2
src/types/custom_components/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './navTypes'
|
||||
export * from './widgetTypes'
|
||||
9
src/types/custom_components/navTypes.ts
Normal file
9
src/types/custom_components/navTypes.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface NavItemData {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface NavGroupData {
|
||||
title: string
|
||||
items: NavItemData[]
|
||||
}
|
||||
3
src/types/custom_components/widgetTypes.ts
Normal file
3
src/types/custom_components/widgetTypes.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { InjectionKey } from 'vue'
|
||||
|
||||
export const OnCloseKey: InjectionKey<() => void> = Symbol()
|
||||
@@ -27,25 +27,41 @@ export class ExecutableGroupNodeChildDTO extends ExecutableNodeDTO {
|
||||
}
|
||||
|
||||
override resolveInput(slot: number) {
|
||||
// Check if this group node is inside a subgraph (unsupported)
|
||||
if (this.id.split(':').length > 2) {
|
||||
throw new Error(
|
||||
'Group nodes inside subgraphs are not supported. Please convert the group node to a subgraph instead.'
|
||||
)
|
||||
}
|
||||
|
||||
const inputNode = this.node.getInputNode(slot)
|
||||
if (!inputNode) return
|
||||
|
||||
const link = this.node.getInputLink(slot)
|
||||
if (!link) throw new Error('Failed to get input link')
|
||||
|
||||
const id = String(inputNode.id).split(':').at(-1)
|
||||
if (id === undefined) throw new Error('Invalid input node id')
|
||||
const inputNodeId = String(inputNode.id)
|
||||
|
||||
// Try to find the node using the full ID first (for nodes outside the group)
|
||||
let inputNodeDto = this.nodesByExecutionId?.get(inputNodeId)
|
||||
|
||||
// If not found, try with just the last part of the ID (for nodes inside the group)
|
||||
if (!inputNodeDto) {
|
||||
const id = inputNodeId.split(':').at(-1)
|
||||
if (id !== undefined) {
|
||||
inputNodeDto = this.nodesByExecutionId?.get(id)
|
||||
}
|
||||
}
|
||||
|
||||
const inputNodeDto = this.nodesByExecutionId?.get(id)
|
||||
if (!inputNodeDto) {
|
||||
throw new Error(
|
||||
`Failed to get input node ${id} for group node child ${this.id} with slot ${slot}`
|
||||
`Failed to get input node ${inputNodeId} for group node child ${this.id} with slot ${slot}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
node: inputNodeDto,
|
||||
origin_id: String(inputNode.id),
|
||||
origin_id: inputNodeId,
|
||||
origin_slot: link.origin_slot
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,14 +79,14 @@ export default {
|
||||
colors: {
|
||||
zinc: {
|
||||
50: '#fafafa',
|
||||
100: '#f4f4f5',
|
||||
100: '#8282821a',
|
||||
200: '#e4e4e7',
|
||||
300: '#d4d4d8',
|
||||
400: '#a1a1aa',
|
||||
500: '#71717a',
|
||||
600: '#52525b',
|
||||
700: '#3f3f46',
|
||||
800: '#27272a',
|
||||
700: '#38393b',
|
||||
800: '#262729',
|
||||
900: '#18181b',
|
||||
950: '#09090b'
|
||||
},
|
||||
|
||||
@@ -1,440 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ManagerProgressFooter from '@/components/dialog/footer/ManagerProgressFooter.vue'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { TaskLog } from '@/types/comfyManagerTypes'
|
||||
|
||||
// Mock modules
|
||||
vi.mock('@/stores/comfyManagerStore')
|
||||
vi.mock('@/stores/dialogStore')
|
||||
vi.mock('@/stores/settingStore')
|
||||
vi.mock('@/stores/commandStore')
|
||||
vi.mock('@/services/comfyManagerService')
|
||||
|
||||
// Mock useEventListener to capture the event handler
|
||||
let reconnectHandler: (() => void) | null = null
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
const actual = await vi.importActual('@vueuse/core')
|
||||
return {
|
||||
...actual,
|
||||
useEventListener: vi.fn(
|
||||
(_target: any, event: string, handler: any, _options: any) => {
|
||||
if (event === 'reconnected') {
|
||||
reconnectHandler = handler
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
vi.mock('@/services/workflowService', () => ({
|
||||
useWorkflowService: vi.fn(() => ({
|
||||
reloadCurrentWorkflow: vi.fn().mockResolvedValue(undefined)
|
||||
}))
|
||||
}))
|
||||
vi.mock('@/stores/workspace/colorPaletteStore', () => ({
|
||||
useColorPaletteStore: vi.fn(() => ({
|
||||
completedActivePalette: {
|
||||
light_theme: false
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
// Helper function to mount component with required setup
|
||||
const mountComponent = (options: { captureError?: boolean } = {}) => {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {}
|
||||
}
|
||||
})
|
||||
|
||||
const config: any = {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n],
|
||||
mocks: {
|
||||
$t: (key: string) => key // Mock i18n translation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add error handler for tests that expect errors
|
||||
if (options.captureError) {
|
||||
config.global.config = {
|
||||
errorHandler: () => {
|
||||
// Suppress error in test
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mount(ManagerProgressFooter, config)
|
||||
}
|
||||
|
||||
describe('ManagerProgressFooter', () => {
|
||||
const mockTaskLogs: TaskLog[] = []
|
||||
|
||||
const mockComfyManagerStore = {
|
||||
uncompletedCount: 0,
|
||||
taskLogs: mockTaskLogs,
|
||||
allTasksDone: true,
|
||||
clearLogs: vi.fn(),
|
||||
setStale: vi.fn(),
|
||||
// Add other required properties
|
||||
isLoading: { value: false },
|
||||
error: { value: null },
|
||||
statusMessage: { value: 'DONE' },
|
||||
installedPacks: {},
|
||||
installedPacksIds: new Set(),
|
||||
isPackInstalled: vi.fn(),
|
||||
isPackEnabled: vi.fn(),
|
||||
getInstalledPackVersion: vi.fn(),
|
||||
refreshInstalledList: vi.fn(),
|
||||
installPack: vi.fn(),
|
||||
uninstallPack: vi.fn(),
|
||||
updatePack: vi.fn(),
|
||||
updateAllPacks: vi.fn(),
|
||||
disablePack: vi.fn(),
|
||||
enablePack: vi.fn()
|
||||
}
|
||||
|
||||
const mockDialogStore = {
|
||||
closeDialog: vi.fn(),
|
||||
// Add other required properties
|
||||
dialogStack: { value: [] },
|
||||
showDialog: vi.fn(),
|
||||
$id: 'dialog',
|
||||
$state: {} as any,
|
||||
$patch: vi.fn(),
|
||||
$reset: vi.fn(),
|
||||
$subscribe: vi.fn(),
|
||||
$dispose: vi.fn(),
|
||||
$onAction: vi.fn()
|
||||
}
|
||||
|
||||
const mockSettingStore = {
|
||||
get: vi.fn().mockReturnValue(false),
|
||||
set: vi.fn(),
|
||||
// Add other required properties
|
||||
settingValues: { value: {} },
|
||||
settingsById: { value: {} },
|
||||
exists: vi.fn(),
|
||||
getDefaultValue: vi.fn(),
|
||||
loadSettingValues: vi.fn(),
|
||||
updateValue: vi.fn(),
|
||||
$id: 'setting',
|
||||
$state: {} as any,
|
||||
$patch: vi.fn(),
|
||||
$reset: vi.fn(),
|
||||
$subscribe: vi.fn(),
|
||||
$dispose: vi.fn(),
|
||||
$onAction: vi.fn()
|
||||
}
|
||||
|
||||
const mockProgressDialogStore = {
|
||||
isExpanded: false,
|
||||
toggle: vi.fn(),
|
||||
collapse: vi.fn(),
|
||||
expand: vi.fn()
|
||||
}
|
||||
|
||||
const mockCommandStore = {
|
||||
execute: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
|
||||
const mockComfyManagerService = {
|
||||
rebootComfyUI: vi.fn().mockResolvedValue(null)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset task logs
|
||||
mockTaskLogs.length = 0
|
||||
mockComfyManagerStore.taskLogs = mockTaskLogs
|
||||
// Reset event handler
|
||||
reconnectHandler = null
|
||||
|
||||
vi.mocked(useComfyManagerStore).mockReturnValue(
|
||||
mockComfyManagerStore as any
|
||||
)
|
||||
vi.mocked(useDialogStore).mockReturnValue(mockDialogStore as any)
|
||||
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore as any)
|
||||
vi.mocked(useManagerProgressDialogStore).mockReturnValue(
|
||||
mockProgressDialogStore as any
|
||||
)
|
||||
vi.mocked(useCommandStore).mockReturnValue(mockCommandStore as any)
|
||||
vi.mocked(useComfyManagerService).mockReturnValue(
|
||||
mockComfyManagerService as any
|
||||
)
|
||||
})
|
||||
|
||||
describe('State 1: Queue Running', () => {
|
||||
it('should display loading spinner and progress counter when queue is running', async () => {
|
||||
// Setup queue running state
|
||||
mockComfyManagerStore.uncompletedCount = 3
|
||||
mockTaskLogs.push(
|
||||
{ taskName: 'Installing pack1', logs: [] },
|
||||
{ taskName: 'Installing pack2', logs: [] },
|
||||
{ taskName: 'Installing pack3', logs: [] }
|
||||
)
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Check loading spinner exists (DotSpinner component)
|
||||
expect(wrapper.find('.inline-flex').exists()).toBe(true)
|
||||
|
||||
// Check current task name is displayed
|
||||
expect(wrapper.text()).toContain('Installing pack3')
|
||||
|
||||
// Check progress counter (completed: 2 of 3)
|
||||
expect(wrapper.text()).toMatch(/2.*3/)
|
||||
|
||||
// Check expand/collapse button exists
|
||||
const expandButton = wrapper.find('[aria-label="Expand"]')
|
||||
expect(expandButton.exists()).toBe(true)
|
||||
|
||||
// Check Apply Changes button is NOT shown
|
||||
expect(wrapper.text()).not.toContain('manager.applyChanges')
|
||||
})
|
||||
|
||||
it('should toggle expansion when expand button is clicked', async () => {
|
||||
mockComfyManagerStore.uncompletedCount = 1
|
||||
mockTaskLogs.push({ taskName: 'Installing', logs: [] })
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const expandButton = wrapper.find('[aria-label="Expand"]')
|
||||
await expandButton.trigger('click')
|
||||
|
||||
expect(mockProgressDialogStore.toggle).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('State 2: Tasks Completed (Waiting for Restart)', () => {
|
||||
it('should display check mark and Apply Changes button when all tasks are done', async () => {
|
||||
// Setup tasks completed state
|
||||
mockComfyManagerStore.uncompletedCount = 0
|
||||
mockTaskLogs.push(
|
||||
{ taskName: 'Installed pack1', logs: [] },
|
||||
{ taskName: 'Installed pack2', logs: [] }
|
||||
)
|
||||
mockComfyManagerStore.allTasksDone = true
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Check check mark emoji
|
||||
expect(wrapper.text()).toContain('✅')
|
||||
|
||||
// Check restart message (split into 3 parts)
|
||||
expect(wrapper.text()).toContain('manager.clickToFinishSetup')
|
||||
expect(wrapper.text()).toContain('manager.applyChanges')
|
||||
expect(wrapper.text()).toContain('manager.toFinishSetup')
|
||||
|
||||
// Check Apply Changes button exists
|
||||
const applyButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('manager.applyChanges'))
|
||||
expect(applyButton).toBeTruthy()
|
||||
|
||||
// Check no progress counter
|
||||
expect(wrapper.text()).not.toMatch(/\d+.*of.*\d+/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('State 3: Restarting', () => {
|
||||
it('should display restarting message and spinner during restart', async () => {
|
||||
// Setup completed state first
|
||||
mockComfyManagerStore.uncompletedCount = 0
|
||||
mockComfyManagerStore.allTasksDone = true
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Click Apply Changes to trigger restart
|
||||
const applyButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('manager.applyChanges'))
|
||||
await applyButton?.trigger('click')
|
||||
|
||||
// Wait for state update
|
||||
await nextTick()
|
||||
|
||||
// Check restarting message
|
||||
expect(wrapper.text()).toContain('manager.restartingBackend')
|
||||
|
||||
// Check loading spinner during restart
|
||||
expect(wrapper.find('.inline-flex').exists()).toBe(true)
|
||||
|
||||
// Check Apply Changes button is hidden
|
||||
expect(wrapper.text()).not.toContain('manager.applyChanges')
|
||||
})
|
||||
})
|
||||
|
||||
describe('State 4: Restart Completed', () => {
|
||||
it('should display success message and auto-close after 3 seconds', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
// Setup completed state
|
||||
mockComfyManagerStore.uncompletedCount = 0
|
||||
mockComfyManagerStore.allTasksDone = true
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Trigger restart
|
||||
const applyButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('manager.applyChanges'))
|
||||
await applyButton?.trigger('click')
|
||||
|
||||
// Wait for event listener to be set up
|
||||
await nextTick()
|
||||
|
||||
// Trigger the reconnect handler directly
|
||||
if (reconnectHandler) {
|
||||
await reconnectHandler()
|
||||
}
|
||||
|
||||
// Wait for restart completed state
|
||||
await nextTick()
|
||||
|
||||
// Check success message
|
||||
expect(wrapper.text()).toContain('🎉')
|
||||
expect(wrapper.text()).toContain(
|
||||
'manager.extensionsSuccessfullyInstalled'
|
||||
)
|
||||
|
||||
// Check dialog closes after 3 seconds
|
||||
vi.advanceTimersByTime(3000)
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(mockDialogStore.closeDialog).toHaveBeenCalledWith({
|
||||
key: 'global-manager-progress-dialog'
|
||||
})
|
||||
expect(mockComfyManagerStore.clearLogs).toHaveBeenCalled()
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Common Features', () => {
|
||||
it('should always display close button', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const closeButton = wrapper.find('[aria-label="Close"]')
|
||||
expect(closeButton.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should close dialog when close button is clicked', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const closeButton = wrapper.find('[aria-label="Close"]')
|
||||
await closeButton.trigger('click')
|
||||
|
||||
expect(mockDialogStore.closeDialog).toHaveBeenCalledWith({
|
||||
key: 'global-manager-progress-dialog'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Toast Management', () => {
|
||||
it('should suppress reconnection toasts during restart', async () => {
|
||||
mockComfyManagerStore.uncompletedCount = 0
|
||||
mockComfyManagerStore.allTasksDone = true
|
||||
mockSettingStore.get.mockReturnValue(false) // Original setting
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Click Apply Changes
|
||||
const applyButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('manager.applyChanges'))
|
||||
await applyButton?.trigger('click')
|
||||
|
||||
// Check toast setting was disabled
|
||||
expect(mockSettingStore.set).toHaveBeenCalledWith(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('should restore toast settings after restart completes', async () => {
|
||||
mockComfyManagerStore.uncompletedCount = 0
|
||||
mockComfyManagerStore.allTasksDone = true
|
||||
mockSettingStore.get.mockReturnValue(false) // Original setting
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Click Apply Changes
|
||||
const applyButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('manager.applyChanges'))
|
||||
await applyButton?.trigger('click')
|
||||
|
||||
// Wait for event listener to be set up
|
||||
await nextTick()
|
||||
|
||||
// Trigger the reconnect handler directly
|
||||
if (reconnectHandler) {
|
||||
await reconnectHandler()
|
||||
}
|
||||
|
||||
// Wait for settings restoration
|
||||
await nextTick()
|
||||
|
||||
expect(mockSettingStore.set).toHaveBeenCalledWith(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
false // Restored to original
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should restore state and close dialog on restart error', async () => {
|
||||
mockComfyManagerStore.uncompletedCount = 0
|
||||
mockComfyManagerStore.allTasksDone = true
|
||||
|
||||
// Mock restart to throw error
|
||||
mockComfyManagerService.rebootComfyUI.mockRejectedValue(
|
||||
new Error('Restart failed')
|
||||
)
|
||||
|
||||
const wrapper = mountComponent({ captureError: true })
|
||||
|
||||
// Click Apply Changes
|
||||
const applyButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('manager.applyChanges'))
|
||||
|
||||
expect(applyButton).toBeTruthy()
|
||||
|
||||
// The component throws the error but Vue Test Utils catches it
|
||||
// We need to check if the error handling logic was executed
|
||||
await applyButton!.trigger('click').catch(() => {
|
||||
// Error is expected, ignore it
|
||||
})
|
||||
|
||||
// Wait for error handling
|
||||
await nextTick()
|
||||
|
||||
// Check dialog was closed on error
|
||||
expect(mockDialogStore.closeDialog).toHaveBeenCalled()
|
||||
// Check toast settings were restored
|
||||
expect(mockSettingStore.set).toHaveBeenCalledWith(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
false
|
||||
)
|
||||
// Check that the error handler was called
|
||||
expect(mockComfyManagerService.rebootComfyUI).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,121 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { isReactive, isReadonly } from 'vue'
|
||||
|
||||
import {
|
||||
ServerFeatureFlag,
|
||||
useFeatureFlags
|
||||
} from '@/composables/useFeatureFlags'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
// Mock the API module
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getServerFeature: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
describe('useFeatureFlags', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('flags object', () => {
|
||||
it('should provide reactive readonly flags', () => {
|
||||
const { flags } = useFeatureFlags()
|
||||
|
||||
expect(isReadonly(flags)).toBe(true)
|
||||
expect(isReactive(flags)).toBe(true)
|
||||
})
|
||||
|
||||
it('should access supportsPreviewMetadata', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(path, defaultValue) => {
|
||||
if (path === ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA)
|
||||
return true as any
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
expect(flags.supportsPreviewMetadata).toBe(true)
|
||||
expect(api.getServerFeature).toHaveBeenCalledWith(
|
||||
ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA
|
||||
)
|
||||
})
|
||||
|
||||
it('should access maxUploadSize', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(path, defaultValue) => {
|
||||
if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE)
|
||||
return 209715200 as any // 200MB
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
expect(flags.maxUploadSize).toBe(209715200)
|
||||
expect(api.getServerFeature).toHaveBeenCalledWith(
|
||||
ServerFeatureFlag.MAX_UPLOAD_SIZE
|
||||
)
|
||||
})
|
||||
|
||||
it('should return undefined when features are not available and no default provided', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(_path, defaultValue) => defaultValue as any
|
||||
)
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
expect(flags.supportsPreviewMetadata).toBeUndefined()
|
||||
expect(flags.maxUploadSize).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('featureFlag', () => {
|
||||
it('should create reactive computed for custom feature flags', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(path, defaultValue) => {
|
||||
if (path === 'custom.feature') return 'custom-value' as any
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
|
||||
const { featureFlag } = useFeatureFlags()
|
||||
const customFlag = featureFlag('custom.feature', 'default')
|
||||
|
||||
expect(customFlag.value).toBe('custom-value')
|
||||
expect(api.getServerFeature).toHaveBeenCalledWith(
|
||||
'custom.feature',
|
||||
'default'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle nested paths', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(path, defaultValue) => {
|
||||
if (path === 'extension.custom.nested.feature') return true as any
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
|
||||
const { featureFlag } = useFeatureFlags()
|
||||
const nestedFlag = featureFlag('extension.custom.nested.feature', false)
|
||||
|
||||
expect(nestedFlag.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should work with ServerFeatureFlag enum', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(path, defaultValue) => {
|
||||
if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE)
|
||||
return 104857600 as any
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
|
||||
const { featureFlag } = useFeatureFlags()
|
||||
const maxUploadSize = featureFlag(ServerFeatureFlag.MAX_UPLOAD_SIZE)
|
||||
|
||||
expect(maxUploadSize.value).toBe(104857600)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,28 +3,39 @@ import { nextTick } from 'vue'
|
||||
|
||||
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
const triggerRAF = async () => {
|
||||
// Trigger all RAF callbacks
|
||||
Object.values(rafCallbacks).forEach((cb) => cb?.())
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
const mockPause = vi.fn()
|
||||
const mockResume = vi.fn()
|
||||
|
||||
vi.mock('@vueuse/core', () => {
|
||||
const callbacks: Record<string, () => void> = {}
|
||||
let callbackId = 0
|
||||
const rafCallbacks: Record<string, () => void> = {}
|
||||
let rafCallbackId = 0
|
||||
|
||||
vi.mock('@vueuse/core', () => {
|
||||
return {
|
||||
useRafFn: vi.fn((callback, options) => {
|
||||
const id = callbackId++
|
||||
callbacks[id] = callback
|
||||
const id = rafCallbackId++
|
||||
rafCallbacks[id] = callback
|
||||
|
||||
if (options?.immediate !== false) {
|
||||
void Promise.resolve().then(() => callback())
|
||||
}
|
||||
|
||||
const resumeFn = vi.fn(() => {
|
||||
mockResume()
|
||||
// Execute the RAF callback immediately when resumed
|
||||
if (rafCallbacks[id]) {
|
||||
rafCallbacks[id]()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
pause: mockPause,
|
||||
resume: vi.fn(() => {
|
||||
mockResume()
|
||||
void Promise.resolve().then(() => callbacks[id]?.())
|
||||
})
|
||||
resume: resumeFn
|
||||
}
|
||||
}),
|
||||
useThrottleFn: vi.fn((callback) => {
|
||||
@@ -142,7 +153,9 @@ vi.mock('@/stores/workflowStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
const { useMinimap } = await import('@/composables/useMinimap')
|
||||
const { useMinimap } = await import(
|
||||
'@/renderer/extensions/minimap/composables/useMinimap'
|
||||
)
|
||||
const { api } = await import('@/scripts/api')
|
||||
|
||||
describe('useMinimap', () => {
|
||||
@@ -425,7 +438,19 @@ describe('useMinimap', () => {
|
||||
|
||||
await minimap.init()
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
// Force initial render
|
||||
minimap.renderMinimap()
|
||||
|
||||
// Force a render by triggering a graph change
|
||||
mockGraph._nodes.push({
|
||||
id: 'new-node',
|
||||
pos: [150, 150],
|
||||
size: [100, 50]
|
||||
})
|
||||
|
||||
// Trigger RAF to process changes
|
||||
await triggerRAF()
|
||||
await nextTick()
|
||||
|
||||
expect(getContextSpy).toHaveBeenCalled()
|
||||
expect(getContextSpy).toHaveBeenCalledWith('2d')
|
||||
@@ -438,7 +463,15 @@ describe('useMinimap', () => {
|
||||
|
||||
await minimap.init()
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
// Force initial render
|
||||
minimap.renderMinimap()
|
||||
|
||||
// Force a render by modifying a node position
|
||||
mockGraph._nodes[0].pos = [50, 50]
|
||||
|
||||
// Trigger RAF to process changes
|
||||
await triggerRAF()
|
||||
await nextTick()
|
||||
|
||||
const renderingOccurred =
|
||||
mockContext2D.clearRect.mock.calls.length > 0 ||
|
||||
@@ -449,6 +482,15 @@ describe('useMinimap', () => {
|
||||
console.log('Minimap initialized:', minimap.initialized.value)
|
||||
console.log('Canvas exists:', !!defaultCanvasStore.canvas)
|
||||
console.log('Graph exists:', !!defaultCanvasStore.canvas?.graph)
|
||||
console.log(
|
||||
'clearRect calls:',
|
||||
mockContext2D.clearRect.mock.calls.length
|
||||
)
|
||||
console.log('fillRect calls:', mockContext2D.fillRect.mock.calls.length)
|
||||
console.log(
|
||||
'getContext calls:',
|
||||
mockCanvasElement.getContext.mock.calls.length
|
||||
)
|
||||
}
|
||||
|
||||
expect(renderingOccurred).toBe(true)
|
||||
@@ -478,6 +520,10 @@ describe('useMinimap', () => {
|
||||
minimap.canvasRef.value = mockCanvasElement
|
||||
|
||||
await minimap.init()
|
||||
|
||||
// The renderer has a fast path for empty graphs, force it to execute
|
||||
minimap.renderMinimap()
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(minimap.initialized.value).toBe(true)
|
||||
@@ -917,7 +963,7 @@ describe('useMinimap', () => {
|
||||
describe('setMinimapRef', () => {
|
||||
it('should set minimap reference', () => {
|
||||
const minimap = useMinimap()
|
||||
const ref = { value: 'test-ref' }
|
||||
const ref = document.createElement('div')
|
||||
|
||||
minimap.setMinimapRef(ref)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('useManagerQueue', () => {
|
||||
const getEventListenerCallback = () =>
|
||||
vi.mocked(api.addEventListener).mock.calls[0][1]
|
||||
|
||||
const simulateServerStatus = async (status: 'all-done' | 'in_progress') => {
|
||||
const simulateServerStatus = async (status: 'done' | 'in_progress') => {
|
||||
const event = new CustomEvent('cm-queue-status', {
|
||||
detail: { status }
|
||||
})
|
||||
@@ -49,7 +49,7 @@ describe('useManagerQueue', () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
expect(queue.statusMessage.value).toBe('done')
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -104,7 +104,7 @@ describe('useManagerQueue', () => {
|
||||
await nextTick()
|
||||
|
||||
// Should maintain the default status
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
expect(queue.statusMessage.value).toBe('done')
|
||||
})
|
||||
|
||||
it('should handle missing status property gracefully', async () => {
|
||||
@@ -119,7 +119,7 @@ describe('useManagerQueue', () => {
|
||||
await nextTick()
|
||||
|
||||
// Should maintain the default status
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
expect(queue.statusMessage.value).toBe('done')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('useManagerQueue', () => {
|
||||
it('should start the next task when server is idle and queue has items', async () => {
|
||||
const { queue, mockTask } = createQueueWithMockTask()
|
||||
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
|
||||
// Task should have been started
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
@@ -138,7 +138,7 @@ describe('useManagerQueue', () => {
|
||||
const { mockTask } = createQueueWithMockTask()
|
||||
|
||||
// Start the task
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
|
||||
// Simulate task completion
|
||||
@@ -148,7 +148,7 @@ describe('useManagerQueue', () => {
|
||||
await simulateServerStatus('in_progress')
|
||||
expect(mockTask.onComplete).not.toHaveBeenCalled()
|
||||
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
expect(mockTask.onComplete).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('useManagerQueue', () => {
|
||||
queue.enqueueTask(mockTask)
|
||||
|
||||
// Start the task
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
|
||||
// Simulate task completion
|
||||
@@ -167,7 +167,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Simulate server cycle
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
|
||||
// Should not throw errors even without onComplete
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
@@ -184,14 +184,14 @@ describe('useManagerQueue', () => {
|
||||
expect(queue.queueLength.value).toBe(2)
|
||||
|
||||
// Process first task
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
expect(queue.queueLength.value).toBe(1)
|
||||
|
||||
// Complete first task
|
||||
await mockTask1.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
expect(mockTask1.onComplete).toHaveBeenCalled()
|
||||
|
||||
// Process second task
|
||||
@@ -201,7 +201,7 @@ describe('useManagerQueue', () => {
|
||||
// Complete second task
|
||||
await mockTask2.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
expect(mockTask2.onComplete).toHaveBeenCalled()
|
||||
|
||||
// Queue should be empty and all tasks done
|
||||
@@ -219,7 +219,7 @@ describe('useManagerQueue', () => {
|
||||
queue.enqueueTask(mockTask)
|
||||
|
||||
// Start the task
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
|
||||
// Let the promise rejection happen
|
||||
@@ -231,7 +231,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Simulate server cycle
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
|
||||
// onComplete should still be called for failed tasks
|
||||
expect(mockTask.onComplete).toHaveBeenCalled()
|
||||
@@ -252,7 +252,7 @@ describe('useManagerQueue', () => {
|
||||
])
|
||||
|
||||
// Task 1
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
|
||||
// Verify state of onComplete callbacks
|
||||
@@ -266,7 +266,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Task 2
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
expect(mockTask2.task).toHaveBeenCalled()
|
||||
|
||||
// Verify state of onComplete callbacks
|
||||
@@ -279,7 +279,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Task 3
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
|
||||
// Verify state of onComplete callbacks
|
||||
expect(mockTask3.task).toHaveBeenCalled()
|
||||
@@ -297,7 +297,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Add first task and start processing
|
||||
queue.enqueueTask(mockTask1)
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
|
||||
// Add second task while first is processing
|
||||
@@ -307,7 +307,7 @@ describe('useManagerQueue', () => {
|
||||
// Complete first task
|
||||
await mockTask1.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
|
||||
// Second task should now be processed
|
||||
expect(mockTask2.task).toHaveBeenCalled()
|
||||
@@ -318,9 +318,9 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Cycle server status without any tasks
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('done')
|
||||
|
||||
// Should not cause any errors
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useMinimapGraph } from '@/renderer/extensions/minimap/composables/useMinimapGraph'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useThrottleFn: vi.fn((fn) => fn)
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
describe('useMinimapGraph', () => {
|
||||
let mockGraph: LGraph
|
||||
let onGraphChangedMock: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockGraph = {
|
||||
id: 'test-graph-123',
|
||||
_nodes: [
|
||||
{ id: '1', pos: [100, 100], size: [150, 80] },
|
||||
{ id: '2', pos: [300, 200], size: [120, 60] }
|
||||
],
|
||||
links: { link1: { id: 'link1' } },
|
||||
onNodeAdded: vi.fn(),
|
||||
onNodeRemoved: vi.fn(),
|
||||
onConnectionChange: vi.fn()
|
||||
} as any
|
||||
|
||||
onGraphChangedMock = vi.fn()
|
||||
})
|
||||
|
||||
it('should initialize with empty state', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
expect(graphManager.updateFlags.value).toEqual({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
})
|
||||
|
||||
it('should setup event listeners on init', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.init()
|
||||
|
||||
expect(api.addEventListener).toHaveBeenCalledWith(
|
||||
'graphChanged',
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
it('should wrap graph callbacks on setup', () => {
|
||||
const originalOnNodeAdded = vi.fn()
|
||||
const originalOnNodeRemoved = vi.fn()
|
||||
const originalOnConnectionChange = vi.fn()
|
||||
|
||||
mockGraph.onNodeAdded = originalOnNodeAdded
|
||||
mockGraph.onNodeRemoved = originalOnNodeRemoved
|
||||
mockGraph.onConnectionChange = originalOnConnectionChange
|
||||
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.setupEventListeners()
|
||||
|
||||
// Should wrap the callbacks
|
||||
expect(mockGraph.onNodeAdded).not.toBe(originalOnNodeAdded)
|
||||
expect(mockGraph.onNodeRemoved).not.toBe(originalOnNodeRemoved)
|
||||
expect(mockGraph.onConnectionChange).not.toBe(originalOnConnectionChange)
|
||||
|
||||
// Test wrapped callbacks
|
||||
const testNode = { id: '3' } as LGraphNode
|
||||
mockGraph.onNodeAdded!(testNode)
|
||||
|
||||
expect(originalOnNodeAdded).toHaveBeenCalledWith(testNode)
|
||||
expect(onGraphChangedMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should prevent duplicate event listener setup', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
// Store original callbacks for comparison
|
||||
// const originalCallbacks = {
|
||||
// onNodeAdded: mockGraph.onNodeAdded,
|
||||
// onNodeRemoved: mockGraph.onNodeRemoved,
|
||||
// onConnectionChange: mockGraph.onConnectionChange
|
||||
// }
|
||||
|
||||
graphManager.setupEventListeners()
|
||||
const wrappedCallbacks = {
|
||||
onNodeAdded: mockGraph.onNodeAdded,
|
||||
onNodeRemoved: mockGraph.onNodeRemoved,
|
||||
onConnectionChange: mockGraph.onConnectionChange
|
||||
}
|
||||
|
||||
// Setup again - should not re-wrap
|
||||
graphManager.setupEventListeners()
|
||||
|
||||
expect(mockGraph.onNodeAdded).toBe(wrappedCallbacks.onNodeAdded)
|
||||
expect(mockGraph.onNodeRemoved).toBe(wrappedCallbacks.onNodeRemoved)
|
||||
expect(mockGraph.onConnectionChange).toBe(
|
||||
wrappedCallbacks.onConnectionChange
|
||||
)
|
||||
})
|
||||
|
||||
it('should cleanup event listeners properly', () => {
|
||||
const originalOnNodeAdded = vi.fn()
|
||||
const originalOnNodeRemoved = vi.fn()
|
||||
const originalOnConnectionChange = vi.fn()
|
||||
|
||||
mockGraph.onNodeAdded = originalOnNodeAdded
|
||||
mockGraph.onNodeRemoved = originalOnNodeRemoved
|
||||
mockGraph.onConnectionChange = originalOnConnectionChange
|
||||
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.setupEventListeners()
|
||||
graphManager.cleanupEventListeners()
|
||||
|
||||
// Should restore original callbacks
|
||||
expect(mockGraph.onNodeAdded).toBe(originalOnNodeAdded)
|
||||
expect(mockGraph.onNodeRemoved).toBe(originalOnNodeRemoved)
|
||||
expect(mockGraph.onConnectionChange).toBe(originalOnConnectionChange)
|
||||
})
|
||||
|
||||
it('should handle cleanup for never-setup graph', () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.cleanupEventListeners()
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Attempted to cleanup event listeners for graph that was never set up'
|
||||
)
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should detect node position changes', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
// First check - cache initial state
|
||||
let hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(true) // Initial cache population
|
||||
|
||||
// No changes
|
||||
hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(false)
|
||||
|
||||
// Change node position
|
||||
mockGraph._nodes[0].pos = [200, 150]
|
||||
hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
expect(graphManager.updateFlags.value.bounds).toBe(true)
|
||||
expect(graphManager.updateFlags.value.nodes).toBe(true)
|
||||
})
|
||||
|
||||
it('should detect node count changes', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
// Cache initial state
|
||||
graphManager.checkForChanges()
|
||||
|
||||
// Add a node
|
||||
mockGraph._nodes.push({ id: '3', pos: [400, 300], size: [100, 50] } as any)
|
||||
|
||||
const hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
expect(graphManager.updateFlags.value.bounds).toBe(true)
|
||||
expect(graphManager.updateFlags.value.nodes).toBe(true)
|
||||
})
|
||||
|
||||
it('should detect connection changes', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
// Cache initial state
|
||||
graphManager.checkForChanges()
|
||||
|
||||
// Change connections
|
||||
mockGraph.links = new Map([
|
||||
[1, { id: 1 }],
|
||||
[2, { id: 2 }]
|
||||
]) as any
|
||||
|
||||
const hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
expect(graphManager.updateFlags.value.connections).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle node removal in callbacks', () => {
|
||||
const originalOnNodeRemoved = vi.fn()
|
||||
mockGraph.onNodeRemoved = originalOnNodeRemoved
|
||||
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.setupEventListeners()
|
||||
|
||||
const removedNode = { id: '2' } as LGraphNode
|
||||
mockGraph.onNodeRemoved!(removedNode)
|
||||
|
||||
expect(originalOnNodeRemoved).toHaveBeenCalledWith(removedNode)
|
||||
expect(onGraphChangedMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should destroy properly', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.init()
|
||||
graphManager.setupEventListeners()
|
||||
graphManager.destroy()
|
||||
|
||||
expect(api.removeEventListener).toHaveBeenCalledWith(
|
||||
'graphChanged',
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
it('should clear cache', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
// Populate cache
|
||||
graphManager.checkForChanges()
|
||||
|
||||
// Clear cache
|
||||
graphManager.clearCache()
|
||||
|
||||
// Should detect changes again after clear
|
||||
const hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle null graph gracefully', () => {
|
||||
const graphRef = ref(null as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
expect(() => graphManager.setupEventListeners()).not.toThrow()
|
||||
expect(() => graphManager.cleanupEventListeners()).not.toThrow()
|
||||
expect(graphManager.checkForChanges()).toBe(false)
|
||||
})
|
||||
|
||||
it('should clean up removed nodes from cache', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
// Cache initial state
|
||||
graphManager.checkForChanges()
|
||||
|
||||
// Remove a node
|
||||
mockGraph._nodes = mockGraph._nodes.filter((n) => n.id !== '2')
|
||||
|
||||
const hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
expect(graphManager.updateFlags.value.bounds).toBe(true)
|
||||
})
|
||||
|
||||
it('should throttle graph changed callback', () => {
|
||||
const throttledFn = vi.fn()
|
||||
vi.mocked(useThrottleFn).mockReturnValue(throttledFn)
|
||||
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.setupEventListeners()
|
||||
|
||||
// Trigger multiple changes rapidly
|
||||
mockGraph.onNodeAdded!({ id: '3' } as LGraphNode)
|
||||
mockGraph.onNodeAdded!({ id: '4' } as LGraphNode)
|
||||
mockGraph.onNodeAdded!({ id: '5' } as LGraphNode)
|
||||
|
||||
// Should be throttled
|
||||
expect(throttledFn).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,328 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useMinimapInteraction } from '@/renderer/extensions/minimap/composables/useMinimapInteraction'
|
||||
import type { MinimapCanvas } from '@/renderer/extensions/minimap/types'
|
||||
|
||||
describe('useMinimapInteraction', () => {
|
||||
let mockContainer: HTMLDivElement
|
||||
let mockCanvas: MinimapCanvas
|
||||
let centerViewOnMock: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockContainer = {
|
||||
getBoundingClientRect: vi.fn().mockReturnValue({
|
||||
left: 100,
|
||||
top: 50,
|
||||
width: 250,
|
||||
height: 200
|
||||
})
|
||||
} as any
|
||||
|
||||
mockCanvas = {
|
||||
ds: {
|
||||
scale: 1,
|
||||
offset: [0, 0]
|
||||
},
|
||||
setDirty: vi.fn()
|
||||
} as any
|
||||
|
||||
centerViewOnMock = vi.fn()
|
||||
})
|
||||
|
||||
it('should initialize with default values', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
expect(interaction.isDragging.value).toBe(false)
|
||||
expect(interaction.containerRect.value).toEqual({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 250,
|
||||
height: 200
|
||||
})
|
||||
})
|
||||
|
||||
it('should update container rect', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
interaction.updateContainerRect()
|
||||
|
||||
expect(mockContainer.getBoundingClientRect).toHaveBeenCalled()
|
||||
|
||||
expect(interaction.containerRect.value).toEqual({
|
||||
left: 100,
|
||||
top: 50,
|
||||
width: 250,
|
||||
height: 200
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle pointer down and start dragging', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
const event = new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 100
|
||||
})
|
||||
|
||||
interaction.handlePointerDown(event)
|
||||
|
||||
expect(interaction.isDragging.value).toBe(true)
|
||||
expect(mockContainer.getBoundingClientRect).toHaveBeenCalled()
|
||||
expect(centerViewOnMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle pointer move when dragging', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
// Start dragging
|
||||
interaction.handlePointerDown(
|
||||
new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 100
|
||||
})
|
||||
)
|
||||
|
||||
// Move pointer
|
||||
const moveEvent = new PointerEvent('pointermove', {
|
||||
clientX: 200,
|
||||
clientY: 150
|
||||
})
|
||||
|
||||
interaction.handlePointerMove(moveEvent)
|
||||
|
||||
// Should calculate world coordinates and center view
|
||||
expect(centerViewOnMock).toHaveBeenCalledTimes(2) // Once on down, once on move
|
||||
|
||||
// Calculate expected world coordinates
|
||||
const x = 200 - 100 // clientX - containerLeft
|
||||
const y = 150 - 50 // clientY - containerTop
|
||||
const offsetX = (250 - 500 * 0.5) / 2 // (width - bounds.width * scale) / 2
|
||||
const offsetY = (200 - 400 * 0.5) / 2 // (height - bounds.height * scale) / 2
|
||||
const worldX = (x - offsetX) / 0.5 + 0 // (x - offsetX) / scale + bounds.minX
|
||||
const worldY = (y - offsetY) / 0.5 + 0 // (y - offsetY) / scale + bounds.minY
|
||||
|
||||
expect(centerViewOnMock).toHaveBeenLastCalledWith(worldX, worldY)
|
||||
})
|
||||
|
||||
it('should not move when not dragging', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
const moveEvent = new PointerEvent('pointermove', {
|
||||
clientX: 200,
|
||||
clientY: 150
|
||||
})
|
||||
|
||||
interaction.handlePointerMove(moveEvent)
|
||||
|
||||
expect(centerViewOnMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle pointer up to stop dragging', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
// Start dragging
|
||||
interaction.handlePointerDown(
|
||||
new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 100
|
||||
})
|
||||
)
|
||||
|
||||
expect(interaction.isDragging.value).toBe(true)
|
||||
|
||||
interaction.handlePointerUp()
|
||||
|
||||
expect(interaction.isDragging.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle wheel events for zooming', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
deltaY: -100,
|
||||
clientX: 200,
|
||||
clientY: 150
|
||||
})
|
||||
wheelEvent.preventDefault = vi.fn()
|
||||
|
||||
interaction.handleWheel(wheelEvent)
|
||||
|
||||
// Should update canvas scale (zoom in)
|
||||
expect(mockCanvas.ds.scale).toBeCloseTo(1.1)
|
||||
expect(centerViewOnMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should respect zoom limits', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
// Set scale close to minimum
|
||||
mockCanvas.ds.scale = 0.11
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
deltaY: 100, // Zoom out
|
||||
clientX: 200,
|
||||
clientY: 150
|
||||
})
|
||||
wheelEvent.preventDefault = vi.fn()
|
||||
|
||||
interaction.handleWheel(wheelEvent)
|
||||
|
||||
// Should not go below minimum scale
|
||||
expect(mockCanvas.ds.scale).toBe(0.11)
|
||||
expect(centerViewOnMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle null container gracefully', () => {
|
||||
const containerRef = ref<HTMLDivElement | undefined>(undefined)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
// Should not throw
|
||||
expect(() => interaction.updateContainerRect()).not.toThrow()
|
||||
expect(() =>
|
||||
interaction.handlePointerDown(new PointerEvent('pointerdown'))
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle null canvas gracefully', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(null as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
// Should not throw
|
||||
expect(() =>
|
||||
interaction.handlePointerMove(new PointerEvent('pointermove'))
|
||||
).not.toThrow()
|
||||
expect(() => interaction.handleWheel(new WheelEvent('wheel'))).not.toThrow()
|
||||
expect(centerViewOnMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,267 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useMinimapRenderer } from '@/renderer/extensions/minimap/composables/useMinimapRenderer'
|
||||
import { renderMinimapToCanvas } from '@/renderer/extensions/minimap/minimapCanvasRenderer'
|
||||
import type { UpdateFlags } from '@/renderer/extensions/minimap/types'
|
||||
|
||||
vi.mock('@/renderer/extensions/minimap/minimapCanvasRenderer', () => ({
|
||||
renderMinimapToCanvas: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useMinimapRenderer', () => {
|
||||
let mockCanvas: HTMLCanvasElement
|
||||
let mockContext: CanvasRenderingContext2D
|
||||
let mockGraph: LGraph
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockContext = {
|
||||
clearRect: vi.fn()
|
||||
} as any
|
||||
|
||||
mockCanvas = {
|
||||
getContext: vi.fn().mockReturnValue(mockContext)
|
||||
} as any
|
||||
|
||||
mockGraph = {
|
||||
_nodes: [{ id: '1', pos: [0, 0], size: [100, 100] }]
|
||||
} as any
|
||||
})
|
||||
|
||||
it('should initialize with full redraw needed', () => {
|
||||
const canvasRef = ref(mockCanvas)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
const scaleRef = ref(1)
|
||||
const updateFlagsRef = ref<UpdateFlags>({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
const settings = {
|
||||
nodeColors: ref(true),
|
||||
showLinks: ref(true),
|
||||
showGroups: ref(true),
|
||||
renderBypass: ref(false),
|
||||
renderError: ref(false)
|
||||
}
|
||||
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graphRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
updateFlagsRef,
|
||||
settings,
|
||||
250,
|
||||
200
|
||||
)
|
||||
|
||||
expect(renderer.needsFullRedraw.value).toBe(true)
|
||||
expect(renderer.needsBoundsUpdate.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle empty graph with fast path', () => {
|
||||
const emptyGraph = { _nodes: [] } as any
|
||||
const canvasRef = ref(mockCanvas)
|
||||
const graphRef = ref(emptyGraph)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
const scaleRef = ref(1)
|
||||
const updateFlagsRef = ref<UpdateFlags>({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
const settings = {
|
||||
nodeColors: ref(true),
|
||||
showLinks: ref(true),
|
||||
showGroups: ref(true),
|
||||
renderBypass: ref(false),
|
||||
renderError: ref(false)
|
||||
}
|
||||
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graphRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
updateFlagsRef,
|
||||
settings,
|
||||
250,
|
||||
200
|
||||
)
|
||||
|
||||
renderer.renderMinimap()
|
||||
|
||||
expect(mockContext.clearRect).toHaveBeenCalledWith(0, 0, 250, 200)
|
||||
expect(vi.mocked(renderMinimapToCanvas)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should only render when redraw is needed', async () => {
|
||||
const { renderMinimapToCanvas } = await import(
|
||||
'@/renderer/extensions/minimap/minimapCanvasRenderer'
|
||||
)
|
||||
const canvasRef = ref(mockCanvas)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
const scaleRef = ref(1)
|
||||
const updateFlagsRef = ref<UpdateFlags>({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
const settings = {
|
||||
nodeColors: ref(true),
|
||||
showLinks: ref(true),
|
||||
showGroups: ref(true),
|
||||
renderBypass: ref(false),
|
||||
renderError: ref(false)
|
||||
}
|
||||
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graphRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
updateFlagsRef,
|
||||
settings,
|
||||
250,
|
||||
200
|
||||
)
|
||||
|
||||
// First render (needsFullRedraw is true by default)
|
||||
renderer.renderMinimap()
|
||||
expect(vi.mocked(renderMinimapToCanvas)).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Second render without changes (should not render)
|
||||
renderer.renderMinimap()
|
||||
expect(vi.mocked(renderMinimapToCanvas)).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Set update flag and render again
|
||||
updateFlagsRef.value.nodes = true
|
||||
renderer.renderMinimap()
|
||||
expect(vi.mocked(renderMinimapToCanvas)).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should update minimap with bounds and viewport callbacks', () => {
|
||||
const updateBounds = vi.fn()
|
||||
const updateViewport = vi.fn()
|
||||
|
||||
const canvasRef = ref(mockCanvas)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
const scaleRef = ref(1)
|
||||
const updateFlagsRef = ref<UpdateFlags>({
|
||||
bounds: true,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
const settings = {
|
||||
nodeColors: ref(true),
|
||||
showLinks: ref(true),
|
||||
showGroups: ref(true),
|
||||
renderBypass: ref(false),
|
||||
renderError: ref(false)
|
||||
}
|
||||
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graphRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
updateFlagsRef,
|
||||
settings,
|
||||
250,
|
||||
200
|
||||
)
|
||||
|
||||
renderer.updateMinimap(updateBounds, updateViewport)
|
||||
|
||||
expect(updateBounds).toHaveBeenCalled()
|
||||
expect(updateViewport).toHaveBeenCalled()
|
||||
expect(updateFlagsRef.value.bounds).toBe(false)
|
||||
expect(renderer.needsFullRedraw.value).toBe(false) // After rendering, needsFullRedraw is reset to false
|
||||
expect(updateFlagsRef.value.viewport).toBe(false) // After updating viewport, this is reset to false
|
||||
})
|
||||
|
||||
it('should force full redraw when requested', () => {
|
||||
const canvasRef = ref(mockCanvas)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
const scaleRef = ref(1)
|
||||
const updateFlagsRef = ref<UpdateFlags>({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
const settings = {
|
||||
nodeColors: ref(true),
|
||||
showLinks: ref(true),
|
||||
showGroups: ref(true),
|
||||
renderBypass: ref(false),
|
||||
renderError: ref(false)
|
||||
}
|
||||
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graphRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
updateFlagsRef,
|
||||
settings,
|
||||
250,
|
||||
200
|
||||
)
|
||||
|
||||
renderer.forceFullRedraw()
|
||||
|
||||
expect(renderer.needsFullRedraw.value).toBe(true)
|
||||
expect(updateFlagsRef.value.bounds).toBe(true)
|
||||
expect(updateFlagsRef.value.nodes).toBe(true)
|
||||
expect(updateFlagsRef.value.connections).toBe(true)
|
||||
expect(updateFlagsRef.value.viewport).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle null canvas gracefully', () => {
|
||||
const canvasRef = ref<HTMLCanvasElement | undefined>(undefined)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
const scaleRef = ref(1)
|
||||
const updateFlagsRef = ref<UpdateFlags>({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
const settings = {
|
||||
nodeColors: ref(true),
|
||||
showLinks: ref(true),
|
||||
showGroups: ref(true),
|
||||
renderBypass: ref(false),
|
||||
renderError: ref(false)
|
||||
}
|
||||
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graphRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
updateFlagsRef,
|
||||
settings,
|
||||
250,
|
||||
200
|
||||
)
|
||||
|
||||
// Should not throw
|
||||
expect(() => renderer.renderMinimap()).not.toThrow()
|
||||
expect(mockCanvas.getContext).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,122 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useMinimapSettings } from '@/renderer/extensions/minimap/composables/useMinimapSettings'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
vi.mock('@/stores/settingStore')
|
||||
vi.mock('@/stores/workspace/colorPaletteStore')
|
||||
|
||||
describe('useMinimapSettings', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return all minimap settings as computed refs', () => {
|
||||
const mockSettingStore = {
|
||||
get: vi.fn((key: string) => {
|
||||
const settings: Record<string, any> = {
|
||||
'Comfy.Minimap.NodeColors': true,
|
||||
'Comfy.Minimap.ShowLinks': false,
|
||||
'Comfy.Minimap.ShowGroups': true,
|
||||
'Comfy.Minimap.RenderBypassState': false,
|
||||
'Comfy.Minimap.RenderErrorState': true
|
||||
}
|
||||
return settings[key]
|
||||
})
|
||||
}
|
||||
|
||||
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore as any)
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue({
|
||||
completedActivePalette: { light_theme: false }
|
||||
} as any)
|
||||
|
||||
const settings = useMinimapSettings()
|
||||
|
||||
expect(settings.nodeColors.value).toBe(true)
|
||||
expect(settings.showLinks.value).toBe(false)
|
||||
expect(settings.showGroups.value).toBe(true)
|
||||
expect(settings.renderBypass.value).toBe(false)
|
||||
expect(settings.renderError.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should generate container styles based on theme', () => {
|
||||
const mockColorPaletteStore = {
|
||||
completedActivePalette: { light_theme: false }
|
||||
}
|
||||
|
||||
vi.mocked(useSettingStore).mockReturnValue({ get: vi.fn() } as any)
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue(
|
||||
mockColorPaletteStore as any
|
||||
)
|
||||
|
||||
const settings = useMinimapSettings()
|
||||
const styles = settings.containerStyles.value
|
||||
|
||||
expect(styles.width).toBe('250px')
|
||||
expect(styles.height).toBe('200px')
|
||||
expect(styles.backgroundColor).toBe('#15161C') // dark theme color
|
||||
expect(styles.border).toBe('1px solid #333')
|
||||
})
|
||||
|
||||
it('should generate light theme container styles', () => {
|
||||
const mockColorPaletteStore = {
|
||||
completedActivePalette: { light_theme: true }
|
||||
}
|
||||
|
||||
vi.mocked(useSettingStore).mockReturnValue({ get: vi.fn() } as any)
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue(
|
||||
mockColorPaletteStore as any
|
||||
)
|
||||
|
||||
const settings = useMinimapSettings()
|
||||
const styles = settings.containerStyles.value
|
||||
|
||||
expect(styles.backgroundColor).toBe('#FAF9F5') // light theme color
|
||||
expect(styles.border).toBe('1px solid #ccc')
|
||||
})
|
||||
|
||||
it('should generate panel styles based on theme', () => {
|
||||
const mockColorPaletteStore = {
|
||||
completedActivePalette: { light_theme: false }
|
||||
}
|
||||
|
||||
vi.mocked(useSettingStore).mockReturnValue({ get: vi.fn() } as any)
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue(
|
||||
mockColorPaletteStore as any
|
||||
)
|
||||
|
||||
const settings = useMinimapSettings()
|
||||
const styles = settings.panelStyles.value
|
||||
|
||||
expect(styles.backgroundColor).toBe('#15161C')
|
||||
expect(styles.border).toBe('1px solid #333')
|
||||
expect(styles.borderRadius).toBe('8px')
|
||||
})
|
||||
|
||||
it('should create computed properties that call the store getter', () => {
|
||||
const mockGet = vi.fn((key: string) => {
|
||||
if (key === 'Comfy.Minimap.NodeColors') return true
|
||||
if (key === 'Comfy.Minimap.ShowLinks') return false
|
||||
return true
|
||||
})
|
||||
const mockSettingStore = { get: mockGet }
|
||||
|
||||
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore as any)
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue({
|
||||
completedActivePalette: { light_theme: false }
|
||||
} as any)
|
||||
|
||||
const settings = useMinimapSettings()
|
||||
|
||||
// Access the computed properties
|
||||
expect(settings.nodeColors.value).toBe(true)
|
||||
expect(settings.showLinks.value).toBe(false)
|
||||
|
||||
// Verify the store getter was called with the correct keys
|
||||
expect(mockGet).toHaveBeenCalledWith('Comfy.Minimap.NodeColors')
|
||||
expect(mockGet).toHaveBeenCalledWith('Comfy.Minimap.ShowLinks')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,289 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useMinimapViewport } from '@/renderer/extensions/minimap/composables/useMinimapViewport'
|
||||
import type { MinimapCanvas } from '@/renderer/extensions/minimap/types'
|
||||
|
||||
vi.mock('@/composables/canvas/useCanvasTransformSync')
|
||||
vi.mock('@/renderer/core/spatial/boundsCalculator', () => ({
|
||||
calculateNodeBounds: vi.fn(),
|
||||
calculateMinimapScale: vi.fn(),
|
||||
enforceMinimumBounds: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useMinimapViewport', () => {
|
||||
let mockCanvas: MinimapCanvas
|
||||
let mockGraph: LGraph
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockCanvas = {
|
||||
canvas: {
|
||||
clientWidth: 800,
|
||||
clientHeight: 600,
|
||||
width: 1600,
|
||||
height: 1200
|
||||
} as HTMLCanvasElement,
|
||||
ds: {
|
||||
scale: 1,
|
||||
offset: [0, 0]
|
||||
},
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
|
||||
mockGraph = {
|
||||
_nodes: [
|
||||
{ pos: [100, 100], size: [150, 80] },
|
||||
{ pos: [300, 200], size: [120, 60] }
|
||||
]
|
||||
} as any
|
||||
|
||||
vi.mocked(useCanvasTransformSync).mockReturnValue({
|
||||
startSync: vi.fn(),
|
||||
stopSync: vi.fn()
|
||||
} as any)
|
||||
})
|
||||
|
||||
it('should initialize with default bounds', () => {
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
expect(viewport.bounds.value).toEqual({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 0,
|
||||
maxY: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
})
|
||||
|
||||
expect(viewport.scale.value).toBe(1)
|
||||
})
|
||||
|
||||
it('should calculate graph bounds from nodes', async () => {
|
||||
const { calculateNodeBounds, enforceMinimumBounds } = await import(
|
||||
'@/renderer/core/spatial/boundsCalculator'
|
||||
)
|
||||
|
||||
vi.mocked(calculateNodeBounds).mockReturnValue({
|
||||
minX: 100,
|
||||
minY: 100,
|
||||
maxX: 420,
|
||||
maxY: 260,
|
||||
width: 320,
|
||||
height: 160
|
||||
})
|
||||
|
||||
vi.mocked(enforceMinimumBounds).mockImplementation((bounds) => bounds)
|
||||
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.updateBounds()
|
||||
|
||||
expect(calculateNodeBounds).toHaveBeenCalledWith(mockGraph._nodes)
|
||||
expect(enforceMinimumBounds).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty graph', async () => {
|
||||
const { calculateNodeBounds } = await import(
|
||||
'@/renderer/core/spatial/boundsCalculator'
|
||||
)
|
||||
|
||||
vi.mocked(calculateNodeBounds).mockReturnValue(null)
|
||||
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref({ _nodes: [] } as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.updateBounds()
|
||||
|
||||
expect(viewport.bounds.value).toEqual({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 100,
|
||||
maxY: 100,
|
||||
width: 100,
|
||||
height: 100
|
||||
})
|
||||
})
|
||||
|
||||
it('should update canvas dimensions', () => {
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.updateCanvasDimensions()
|
||||
|
||||
expect(viewport.canvasDimensions.value).toEqual({
|
||||
width: 800,
|
||||
height: 600
|
||||
})
|
||||
})
|
||||
|
||||
it('should calculate viewport transform', async () => {
|
||||
const { calculateNodeBounds, enforceMinimumBounds, calculateMinimapScale } =
|
||||
await import('@/renderer/core/spatial/boundsCalculator')
|
||||
|
||||
// Mock the bounds calculation
|
||||
vi.mocked(calculateNodeBounds).mockReturnValue({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 500,
|
||||
maxY: 400,
|
||||
width: 500,
|
||||
height: 400
|
||||
})
|
||||
|
||||
vi.mocked(enforceMinimumBounds).mockImplementation((bounds) => bounds)
|
||||
vi.mocked(calculateMinimapScale).mockReturnValue(0.5)
|
||||
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
// Set canvas transform
|
||||
mockCanvas.ds.scale = 2
|
||||
mockCanvas.ds.offset = [-100, -50]
|
||||
|
||||
// Update bounds and viewport
|
||||
viewport.updateBounds()
|
||||
viewport.updateCanvasDimensions()
|
||||
viewport.updateViewport()
|
||||
|
||||
const transform = viewport.viewportTransform.value
|
||||
|
||||
// World coordinates
|
||||
const worldX = -(-100) // -offset[0] = 100
|
||||
const worldY = -(-50) // -offset[1] = 50
|
||||
|
||||
// Viewport size in world coordinates
|
||||
const viewportWidth = 800 / 2 // canvasWidth / scale = 400
|
||||
const viewportHeight = 600 / 2 // canvasHeight / scale = 300
|
||||
|
||||
// Center offsets
|
||||
const centerOffsetX = (250 - 500 * 0.5) / 2 // (250 - 250) / 2 = 0
|
||||
const centerOffsetY = (200 - 400 * 0.5) / 2 // (200 - 200) / 2 = 0
|
||||
|
||||
// Expected values based on implementation: (worldX - bounds.minX) * scale + centerOffsetX
|
||||
expect(transform.x).toBeCloseTo((worldX - 0) * 0.5 + centerOffsetX) // (100 - 0) * 0.5 + 0 = 50
|
||||
expect(transform.y).toBeCloseTo((worldY - 0) * 0.5 + centerOffsetY) // (50 - 0) * 0.5 + 0 = 25
|
||||
expect(transform.width).toBeCloseTo(viewportWidth * 0.5) // 400 * 0.5 = 200
|
||||
expect(transform.height).toBeCloseTo(viewportHeight * 0.5) // 300 * 0.5 = 150
|
||||
})
|
||||
|
||||
it('should center view on world coordinates', () => {
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.updateCanvasDimensions()
|
||||
mockCanvas.ds.scale = 2
|
||||
|
||||
viewport.centerViewOn(300, 200)
|
||||
|
||||
// Should update canvas offset to center on the given world coordinates
|
||||
const expectedOffsetX = -(300 - 800 / 2 / 2) // -(worldX - viewportWidth/2)
|
||||
const expectedOffsetY = -(200 - 600 / 2 / 2) // -(worldY - viewportHeight/2)
|
||||
|
||||
expect(mockCanvas.ds.offset[0]).toBe(expectedOffsetX)
|
||||
expect(mockCanvas.ds.offset[1]).toBe(expectedOffsetY)
|
||||
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('should start and stop viewport sync', () => {
|
||||
const startSyncMock = vi.fn()
|
||||
const stopSyncMock = vi.fn()
|
||||
|
||||
vi.mocked(useCanvasTransformSync).mockReturnValue({
|
||||
startSync: startSyncMock,
|
||||
stopSync: stopSyncMock
|
||||
} as any)
|
||||
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.startViewportSync()
|
||||
expect(startSyncMock).toHaveBeenCalled()
|
||||
|
||||
viewport.stopViewportSync()
|
||||
expect(stopSyncMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle null canvas gracefully', () => {
|
||||
const canvasRef = ref(null as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
// Should not throw
|
||||
expect(() => viewport.updateCanvasDimensions()).not.toThrow()
|
||||
expect(() => viewport.updateViewport()).not.toThrow()
|
||||
expect(() => viewport.centerViewOn(100, 100)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should calculate scale correctly', async () => {
|
||||
const { calculateMinimapScale, calculateNodeBounds, enforceMinimumBounds } =
|
||||
await import('@/renderer/core/spatial/boundsCalculator')
|
||||
|
||||
const testBounds = {
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 500,
|
||||
maxY: 400,
|
||||
width: 500,
|
||||
height: 400
|
||||
}
|
||||
|
||||
vi.mocked(calculateNodeBounds).mockReturnValue(testBounds)
|
||||
vi.mocked(enforceMinimumBounds).mockImplementation((bounds) => bounds)
|
||||
vi.mocked(calculateMinimapScale).mockReturnValue(0.4)
|
||||
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.updateBounds()
|
||||
|
||||
expect(calculateMinimapScale).toHaveBeenCalledWith(testBounds, 250, 200)
|
||||
expect(viewport.scale.value).toBe(0.4)
|
||||
})
|
||||
|
||||
it('should handle device pixel ratio', () => {
|
||||
const originalDPR = window.devicePixelRatio
|
||||
Object.defineProperty(window, 'devicePixelRatio', {
|
||||
value: 2,
|
||||
configurable: true
|
||||
})
|
||||
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.updateCanvasDimensions()
|
||||
|
||||
// Should use client dimensions or calculate from canvas dimensions / dpr
|
||||
expect(viewport.canvasDimensions.value.width).toBe(800)
|
||||
expect(viewport.canvasDimensions.value.height).toBe(600)
|
||||
|
||||
Object.defineProperty(window, 'devicePixelRatio', {
|
||||
value: originalDPR,
|
||||
configurable: true
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,324 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { renderMinimapToCanvas } from '@/renderer/extensions/minimap/minimapCanvasRenderer'
|
||||
import type { MinimapRenderContext } from '@/renderer/extensions/minimap/types'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
|
||||
vi.mock('@/stores/workspace/colorPaletteStore')
|
||||
vi.mock('@/utils/colorUtil', () => ({
|
||||
adjustColor: vi.fn((color: string) => color + '_adjusted')
|
||||
}))
|
||||
|
||||
describe('minimapCanvasRenderer', () => {
|
||||
let mockCanvas: HTMLCanvasElement
|
||||
let mockContext: CanvasRenderingContext2D
|
||||
let mockGraph: LGraph
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockContext = {
|
||||
clearRect: vi.fn(),
|
||||
fillRect: vi.fn(),
|
||||
strokeRect: vi.fn(),
|
||||
beginPath: vi.fn(),
|
||||
moveTo: vi.fn(),
|
||||
lineTo: vi.fn(),
|
||||
stroke: vi.fn(),
|
||||
arc: vi.fn(),
|
||||
fill: vi.fn(),
|
||||
fillStyle: '',
|
||||
strokeStyle: '',
|
||||
lineWidth: 1
|
||||
} as any
|
||||
|
||||
mockCanvas = {
|
||||
getContext: vi.fn().mockReturnValue(mockContext)
|
||||
} as any
|
||||
|
||||
mockGraph = {
|
||||
_nodes: [
|
||||
{
|
||||
id: '1',
|
||||
pos: [100, 100],
|
||||
size: [150, 80],
|
||||
bgcolor: '#FF0000',
|
||||
mode: LGraphEventMode.ALWAYS,
|
||||
has_errors: false,
|
||||
outputs: []
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
pos: [300, 200],
|
||||
size: [120, 60],
|
||||
bgcolor: '#00FF00',
|
||||
mode: LGraphEventMode.BYPASS,
|
||||
has_errors: true,
|
||||
outputs: []
|
||||
}
|
||||
] as unknown as LGraphNode[],
|
||||
_groups: [],
|
||||
links: {},
|
||||
getNodeById: vi.fn()
|
||||
} as any
|
||||
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue({
|
||||
completedActivePalette: { light_theme: false }
|
||||
} as any)
|
||||
})
|
||||
|
||||
it('should clear canvas and render nodes', () => {
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: true,
|
||||
renderError: true
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Should clear the canvas first
|
||||
expect(mockContext.clearRect).toHaveBeenCalledWith(0, 0, 250, 200)
|
||||
|
||||
// Should render nodes (batch by color)
|
||||
expect(mockContext.fillRect).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty graph', () => {
|
||||
mockGraph._nodes = []
|
||||
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
expect(mockContext.clearRect).toHaveBeenCalledWith(0, 0, 250, 200)
|
||||
expect(mockContext.fillRect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should batch render nodes by color', () => {
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Should set fill style for each color group
|
||||
const fillStyleCalls = []
|
||||
let currentStyle = ''
|
||||
|
||||
mockContext.fillStyle = ''
|
||||
Object.defineProperty(mockContext, 'fillStyle', {
|
||||
get: () => currentStyle,
|
||||
set: (value) => {
|
||||
currentStyle = value
|
||||
fillStyleCalls.push(value)
|
||||
}
|
||||
})
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Different colors for different nodes
|
||||
expect(fillStyleCalls.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should render bypass nodes with special color', () => {
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: true,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Node 2 is in bypass mode, should be rendered
|
||||
expect(mockContext.fillRect).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render error outlines when enabled', () => {
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: false,
|
||||
renderError: true
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Should set stroke style for errors
|
||||
expect(mockContext.strokeStyle).toBe('#FF0000')
|
||||
expect(mockContext.strokeRect).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render groups when enabled', () => {
|
||||
mockGraph._groups = [
|
||||
{
|
||||
pos: [50, 50],
|
||||
size: [400, 300],
|
||||
color: '#0000FF'
|
||||
}
|
||||
] as any
|
||||
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: true,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Groups should be rendered before nodes
|
||||
expect(mockContext.fillRect).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render connections when enabled', () => {
|
||||
const targetNode = {
|
||||
id: '2',
|
||||
pos: [300, 200],
|
||||
size: [120, 60]
|
||||
}
|
||||
|
||||
mockGraph._nodes[0].outputs = [
|
||||
{
|
||||
links: [1]
|
||||
}
|
||||
] as any
|
||||
|
||||
// Create a hybrid Map/Object for links as LiteGraph expects
|
||||
const linksMap = new Map([[1, { id: 1, target_id: 2 }]])
|
||||
const links = Object.assign(linksMap, {
|
||||
1: { id: 1, target_id: 2 }
|
||||
})
|
||||
mockGraph.links = links as any
|
||||
|
||||
mockGraph.getNodeById = vi.fn().mockReturnValue(targetNode)
|
||||
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: false,
|
||||
showLinks: true,
|
||||
showGroups: false,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Should draw connection lines
|
||||
expect(mockContext.beginPath).toHaveBeenCalled()
|
||||
expect(mockContext.moveTo).toHaveBeenCalled()
|
||||
expect(mockContext.lineTo).toHaveBeenCalled()
|
||||
expect(mockContext.stroke).toHaveBeenCalled()
|
||||
|
||||
// Should draw connection slots
|
||||
expect(mockContext.arc).toHaveBeenCalled()
|
||||
expect(mockContext.fill).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle light theme colors', () => {
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue({
|
||||
completedActivePalette: { light_theme: true }
|
||||
} as any)
|
||||
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Color adjustment should be called for light theme
|
||||
expect(adjustColor).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should calculate correct offsets for centering', () => {
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 200, height: 100 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: false,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// With bounds 200x100 at scale 0.5 = 100x50
|
||||
// Canvas is 250x200, so offset should be (250-100)/2 = 75, (200-50)/2 = 75
|
||||
// This affects node positioning
|
||||
expect(mockContext.fillRect).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -3,8 +3,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
vi.mock('@/composables/useMinimap', () => ({
|
||||
useMinimap: vi.fn()
|
||||
vi.mock('@/renderer/thumbnail/graphThumbnailRenderer', () => ({
|
||||
createGraphThumbnail: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
@@ -19,13 +19,14 @@ vi.mock('@/scripts/api', () => ({
|
||||
}))
|
||||
|
||||
const { useWorkflowThumbnail } = await import(
|
||||
'@/composables/useWorkflowThumbnail'
|
||||
'@/renderer/thumbnail/composables/useWorkflowThumbnail'
|
||||
)
|
||||
const { createGraphThumbnail } = await import(
|
||||
'@/renderer/thumbnail/graphThumbnailRenderer'
|
||||
)
|
||||
const { useMinimap } = await import('@/composables/useMinimap')
|
||||
const { api } = await import('@/scripts/api')
|
||||
|
||||
describe('useWorkflowThumbnail', () => {
|
||||
let mockMinimapInstance: any
|
||||
let workflowStore: ReturnType<typeof useWorkflowStore>
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -39,35 +40,23 @@ describe('useWorkflowThumbnail', () => {
|
||||
// Now set up mocks
|
||||
vi.clearAllMocks()
|
||||
|
||||
const blob = new Blob()
|
||||
|
||||
global.URL.createObjectURL = vi.fn(() => 'data:image/png;base64,test')
|
||||
global.URL.revokeObjectURL = vi.fn()
|
||||
|
||||
// Mock API responses
|
||||
vi.mocked(api.moveUserData).mockResolvedValue({ status: 200 } as Response)
|
||||
|
||||
mockMinimapInstance = {
|
||||
renderMinimap: vi.fn(),
|
||||
canvasRef: {
|
||||
value: {
|
||||
toBlob: vi.fn((cb) => cb(blob))
|
||||
}
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
vi.mocked(useMinimap).mockReturnValue(mockMinimapInstance)
|
||||
// Default createGraphThumbnail to return test value
|
||||
vi.mocked(createGraphThumbnail).mockReturnValue(
|
||||
'data:image/png;base64,test'
|
||||
)
|
||||
})
|
||||
|
||||
it('should capture minimap thumbnail', async () => {
|
||||
const { createMinimapPreview } = useWorkflowThumbnail()
|
||||
const thumbnail = await createMinimapPreview()
|
||||
|
||||
expect(useMinimap).toHaveBeenCalledOnce()
|
||||
expect(mockMinimapInstance.renderMinimap).toHaveBeenCalledOnce()
|
||||
|
||||
expect(createGraphThumbnail).toHaveBeenCalledOnce()
|
||||
expect(thumbnail).toBe('data:image/png;base64,test')
|
||||
})
|
||||
|
||||
@@ -161,6 +150,9 @@ describe('useWorkflowThumbnail', () => {
|
||||
// Reset the mock to track new calls and create different URL
|
||||
vi.clearAllMocks()
|
||||
global.URL.createObjectURL = vi.fn(() => 'data:image/png;base64,test2')
|
||||
vi.mocked(createGraphThumbnail).mockReturnValue(
|
||||
'data:image/png;base64,test2'
|
||||
)
|
||||
|
||||
// Store second thumbnail for same workflow - should revoke the first URL
|
||||
await storeThumbnail(mockWorkflow)
|
||||
@@ -6,8 +6,6 @@ import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
InstalledPacksResponse,
|
||||
ManagerChannel,
|
||||
ManagerDatabaseSource,
|
||||
ManagerPackInstalled
|
||||
} from '@/types/comfyManagerTypes'
|
||||
|
||||
@@ -15,34 +13,6 @@ vi.mock('@/services/comfyManagerService', () => ({
|
||||
useComfyManagerService: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: () => ({
|
||||
showManagerProgressDialog: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useManagerQueue', () => {
|
||||
const enqueueTaskMock = vi.fn()
|
||||
|
||||
return {
|
||||
useManagerQueue: () => ({
|
||||
statusMessage: ref(''),
|
||||
allTasksDone: ref(false),
|
||||
enqueueTask: enqueueTaskMock,
|
||||
uncompletedCount: ref(0)
|
||||
}),
|
||||
enqueueTask: enqueueTaskMock
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/composables/useServerLogs', () => ({
|
||||
useServerLogs: () => ({
|
||||
startListening: vi.fn(),
|
||||
stopListening: vi.fn(),
|
||||
logs: ref([])
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: vi.fn((key) => key)
|
||||
@@ -63,7 +33,11 @@ interface EnabledDisabledTestCase {
|
||||
}
|
||||
|
||||
describe('useComfyManagerStore', () => {
|
||||
let mockManagerService: ReturnType<typeof useComfyManagerService>
|
||||
let mockManagerService: {
|
||||
isLoading: ReturnType<typeof ref<boolean>>
|
||||
error: ReturnType<typeof ref<string | null>>
|
||||
listInstalledPacks: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
const triggerPacksChange = async (
|
||||
installedPacks: InstalledPacksResponse,
|
||||
@@ -81,21 +55,10 @@ describe('useComfyManagerStore', () => {
|
||||
mockManagerService = {
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startQueue: vi.fn().mockResolvedValue(null),
|
||||
resetQueue: vi.fn().mockResolvedValue(null),
|
||||
getQueueStatus: vi.fn().mockResolvedValue(null),
|
||||
listInstalledPacks: vi.fn().mockResolvedValue({}),
|
||||
getImportFailInfo: vi.fn().mockResolvedValue(null),
|
||||
installPack: vi.fn().mockResolvedValue(null),
|
||||
uninstallPack: vi.fn().mockResolvedValue(null),
|
||||
enablePack: vi.fn().mockResolvedValue(null),
|
||||
disablePack: vi.fn().mockResolvedValue(null),
|
||||
updatePack: vi.fn().mockResolvedValue(null),
|
||||
updateAllPacks: vi.fn().mockResolvedValue(null),
|
||||
rebootComfyUI: vi.fn().mockResolvedValue(null),
|
||||
isLegacyManagerUI: vi.fn().mockResolvedValue(false)
|
||||
listInstalledPacks: vi.fn().mockResolvedValue({})
|
||||
}
|
||||
|
||||
// @ts-expect-error Mocking the return type of useComfyManagerService
|
||||
vi.mocked(useComfyManagerService).mockReturnValue(mockManagerService)
|
||||
})
|
||||
|
||||
@@ -350,90 +313,4 @@ describe('useComfyManagerStore', () => {
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('isPackInstalling', () => {
|
||||
it('should return false for packs not being installed', () => {
|
||||
const store = useComfyManagerStore()
|
||||
expect(store.isPackInstalling('test-pack')).toBe(false)
|
||||
expect(store.isPackInstalling(undefined)).toBe(false)
|
||||
expect(store.isPackInstalling('')).toBe(false)
|
||||
})
|
||||
|
||||
it('should track pack as installing when installPack is called', async () => {
|
||||
const store = useComfyManagerStore()
|
||||
|
||||
// Call installPack
|
||||
await store.installPack.call({
|
||||
id: 'test-pack',
|
||||
repository: 'https://github.com/test/test-pack',
|
||||
channel: ManagerChannel.DEV,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
selected_version: 'latest',
|
||||
version: 'latest'
|
||||
})
|
||||
|
||||
// Check that the pack is marked as installing
|
||||
expect(store.isPackInstalling('test-pack')).toBe(true)
|
||||
})
|
||||
|
||||
it('should remove pack from installing list when explicitly removed', async () => {
|
||||
const store = useComfyManagerStore()
|
||||
|
||||
// Call installPack
|
||||
await store.installPack.call({
|
||||
id: 'test-pack',
|
||||
repository: 'https://github.com/test/test-pack',
|
||||
channel: ManagerChannel.DEV,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
selected_version: 'latest',
|
||||
version: 'latest'
|
||||
})
|
||||
|
||||
// Verify pack is installing
|
||||
expect(store.isPackInstalling('test-pack')).toBe(true)
|
||||
|
||||
// Call installPack again for another pack to demonstrate multiple installs
|
||||
await store.installPack.call({
|
||||
id: 'another-pack',
|
||||
repository: 'https://github.com/test/another-pack',
|
||||
channel: ManagerChannel.DEV,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
selected_version: 'latest',
|
||||
version: 'latest'
|
||||
})
|
||||
|
||||
// Both should be installing
|
||||
expect(store.isPackInstalling('test-pack')).toBe(true)
|
||||
expect(store.isPackInstalling('another-pack')).toBe(true)
|
||||
})
|
||||
|
||||
it('should track multiple packs installing independently', async () => {
|
||||
const store = useComfyManagerStore()
|
||||
|
||||
// Install pack 1
|
||||
await store.installPack.call({
|
||||
id: 'pack-1',
|
||||
repository: 'https://github.com/test/pack-1',
|
||||
channel: ManagerChannel.DEV,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
selected_version: 'latest',
|
||||
version: 'latest'
|
||||
})
|
||||
|
||||
// Install pack 2
|
||||
await store.installPack.call({
|
||||
id: 'pack-2',
|
||||
repository: 'https://github.com/test/pack-2',
|
||||
channel: ManagerChannel.DEV,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
selected_version: 'latest',
|
||||
version: 'latest'
|
||||
})
|
||||
|
||||
// Both should be installing
|
||||
expect(store.isPackInstalling('pack-1')).toBe(true)
|
||||
expect(store.isPackInstalling('pack-2')).toBe(true)
|
||||
expect(store.isPackInstalling('pack-3')).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
199
tests-ui/tests/utils/executableGroupNodeChildDTO.test.ts
Normal file
199
tests-ui/tests/utils/executableGroupNodeChildDTO.test.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { GroupNodeHandler } from '@/extensions/core/groupNode'
|
||||
import type {
|
||||
ExecutableLGraphNode,
|
||||
ExecutionId,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { ExecutableGroupNodeChildDTO } from '@/utils/executableGroupNodeChildDTO'
|
||||
|
||||
describe('ExecutableGroupNodeChildDTO', () => {
|
||||
let mockNode: LGraphNode
|
||||
let mockInputNode: LGraphNode
|
||||
let mockNodesByExecutionId: Map<ExecutionId, ExecutableLGraphNode>
|
||||
let mockGroupNodeHandler: GroupNodeHandler
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock nodes
|
||||
mockNode = {
|
||||
id: '3', // Simple node ID for most tests
|
||||
graph: {},
|
||||
getInputNode: vi.fn(),
|
||||
getInputLink: vi.fn(),
|
||||
inputs: []
|
||||
} as any
|
||||
|
||||
mockInputNode = {
|
||||
id: '1',
|
||||
graph: {}
|
||||
} as any
|
||||
|
||||
// Create the nodesByExecutionId map
|
||||
mockNodesByExecutionId = new Map()
|
||||
|
||||
mockGroupNodeHandler = {} as GroupNodeHandler
|
||||
})
|
||||
|
||||
describe('resolveInput', () => {
|
||||
it('should resolve input from external node (node outside the group)', () => {
|
||||
// Setup: Group node child with ID '10:3'
|
||||
const groupNodeChild = {
|
||||
id: '10:3',
|
||||
graph: {},
|
||||
getInputNode: vi.fn().mockReturnValue(mockInputNode),
|
||||
getInputLink: vi.fn().mockReturnValue({
|
||||
origin_slot: 0
|
||||
}),
|
||||
inputs: []
|
||||
} as any
|
||||
|
||||
// External node with ID '1'
|
||||
const externalNodeDto = {
|
||||
id: '1',
|
||||
type: 'TestNode'
|
||||
} as ExecutableLGraphNode
|
||||
|
||||
mockNodesByExecutionId.set('1', externalNodeDto)
|
||||
|
||||
const dto = new ExecutableGroupNodeChildDTO(
|
||||
groupNodeChild,
|
||||
[], // No subgraph path - group is in root graph
|
||||
mockNodesByExecutionId,
|
||||
undefined,
|
||||
mockGroupNodeHandler
|
||||
)
|
||||
|
||||
const result = dto.resolveInput(0)
|
||||
|
||||
expect(result).toEqual({
|
||||
node: externalNodeDto,
|
||||
origin_id: '1',
|
||||
origin_slot: 0
|
||||
})
|
||||
})
|
||||
|
||||
it('should resolve input from internal node (node inside the same group)', () => {
|
||||
// Setup: Group node child with ID '10:3'
|
||||
const groupNodeChild = {
|
||||
id: '10:3',
|
||||
graph: {},
|
||||
getInputNode: vi.fn(),
|
||||
getInputLink: vi.fn(),
|
||||
inputs: []
|
||||
} as any
|
||||
|
||||
// Internal node with ID '10:2'
|
||||
const internalInputNode = {
|
||||
id: '10:2',
|
||||
graph: {}
|
||||
} as LGraphNode
|
||||
|
||||
const internalNodeDto = {
|
||||
id: '2',
|
||||
type: 'InternalNode'
|
||||
} as ExecutableLGraphNode
|
||||
|
||||
// Internal nodes are stored with just their index
|
||||
mockNodesByExecutionId.set('2', internalNodeDto)
|
||||
|
||||
groupNodeChild.getInputNode.mockReturnValue(internalInputNode)
|
||||
groupNodeChild.getInputLink.mockReturnValue({
|
||||
origin_slot: 1
|
||||
})
|
||||
|
||||
const dto = new ExecutableGroupNodeChildDTO(
|
||||
groupNodeChild,
|
||||
[],
|
||||
mockNodesByExecutionId,
|
||||
undefined,
|
||||
mockGroupNodeHandler
|
||||
)
|
||||
|
||||
const result = dto.resolveInput(0)
|
||||
|
||||
expect(result).toEqual({
|
||||
node: internalNodeDto,
|
||||
origin_id: '10:2',
|
||||
origin_slot: 1
|
||||
})
|
||||
})
|
||||
|
||||
it('should return undefined if no input node exists', () => {
|
||||
mockNode.getInputNode = vi.fn().mockReturnValue(null)
|
||||
|
||||
const dto = new ExecutableGroupNodeChildDTO(
|
||||
mockNode,
|
||||
[],
|
||||
mockNodesByExecutionId,
|
||||
undefined,
|
||||
mockGroupNodeHandler
|
||||
)
|
||||
|
||||
const result = dto.resolveInput(0)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should throw error if input link is missing', () => {
|
||||
mockNode.getInputNode = vi.fn().mockReturnValue(mockInputNode)
|
||||
mockNode.getInputLink = vi.fn().mockReturnValue(null)
|
||||
|
||||
const dto = new ExecutableGroupNodeChildDTO(
|
||||
mockNode,
|
||||
[],
|
||||
mockNodesByExecutionId,
|
||||
undefined,
|
||||
mockGroupNodeHandler
|
||||
)
|
||||
|
||||
expect(() => dto.resolveInput(0)).toThrow('Failed to get input link')
|
||||
})
|
||||
|
||||
it('should throw error if input node cannot be found in nodesByExecutionId', () => {
|
||||
// Node exists but is not in the map
|
||||
mockNode.getInputNode = vi.fn().mockReturnValue(mockInputNode)
|
||||
mockNode.getInputLink = vi.fn().mockReturnValue({
|
||||
origin_slot: 0
|
||||
})
|
||||
|
||||
const dto = new ExecutableGroupNodeChildDTO(
|
||||
mockNode,
|
||||
[],
|
||||
mockNodesByExecutionId, // Empty map
|
||||
undefined,
|
||||
mockGroupNodeHandler
|
||||
)
|
||||
|
||||
expect(() => dto.resolveInput(0)).toThrow(
|
||||
'Failed to get input node 1 for group node child 3 with slot 0'
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error for group nodes inside subgraphs (unsupported)', () => {
|
||||
// Setup: Group node child inside a subgraph (execution ID has more than 2 segments)
|
||||
const nestedGroupNode = {
|
||||
id: '1:2:3', // subgraph:groupnode:innernode
|
||||
graph: {},
|
||||
getInputNode: vi.fn().mockReturnValue(mockInputNode),
|
||||
getInputLink: vi.fn().mockReturnValue({
|
||||
origin_slot: 0
|
||||
}),
|
||||
inputs: []
|
||||
} as any
|
||||
|
||||
// Create DTO with deeply nested path to simulate group node inside subgraph
|
||||
const dto = new ExecutableGroupNodeChildDTO(
|
||||
nestedGroupNode,
|
||||
['1', '2'], // Path indicating it's inside a subgraph then group
|
||||
mockNodesByExecutionId,
|
||||
undefined,
|
||||
mockGroupNodeHandler
|
||||
)
|
||||
|
||||
expect(() => dto.resolveInput(0)).toThrow(
|
||||
'Group nodes inside subgraphs are not supported. Please convert the group node to a subgraph instead.'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user