Merge branch 'main' into qpo-progressbar-node-name

This commit is contained in:
Alexander Brown
2026-01-05 16:06:03 -08:00
committed by GitHub
41 changed files with 9327 additions and 6897 deletions

1
THIRD_PARTY_NOTICES.md Normal file
View File

@@ -0,0 +1 @@
AMD and the AMD Arrow logo are trademarks of Advanced Micro Devices, Inc.

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/desktop-ui",
"version": "0.0.4",
"version": "0.0.6",
"type": "module",
"nx": {
"tags": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,84 @@
// eslint-disable-next-line storybook/no-renderer-packages
import type { Meta, StoryObj } from '@storybook/vue3'
import type {
ElectronAPI,
TorchDeviceType
} from '@comfyorg/comfyui-electron-types'
import { ref } from 'vue'
import GpuPicker from './GpuPicker.vue'
type Platform = ReturnType<ElectronAPI['getPlatform']>
type ElectronAPIStub = Pick<ElectronAPI, 'getPlatform'>
type WindowWithElectron = Window & { electronAPI?: ElectronAPIStub }
const meta: Meta<typeof GpuPicker> = {
title: 'Desktop/Components/GpuPicker',
component: GpuPicker,
parameters: {
layout: 'padded',
backgrounds: {
default: 'dark',
values: [
{ name: 'dark', value: '#0a0a0a' },
{ name: 'neutral-900', value: '#171717' },
{ name: 'neutral-950', value: '#0a0a0a' }
]
}
}
}
export default meta
type Story = StoryObj<typeof meta>
function createElectronDecorator(platform: Platform) {
function getPlatform() {
return platform
}
return function ElectronDecorator() {
const windowWithElectron = window as WindowWithElectron
windowWithElectron.electronAPI = { getPlatform }
return { template: '<story />' }
}
}
function renderWithDevice(device: TorchDeviceType | null) {
return function Render() {
return {
components: { GpuPicker },
setup() {
const selected = ref<TorchDeviceType | null>(device)
return { selected }
},
template: `
<div class="min-h-screen bg-neutral-950 p-8">
<GpuPicker v-model:device="selected" />
</div>
`
}
}
}
const windowsDecorator = createElectronDecorator('win32')
const macDecorator = createElectronDecorator('darwin')
export const WindowsNvidiaSelected: Story = {
decorators: [windowsDecorator],
render: renderWithDevice('nvidia')
}
export const WindowsAmdSelected: Story = {
decorators: [windowsDecorator],
render: renderWithDevice('amd')
}
export const WindowsCpuSelected: Story = {
decorators: [windowsDecorator],
render: renderWithDevice('cpu')
}
export const MacMpsSelected: Story = {
decorators: [macDecorator],
render: renderWithDevice('mps')
}

View File

@@ -11,29 +11,32 @@
<!-- Apple Metal / NVIDIA -->
<HardwareOption
v-if="platform === 'darwin'"
:image-path="'./assets/images/apple-mps-logo.png'"
image-path="./assets/images/apple-mps-logo.png"
placeholder-text="Apple Metal"
subtitle="Apple Metal"
:value="'mps'"
:selected="selected === 'mps'"
:recommended="true"
@click="pickGpu('mps')"
/>
<HardwareOption
v-else
:image-path="'./assets/images/nvidia-logo-square.jpg'"
placeholder-text="NVIDIA"
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
:value="'nvidia'"
:selected="selected === 'nvidia'"
:recommended="true"
@click="pickGpu('nvidia')"
/>
<template v-else>
<HardwareOption
image-path="./assets/images/nvidia-logo-square.jpg"
placeholder-text="NVIDIA"
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
:selected="selected === 'nvidia'"
@click="pickGpu('nvidia')"
/>
<HardwareOption
image-path="./assets/images/amd-rocm-logo.png"
placeholder-text="AMD"
:subtitle="$t('install.gpuPicker.amdSubtitle')"
:selected="selected === 'amd'"
@click="pickGpu('amd')"
/>
</template>
<!-- CPU -->
<HardwareOption
placeholder-text="CPU"
:subtitle="$t('install.gpuPicker.cpuSubtitle')"
:value="'cpu'"
:selected="selected === 'cpu'"
@click="pickGpu('cpu')"
/>
@@ -41,7 +44,6 @@
<HardwareOption
placeholder-text="Manual Install"
:subtitle="$t('install.gpuPicker.manualSubtitle')"
:value="'unsupported'"
:selected="selected === 'unsupported'"
@click="pickGpu('unsupported')"
/>
@@ -81,13 +83,15 @@ const selected = defineModel<TorchDeviceType | null>('device', {
const electron = electronAPI()
const platform = electron.getPlatform()
const showRecommendedBadge = computed(
() => selected.value === 'mps' || selected.value === 'nvidia'
const recommendedDevices: TorchDeviceType[] = ['mps', 'nvidia', 'amd']
const showRecommendedBadge = computed(() =>
selected.value ? recommendedDevices.includes(selected.value) : false
)
const descriptionKeys = {
mps: 'appleMetal',
nvidia: 'nvidia',
amd: 'amd',
cpu: 'cpu',
unsupported: 'manual'
} as const
@@ -97,7 +101,7 @@ const descriptionText = computed(() => {
return st(`install.gpuPicker.${key}Description`, '')
})
const pickGpu = (value: TorchDeviceType) => {
function pickGpu(value: TorchDeviceType) {
selected.value = value
}
</script>

View File

@@ -29,7 +29,6 @@ export const AppleMetalSelected: Story = {
imagePath: '/assets/images/apple-mps-logo.png',
placeholderText: 'Apple Metal',
subtitle: 'Apple Metal',
value: 'mps',
selected: true
}
}
@@ -39,7 +38,6 @@ export const AppleMetalUnselected: Story = {
imagePath: '/assets/images/apple-mps-logo.png',
placeholderText: 'Apple Metal',
subtitle: 'Apple Metal',
value: 'mps',
selected: false
}
}
@@ -48,7 +46,6 @@ export const CPUOption: Story = {
args: {
placeholderText: 'CPU',
subtitle: 'Subtitle',
value: 'cpu',
selected: false
}
}
@@ -57,7 +54,6 @@ export const ManualInstall: Story = {
args: {
placeholderText: 'Manual Install',
subtitle: 'Subtitle',
value: 'unsupported',
selected: false
}
}
@@ -67,7 +63,6 @@ export const NvidiaSelected: Story = {
imagePath: '/assets/images/nvidia-logo-square.jpg',
placeholderText: 'NVIDIA',
subtitle: 'NVIDIA',
value: 'nvidia',
selected: true
}
}

View File

@@ -36,17 +36,13 @@
</template>
<script setup lang="ts">
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
import { cn } from '@/utils/tailwindUtil'
interface Props {
imagePath?: string
placeholderText: string
subtitle?: string
value: TorchDeviceType
selected?: boolean
recommended?: boolean
}
defineProps<Props>()

View File

@@ -104,8 +104,8 @@
</template>
<script setup lang="ts">
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
import { TorchMirrorUrl } from '@comfyorg/comfyui-electron-types'
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
import { isInChina } from '@comfyorg/shared-frontend-utils/networkUtil'
import Accordion from 'primevue/accordion'
import AccordionContent from 'primevue/accordioncontent'
@@ -155,7 +155,7 @@ const activeAccordionIndex = ref<string[] | undefined>(undefined)
const electron = electronAPI()
// Mirror configuration logic
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
function getTorchMirrorItem(device: TorchDeviceType): UVMirror {
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
switch (device) {
case 'mps':
@@ -170,6 +170,7 @@ const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
mirror: TorchMirrorUrl.Cuda,
fallbackMirror: TorchMirrorUrl.Cuda
}
case 'amd':
case 'cpu':
default:
return {

View File

@@ -63,7 +63,6 @@ const taskStore = useMaintenanceTaskStore()
defineProps<{
displayAsList: string
filter: MaintenanceFilter
isRefreshing: boolean
}>()
const executeTask = async (task: MaintenanceTask) => {

View File

@@ -143,6 +143,8 @@ const goToPreviousStep = () => {
const electron = electronAPI()
const router = useRouter()
const install = async () => {
if (!device.value) return
const options: InstallOptions = {
installPath: installPath.value,
autoUpdate: autoUpdate.value,
@@ -152,7 +154,6 @@ const install = async () => {
pythonMirror: pythonMirror.value,
pypiMirror: pypiMirror.value,
torchMirror: torchMirror.value,
// @ts-expect-error fixme ts strict error
device: device.value
}
electron.installComfyUI(options)
@@ -166,7 +167,11 @@ onMounted(async () => {
if (!electron) return
const detectedGpu = await electron.Config.getDetectedGpu()
if (detectedGpu === 'mps' || detectedGpu === 'nvidia') {
if (
detectedGpu === 'mps' ||
detectedGpu === 'nvidia' ||
detectedGpu === 'amd'
) {
device.value = detectedGpu
}

View File

@@ -74,7 +74,6 @@
class="border-neutral-700 border-solid border-x-0 border-y"
:filter
:display-as-list
:is-refreshing
/>
<!-- Actions -->

View File

@@ -19,6 +19,7 @@ test.describe('Graph', () => {
})
test('Validate workflow links', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Validation.Workflows', true)
await comfyPage.loadWorkflow('links/bad_link')
await expect(comfyPage.getVisibleToastCount()).resolves.toBe(2)
})

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.37.2",
"version": "1.37.3",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -42,6 +42,7 @@
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
"preview": "nx preview",
"storybook": "nx storybook",
"storybook:desktop": "nx run @comfyorg/desktop-ui:storybook",
"stylelint:fix": "stylelint --cache --fix '{apps,packages,src}/**/*.{css,vue}'",
"stylelint": "stylelint --cache '{apps,packages,src}/**/*.{css,vue}'",
"test:browser": "pnpm exec nx e2e",
@@ -186,6 +187,7 @@
"vue-i18n": "catalog:",
"vue-router": "catalog:",
"vuefire": "catalog:",
"wwobjloader2": "catalog:",
"yjs": "catalog:",
"zod": "catalog:",
"zod-validation-error": "catalog:"

46
pnpm-lock.yaml generated
View File

@@ -10,8 +10,8 @@ catalogs:
specifier: ^5.2.0
version: 5.2.0
'@comfyorg/comfyui-electron-types':
specifier: 0.5.5
version: 0.5.5
specifier: 0.6.2
version: 0.6.2
'@eslint/js':
specifier: ^9.39.1
version: 9.39.1
@@ -309,6 +309,9 @@ catalogs:
vuefire:
specifier: ^3.2.1
version: 3.2.1
wwobjloader2:
specifier: ^6.2.1
version: 6.2.1
yjs:
specifier: ^13.6.27
version: 13.6.27
@@ -337,7 +340,7 @@ importers:
version: 1.3.1
'@comfyorg/comfyui-electron-types':
specifier: 'catalog:'
version: 0.5.5
version: 0.6.2
'@comfyorg/design-system':
specifier: workspace:*
version: link:packages/design-system
@@ -497,6 +500,9 @@ importers:
vuefire:
specifier: 'catalog:'
version: 3.2.1(consola@3.4.2)(firebase@11.6.0)(vue@3.5.13(typescript@5.9.3))
wwobjloader2:
specifier: 'catalog:'
version: 6.2.1(three@0.170.0)
yjs:
specifier: 'catalog:'
version: 13.6.27
@@ -737,7 +743,7 @@ importers:
dependencies:
'@comfyorg/comfyui-electron-types':
specifier: 'catalog:'
version: 0.5.5
version: 0.6.2
'@comfyorg/shared-frontend-utils':
specifier: workspace:*
version: link:../../packages/shared-frontend-utils
@@ -1477,8 +1483,8 @@ packages:
'@cacheable/utils@2.3.2':
resolution: {integrity: sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg==}
'@comfyorg/comfyui-electron-types@0.5.5':
resolution: {integrity: sha512-f3XOXpMsALIwHakz7FekVPm4/Fh2pvJPEi8tRe8jYGBt8edsd4Mkkq31Yjs2Weem3BP7yNwbdNuSiQdP/pxJyg==}
'@comfyorg/comfyui-electron-types@0.6.2':
resolution: {integrity: sha512-r3By5Wbizq8jagUrhtcym79HYUTinsvoBnYkFFWbUmrURBWIaC0HduFVkRkI1PNdI76piW+JSOJJnw00YCVXeg==}
'@csstools/color-helpers@5.1.0':
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
@@ -8225,6 +8231,19 @@ packages:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'}
wtd-core@3.0.0:
resolution: {integrity: sha512-LSPfAQ5ULSV5vPhipcjdQvV5xOx25QesYK23jwUOF99xOx6fuulk7CMQerERRwA4uoQooNmRd8AT6IPBwORlWQ==}
wtd-three-ext@3.0.0:
resolution: {integrity: sha512-PLZJipCAiinot8D1uB4A7+XHxPAYeZXDhczbbazK7pKdqpE77zMizQH4rSZsaNbzktgnIfpgK/ODqhJTdrUjUw==}
peerDependencies:
three: '>= 0.137.5 < 1'
wwobjloader2@6.2.1:
resolution: {integrity: sha512-/v/sfUX0PMQAI8souzCs6xsO9LR3RyL+ujnOiS/1pngUlakKyHYC5XMQvu77pTeWzY3rzNyt5Q/bg5O3RukA+g==}
peerDependencies:
three: '>= 0.137.5 < 1'
xdg-basedir@5.1.0:
resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==}
engines: {node: '>=12'}
@@ -9179,7 +9198,7 @@ snapshots:
hashery: 1.3.0
keyv: 5.5.5
'@comfyorg/comfyui-electron-types@0.5.5': {}
'@comfyorg/comfyui-electron-types@0.6.2': {}
'@csstools/color-helpers@5.1.0': {}
@@ -17125,6 +17144,19 @@ snapshots:
dependencies:
is-wsl: 3.1.0
wtd-core@3.0.0: {}
wtd-three-ext@3.0.0(three@0.170.0):
dependencies:
three: 0.170.0
wtd-core: 3.0.0
wwobjloader2@6.2.1(three@0.170.0):
dependencies:
three: 0.170.0
wtd-core: 3.0.0
wtd-three-ext: 3.0.0(three@0.170.0)
xdg-basedir@5.1.0: {}
xml-name-validator@4.0.0: {}

View File

@@ -4,7 +4,7 @@ packages:
catalog:
'@alloc/quick-lru': ^5.2.0
'@comfyorg/comfyui-electron-types': 0.5.5
'@comfyorg/comfyui-electron-types': 0.6.2
'@eslint/js': ^9.39.1
'@iconify-json/lucide': ^1.1.178
'@iconify/json': ^2.2.380
@@ -104,6 +104,7 @@ catalog:
vue-router: ^4.4.3
vue-tsc: ^3.2.1
vuefire: ^3.2.1
wwobjloader2: ^6.2.1
yjs: ^13.6.27
zod: ^3.23.8
zod-to-json-schema: ^3.24.1

View File

@@ -92,7 +92,8 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { app } from '@/scripts/app'
import { useQueueStore } from '@/stores/queueStore'
import { useCommandStore } from '@/stores/commandStore'
import { useQueueStore, useQueueUIStore } from '@/stores/queueStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isElectron } from '@/utils/envUtil'
@@ -106,8 +107,10 @@ const { isLoggedIn } = useCurrentUser()
const isDesktop = isElectron()
const { t } = useI18n()
const { toastErrorHandler } = useErrorHandling()
const isQueueOverlayExpanded = ref(false)
const commandStore = useCommandStore()
const queueStore = useQueueStore()
const queueUIStore = useQueueUIStore()
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
const isTopMenuHovered = ref(false)
const queuedCount = computed(() => queueStore.pendingTasks.length)
const queueHistoryTooltipConfig = computed(() =>
@@ -133,7 +136,7 @@ onMounted(() => {
})
const toggleQueueOverlay = () => {
isQueueOverlayExpanded.value = !isQueueOverlayExpanded.value
commandStore.execute('Comfy.Queue.ToggleOverlay')
}
const openCustomNodeManager = async () => {

View File

@@ -33,6 +33,9 @@
v-model:playing="playing"
v-model:selected-speed="selectedSpeed"
v-model:selected-animation="selectedAnimation"
v-model:animation-progress="animationProgress"
v-model:animation-duration="animationDuration"
@seek="handleSeek"
/>
</div>
<div
@@ -119,6 +122,8 @@ const {
playing,
selectedSpeed,
selectedAnimation,
animationProgress,
animationDuration,
loading,
loadingMessage,
@@ -130,6 +135,7 @@ const {
handleStopRecording,
handleExportRecording,
handleClearRecording,
handleSeek,
handleBackgroundImageUpdate,
handleExportModel,
handleModelDrop,

View File

@@ -1,7 +1,7 @@
<template>
<div
ref="container"
class="relative h-full w-full"
class="relative h-full w-full min-h-[200px]"
data-capture-wheel="true"
@pointerdown.stop
@pointermove.stop

View File

@@ -15,6 +15,16 @@
@dragleave.stop="handleDragLeave"
@drop.prevent.stop="handleDrop"
/>
<AnimationControls
v-if="viewer.animations.value && viewer.animations.value.length > 0"
v-model:animations="viewer.animations.value"
v-model:playing="viewer.playing.value"
v-model:selected-speed="viewer.selectedSpeed.value"
v-model:selected-animation="viewer.selectedAnimation.value"
v-model:animation-progress="viewer.animationProgress.value"
v-model:animation-duration="viewer.animationDuration.value"
@seek="viewer.handleSeek"
/>
<div
v-if="isDragging"
class="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
@@ -85,6 +95,7 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, toRaw } from 'vue'
import AnimationControls from '@/components/load3d/controls/AnimationControls.vue'
import CameraControls from '@/components/load3d/controls/viewer/ViewerCameraControls.vue'
import ExportControls from '@/components/load3d/controls/viewer/ViewerExportControls.vue'
import LightControls from '@/components/load3d/controls/viewer/ViewerLightControls.vue'

View File

@@ -1,42 +1,64 @@
<template>
<div
v-if="animations && animations.length > 0"
class="pointer-events-auto absolute top-0 left-0 z-10 flex w-full items-center justify-center gap-2 pt-2"
class="pointer-events-auto absolute top-0 left-0 z-10 flex w-full flex-col items-center gap-2 pt-2"
>
<Button
size="icon"
variant="textonly"
class="rounded-full"
:aria-label="$t('g.playPause')"
@click="togglePlay"
>
<i
:class="['pi', playing ? 'pi-pause' : 'pi-play', 'text-lg text-white']"
<div class="flex items-center justify-center gap-2">
<Button
size="icon"
variant="textonly"
class="rounded-full"
:aria-label="$t('g.playPause')"
@click="togglePlay"
>
<i
:class="[
'pi',
playing ? 'pi-pause' : 'pi-play',
'text-lg text-white'
]"
/>
</Button>
<Select
v-model="selectedSpeed"
:options="speedOptions"
option-label="name"
option-value="value"
class="w-24"
/>
</Button>
<Select
v-model="selectedSpeed"
:options="speedOptions"
option-label="name"
option-value="value"
class="w-24"
/>
<Select
v-model="selectedAnimation"
:options="animations"
option-label="name"
option-value="index"
class="w-32"
/>
</div>
<Select
v-model="selectedAnimation"
:options="animations"
option-label="name"
option-value="index"
class="w-32"
/>
<div class="flex w-full max-w-xs items-center gap-2 px-4">
<Slider
:model-value="[animationProgress]"
:min="0"
:max="100"
:step="0.1"
class="flex-1"
@update:model-value="handleSliderChange"
/>
<span class="min-w-16 text-xs text-white">
{{ formatTime(currentTime) }} / {{ formatTime(animationDuration) }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import Select from 'primevue/select'
import { computed } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import Slider from '@/components/ui/slider/Slider.vue'
type Animation = { name: string; index: number }
@@ -44,6 +66,16 @@ const animations = defineModel<Animation[]>('animations')
const playing = defineModel<boolean>('playing')
const selectedSpeed = defineModel<number>('selectedSpeed')
const selectedAnimation = defineModel<number>('selectedAnimation')
const animationProgress = defineModel<number>('animationProgress', {
default: 0
})
const animationDuration = defineModel<number>('animationDuration', {
default: 0
})
const emit = defineEmits<{
seek: [progress: number]
}>()
const speedOptions = [
{ name: '0.1x', value: 0.1 },
@@ -53,7 +85,25 @@ const speedOptions = [
{ name: '2x', value: 2 }
]
const togglePlay = () => {
const currentTime = computed(() => {
if (!animationDuration.value) return 0
return (animationProgress.value / 100) * animationDuration.value
})
function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60)
const secs = (seconds % 60).toFixed(1)
return mins > 0 ? `${mins}:${secs.padStart(4, '0')}` : `${secs}s`
}
function togglePlay() {
playing.value = !playing.value
}
function handleSliderChange(value: number[] | undefined) {
if (!value) return
const progress = value[0]
animationProgress.value = progress
emit('seek', progress)
}
</script>

View File

@@ -1,8 +1,6 @@
<template>
<div class="flex flex-col gap-3 pb-3">
<h3
class="text-center text-[15px] font-sans text-[var(--descrip-text)] mt-2.5"
>
<h3 class="text-center text-[15px] font-sans text-descrip-text mt-2.5">
{{ t('maskEditor.brushSettings') }}
</h3>
@@ -10,120 +8,211 @@
{{ t('maskEditor.resetToDefault') }}
</button>
<!-- Brush Shape -->
<div class="flex flex-col gap-3 pb-3">
<span class="text-left text-xs font-sans text-[var(--descrip-text)]">{{
t('maskEditor.brushShape')
}}</span>
<span class="text-left text-xs font-sans text-descrip-text">
{{ t('maskEditor.brushShape') }}
</span>
<div
class="flex flex-row gap-2.5 items-center min-h-6 relative h-[50px] w-full rounded-[10px] bg-secondary-background-hover"
class="flex flex-row gap-2.5 items-center h-[50px] w-full rounded-[10px] bg-secondary-background-hover"
>
<div
class="maskEditor_sidePanelBrushShapeCircle bg-transparent hover:bg-comfy-menu-bg"
:class="{ active: store.brushSettings.type === BrushShape.Arc }"
:style="{
background:
class="maskEditor_sidePanelBrushShapeCircle hover:bg-comfy-menu-bg"
:class="
cn(
store.brushSettings.type === BrushShape.Arc
? 'var(--p-button-text-primary-color)'
: ''
}"
? 'bg-[var(--p-button-text-primary-color)] active'
: 'bg-transparent'
)
"
@click="setBrushShape(BrushShape.Arc)"
></div>
<div
class="maskEditor_sidePanelBrushShapeSquare bg-transparent hover:bg-comfy-menu-bg"
:class="{ active: store.brushSettings.type === BrushShape.Rect }"
:style="{
background:
class="maskEditor_sidePanelBrushShapeSquare hover:bg-comfy-menu-bg"
:class="
cn(
store.brushSettings.type === BrushShape.Rect
? 'var(--p-button-text-primary-color)'
: ''
}"
? 'bg-[var(--p-button-text-primary-color)] active'
: 'bg-transparent'
)
"
@click="setBrushShape(BrushShape.Rect)"
></div>
</div>
</div>
<!-- Color -->
<div class="flex flex-col gap-3 pb-3">
<span class="text-left text-xs font-sans text-[var(--descrip-text)]">{{
t('maskEditor.colorSelector')
}}</span>
<input type="color" :value="store.rgbColor" @input="onColorChange" />
<span class="text-left text-xs font-sans text-descrip-text">
{{ t('maskEditor.colorSelector') }}
</span>
<input
ref="colorInputRef"
v-model="store.rgbColor"
type="color"
class="h-10 rounded-md cursor-pointer"
/>
</div>
<SliderControl
:label="t('maskEditor.thickness')"
:min="1"
:max="500"
:step="1"
:model-value="store.brushSettings.size"
@update:model-value="onThicknessChange"
/>
<!-- Thickness -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-left text-xs font-sans text-descrip-text">
{{ t('maskEditor.thickness') }}
</span>
<input
v-model.number="brushSize"
type="number"
class="w-16 px-2 py-1 text-sm text-center border rounded-md bg-comfy-menu-bg border-p-form-field-border-color text-input-text"
:min="1"
:max="250"
:step="1"
/>
</div>
<SliderControl
v-model="brushSize"
class="flex-1"
label=""
:min="1"
:max="250"
:step="1"
/>
</div>
<SliderControl
:label="t('maskEditor.opacity')"
:min="0"
:max="1"
:step="0.01"
:model-value="store.brushSettings.opacity"
@update:model-value="onOpacityChange"
/>
<!-- Opacity -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-left text-xs font-sans text-descrip-text">
{{ t('maskEditor.opacity') }}
</span>
<input
v-model.number="brushOpacity"
type="number"
class="w-16 px-2 py-1 text-sm text-center border rounded-md bg-comfy-menu-bg border-p-form-field-border-color text-input-text"
:min="0"
:max="1"
:step="0.01"
/>
</div>
<SliderControl
v-model="brushOpacity"
class="flex-1"
label=""
:min="0"
:max="1"
:step="0.01"
/>
</div>
<SliderControl
:label="t('maskEditor.hardness')"
:min="0"
:max="1"
:step="0.01"
:model-value="store.brushSettings.hardness"
@update:model-value="onHardnessChange"
/>
<!-- Hardness -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-left text-xs font-sans text-descrip-text">
{{ t('maskEditor.hardness') }}
</span>
<input
v-model.number="brushHardness"
type="number"
class="w-16 px-2 py-1 text-sm text-center border rounded-md bg-comfy-menu-bg border-p-form-field-border-color text-input-text"
:min="0"
:max="1"
:step="0.01"
/>
</div>
<SliderControl
v-model="brushHardness"
class="flex-1"
label=""
:min="0"
:max="1"
:step="0.01"
/>
</div>
<SliderControl
:label="$t('maskEditor.stepSize')"
:min="1"
:max="100"
:step="1"
:model-value="store.brushSettings.stepSize"
@update:model-value="onStepSizeChange"
/>
<!-- Step Size -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-left text-xs font-sans text-descrip-text">
{{ t('maskEditor.stepSize') }}
</span>
<input
v-model.number="brushStepSize"
type="number"
class="w-16 px-2 py-1 text-sm text-center border rounded-md bg-comfy-menu-bg border-p-form-field-border-color text-input-text"
:min="1"
:max="100"
:step="1"
/>
</div>
<SliderControl
v-model="brushStepSize"
class="flex-1"
label=""
:min="1"
:max="100"
:step="1"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { BrushShape } from '@/extensions/core/maskeditor/types'
import { t } from '@/i18n'
import { useMaskEditorStore } from '@/stores/maskEditorStore'
import { cn } from '@/utils/tailwindUtil'
import SliderControl from './controls/SliderControl.vue'
const store = useMaskEditorStore()
const textButtonClass =
'h-7.5 w-32 rounded-[10px] border border-[var(--p-form-field-border-color)] text-[var(--input-text)] font-sans pointer-events-auto transition-colors duration-100 bg-[var(--comfy-menu-bg)] hover:bg-secondary-background-hover'
const colorInputRef = ref<HTMLInputElement>()
const textButtonClass =
'h-7.5 w-32 rounded-[10px] border border-p-form-field-border-color text-input-text font-sans transition-colors duration-100 bg-comfy-menu-bg hover:bg-secondary-background-hover'
/* Computed properties that use store setters for validation */
const brushSize = computed({
get: () => store.brushSettings.size,
set: (value: number) => store.setBrushSize(value)
})
const brushOpacity = computed({
get: () => store.brushSettings.opacity,
set: (value: number) => store.setBrushOpacity(value)
})
const brushHardness = computed({
get: () => store.brushSettings.hardness,
set: (value: number) => store.setBrushHardness(value)
})
const brushStepSize = computed({
get: () => store.brushSettings.stepSize,
set: (value: number) => store.setBrushStepSize(value)
})
/* Brush shape */
const setBrushShape = (shape: BrushShape) => {
store.brushSettings.type = shape
}
const onColorChange = (event: Event) => {
store.rgbColor = (event.target as HTMLInputElement).value
}
const onThicknessChange = (value: number) => {
store.setBrushSize(value)
}
const onOpacityChange = (value: number) => {
store.setBrushOpacity(value)
}
const onHardnessChange = (value: number) => {
store.setBrushHardness(value)
}
const onStepSizeChange = (value: number) => {
store.setBrushStepSize(value)
}
/* Reset */
const resetToDefault = () => {
store.resetBrushToDefault()
}
onMounted(() => {
if (colorInputRef.value) {
store.colorInput = colorInputRef.value
}
})
onBeforeUnmount(() => {
store.colorInput = null
})
</script>

View File

@@ -202,6 +202,7 @@ onBeforeUnmount(() => {
}
store.canvasHistory.clearStates()
store.resetState()
dataStore.reset()
})

View File

@@ -39,7 +39,11 @@ import { useLitegraphService } from '@/services/litegraphService'
import type { ComfyCommand } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useHelpCenterStore } from '@/stores/helpCenterStore'
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
import {
useQueueSettingsStore,
useQueueStore,
useQueueUIStore
} from '@/stores/queueStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { useSubgraphStore } from '@/stores/subgraphStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
@@ -423,6 +427,18 @@ export function useCoreCommands(): ComfyCommand[] {
},
active: () => useSettingStore().get('Comfy.Minimap.Visible')
},
{
id: 'Comfy.Queue.ToggleOverlay',
icon: 'pi pi-history',
label: () => t('queue.toggleJobHistory'),
menubarLabel: () => t('queue.jobHistory'),
versionAdded: '1.37.0',
category: 'view-controls' as const,
function: () => {
useQueueUIStore().toggleOverlay()
},
active: () => useQueueUIStore().isOverlayExpanded
},
{
id: 'Comfy.QueuePrompt',
icon: 'pi pi-play',

View File

@@ -60,6 +60,8 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
const playing = ref(false)
const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const animationProgress = ref(0)
const animationDuration = ref(0)
const loading = ref(false)
const loadingMessage = ref('')
const isPreview = ref(false)
@@ -357,6 +359,13 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
}
}
const handleSeek = (progress: number) => {
if (load3d && animationDuration.value > 0) {
const time = (progress / 100) * animationDuration.value
load3d.setAnimationTime(time)
}
}
const handleBackgroundImageUpdate = async (file: File | null) => {
if (!file) {
sceneConfig.value.backgroundImage = ''
@@ -514,6 +523,14 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
animationListChange: (newValue: AnimationItem[]) => {
animations.value = newValue
},
animationProgressChange: (data: {
progress: number
currentTime: number
duration: number
}) => {
animationProgress.value = data.progress
animationDuration.value = data.duration
},
cameraChanged: (cameraState: CameraState) => {
const rawNode = toRaw(nodeRef.value)
if (rawNode) {
@@ -573,6 +590,8 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
playing,
selectedSpeed,
selectedAnimation,
animationProgress,
animationDuration,
loading,
loadingMessage,
@@ -585,6 +604,7 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
handleStopRecording,
handleExportRecording,
handleClearRecording,
handleSeek,
handleBackgroundImageUpdate,
handleExportModel,
handleModelDrop,

View File

@@ -3,6 +3,7 @@ import { ref, toRaw, watch } from 'vue'
import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import type {
AnimationItem,
BackgroundRenderModeType,
CameraState,
CameraType,
@@ -49,6 +50,14 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
const isSplatModel = ref(false)
const isPlyModel = ref(false)
// Animation state
const animations = ref<AnimationItem[]>([])
const playing = ref(false)
const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const animationProgress = ref(0)
const animationDuration = ref(0)
let load3d: Load3d | null = null
let sourceLoad3d: Load3d | null = null
@@ -174,6 +183,61 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
}
})
// Animation watches
watch(playing, (newValue) => {
if (load3d) {
load3d.toggleAnimation(newValue)
}
})
watch(selectedSpeed, (newValue) => {
if (load3d && newValue) {
load3d.setAnimationSpeed(newValue)
}
})
watch(selectedAnimation, (newValue) => {
if (load3d && newValue !== undefined) {
load3d.updateSelectedAnimation(newValue)
}
})
const handleSeek = (progress: number) => {
if (load3d && animationDuration.value > 0) {
const time = (progress / 100) * animationDuration.value
load3d.setAnimationTime(time)
}
}
const setupAnimationEvents = () => {
if (!load3d) return
load3d.addEventListener(
'animationListChange',
(newValue: AnimationItem[]) => {
animations.value = newValue
}
)
load3d.addEventListener(
'animationProgressChange',
(data: { progress: number; currentTime: number; duration: number }) => {
animationProgress.value = data.progress
animationDuration.value = data.duration
}
)
// Initialize animation list if animations already exist
if (load3d.hasAnimations()) {
const clips = load3d.animationManager.animationClips
animations.value = clips.map((clip, index) => ({
name: clip.name || `Animation ${index + 1}`,
index
}))
animationDuration.value = load3d.getAnimationDuration()
}
}
/**
* Initialize viewer in node mode (with source Load3d)
*/
@@ -270,6 +334,8 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
upDirection: upDirection.value,
materialMode: materialMode.value
}
setupAnimationEvents()
} catch (error) {
console.error('Error initializing Load3d viewer:', error)
useToastStore().addAlert(
@@ -310,6 +376,8 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
isPlyModel.value = load3d.isPlyModel()
isPreview.value = true
setupAnimationEvents()
} catch (error) {
console.error('Error initializing standalone 3D viewer:', error)
useToastStore().addAlert('Failed to load 3D model')
@@ -527,6 +595,14 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
isSplatModel,
isPlyModel,
// Animation state
animations,
playing,
selectedSpeed,
selectedAnimation,
animationProgress,
animationDuration,
// Methods
initializeViewer,
initializeStandaloneViewer,
@@ -539,6 +615,7 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
refreshViewport,
handleBackgroundImageUpdate,
handleModelDrop,
handleSeek,
cleanup
}
}

View File

@@ -125,6 +125,13 @@ export class AnimationManager implements AnimationManagerInterface {
}
this.animationActions = [action]
// Emit initial progress to set duration
this.eventManager.emitEvent('animationProgressChange', {
progress: 0,
currentTime: 0,
duration: clip.duration
})
}
toggleAnimation(play?: boolean): void {
@@ -150,8 +157,58 @@ export class AnimationManager implements AnimationManagerInterface {
update(delta: number): void {
if (this.currentAnimation && this.isAnimationPlaying) {
this.currentAnimation.update(delta)
if (this.animationActions.length > 0) {
const action = this.animationActions[0]
const clip = action.getClip()
const progress = (action.time / clip.duration) * 100
this.eventManager.emitEvent('animationProgressChange', {
progress,
currentTime: action.time,
duration: clip.duration
})
}
}
}
getAnimationTime(): number {
if (this.animationActions.length === 0) return 0
return this.animationActions[0].time
}
getAnimationDuration(): number {
if (this.animationActions.length === 0) return 0
return this.animationActions[0].getClip().duration
}
setAnimationTime(time: number): void {
if (this.animationActions.length === 0) return
const duration = this.getAnimationDuration()
const clampedTime = Math.max(0, Math.min(time, duration))
// Temporarily unpause to allow time update, then restore
const wasPaused = this.animationActions.map((action) => action.paused)
this.animationActions.forEach((action) => {
action.paused = false
action.time = clampedTime
})
if (this.currentAnimation) {
this.currentAnimation.setTime(clampedTime)
this.currentAnimation.update(0)
}
// Restore paused state
this.animationActions.forEach((action, i) => {
action.paused = wasPaused[i]
})
this.eventManager.emitEvent('animationProgressChange', {
progress: (clampedTime / duration) * 100,
currentTime: clampedTime,
duration
})
}
reset(): void {}
}

View File

@@ -392,7 +392,8 @@ class Load3d {
this.STATUS_MOUSE_ON_SCENE ||
this.STATUS_MOUSE_ON_VIEWER ||
this.isRecording() ||
!this.INITIAL_RENDER_DONE
!this.INITIAL_RENDER_DONE ||
this.animationManager.isAnimationPlaying
)
}
@@ -726,6 +727,19 @@ class Load3d {
return this.animationManager.animationClips.length > 0
}
public getAnimationTime(): number {
return this.animationManager.getAnimationTime()
}
public getAnimationDuration(): number {
return this.animationManager.getAnimationDuration()
}
public setAnimationTime(time: number): void {
this.animationManager.setAnimationTime(time)
this.forceRender()
}
public remove(): void {
if (this.contextMenuAbortController) {
this.contextMenuAbortController.abort()

View File

@@ -34,9 +34,26 @@ class Load3dUtils {
return await resp.json()
}
static readonly MAX_UPLOAD_SIZE_MB = 100
static async uploadFile(file: File, subfolder: string) {
let uploadPath
const fileSizeMB = file.size / 1024 / 1024
if (fileSizeMB > this.MAX_UPLOAD_SIZE_MB) {
const message = t('toastMessages.fileTooLarge', {
size: fileSizeMB.toFixed(1),
maxSize: this.MAX_UPLOAD_SIZE_MB
})
console.warn(
'[Load3D] uploadFile: file too large',
fileSizeMB.toFixed(2),
'MB'
)
useToastStore().addAlert(message)
return undefined
}
try {
const body = new FormData()
body.append('image', file)
@@ -61,7 +78,7 @@ class Load3dUtils {
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
}
} catch (error) {
console.error('Upload error:', error)
console.error('[Load3D] uploadFile: exception', error)
useToastStore().addAlert(
error instanceof Error
? error.message

View File

@@ -3,9 +3,10 @@ import * as THREE from 'three'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { MtlObjBridge, OBJLoader2Parallel } from 'wwobjloader2'
import OBJLoader2WorkerUrl from 'wwobjloader2/worker?url'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -22,7 +23,7 @@ import { FastPLYLoader } from './loader/FastPLYLoader'
export class LoaderManager implements LoaderManagerInterface {
gltfLoader: GLTFLoader
objLoader: OBJLoader
objLoader: OBJLoader2Parallel
mtlLoader: MTLLoader
fbxLoader: FBXLoader
stlLoader: STLLoader
@@ -41,7 +42,12 @@ export class LoaderManager implements LoaderManagerInterface {
this.eventManager = eventManager
this.gltfLoader = new GLTFLoader()
this.objLoader = new OBJLoader()
this.objLoader = new OBJLoader2Parallel()
// Set worker URL for Vite compatibility
this.objLoader.setWorkerUrl(
true,
new URL(OBJLoader2WorkerUrl, import.meta.url)
)
this.mtlLoader = new MTLLoader()
this.fbxLoader = new FBXLoader()
this.stlLoader = new STLLoader()
@@ -173,7 +179,9 @@ export class LoaderManager implements LoaderManagerInterface {
const materials = await this.mtlLoader.loadAsync(mtlFileName)
materials.preload()
this.objLoader.setMaterials(materials)
const materialsFromMtl =
MtlObjBridge.addMaterialsFromMtlLoader(materials)
this.objLoader.setMaterials(materialsFromMtl)
} catch (e) {
console.log(
'No MTL file found or error loading it, continuing without materials'
@@ -181,8 +189,10 @@ export class LoaderManager implements LoaderManagerInterface {
}
}
this.objLoader.setPath(path)
model = await this.objLoader.loadAsync(filename)
// OBJLoader2Parallel uses Web Worker for parsing (non-blocking)
const objUrl = path + encodeURIComponent(filename)
model = await this.objLoader.loadAsync(objUrl)
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
this.modelManager.originalMaterials.set(child, child.material)
@@ -193,7 +203,6 @@ export class LoaderManager implements LoaderManagerInterface {
case 'gltf':
case 'glb':
this.gltfLoader.setPath(path)
const gltf = await this.gltfLoader.loadAsync(filename)
this.modelManager.setOriginalModel(gltf)

View File

@@ -4,8 +4,8 @@ import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
import { type GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { type OBJLoader2Parallel } from 'wwobjloader2'
export type MaterialMode =
| 'original'
@@ -146,6 +146,9 @@ export interface AnimationManagerInterface extends BaseManager {
updateSelectedAnimation(index: number): void
toggleAnimation(play?: boolean): void
update(delta: number): void
getAnimationTime(): number
getAnimationDuration(): number
setAnimationTime(time: number): void
}
export interface ModelManagerInterface {
@@ -176,7 +179,7 @@ export interface ModelManagerInterface {
export interface LoaderManagerInterface {
gltfLoader: GLTFLoader
objLoader: OBJLoader
objLoader: OBJLoader2Parallel
mtlLoader: MTLLoader
fbxLoader: FBXLoader
stlLoader: STLLoader

View File

@@ -37,6 +37,15 @@ function isOpened(): boolean {
return useDialogStore().isDialogOpen('global-mask-editor')
}
const changeBrushSize = async (sizeChanger: (oldSize: number) => number) => {
if (!isOpened()) return
const store = useMaskEditorStore()
const oldBrushSize = store.brushSettings.size
const newBrushSize = sizeChanger(oldBrushSize)
store.setBrushSize(newBrushSize)
}
app.registerExtension({
name: 'Comfy.MaskEditor',
settings: [
@@ -82,13 +91,24 @@ app.registerExtension({
id: 'Comfy.MaskEditor.BrushSize.Increase',
icon: 'pi pi-plus-circle',
label: 'Increase Brush Size in MaskEditor',
function: () => changeBrushSize((old) => _.clamp(old + 4, 1, 100))
function: () => changeBrushSize((old) => _.clamp(old + 2, 1, 250))
},
{
id: 'Comfy.MaskEditor.BrushSize.Decrease',
icon: 'pi pi-minus-circle',
label: 'Decrease Brush Size in MaskEditor',
function: () => changeBrushSize((old) => _.clamp(old - 4, 1, 100))
function: () => changeBrushSize((old) => _.clamp(old - 2, 1, 250))
},
{
id: 'Comfy.MaskEditor.ColorPicker',
icon: 'pi pi-palette',
label: 'Open Color Picker in MaskEditor',
function: () => {
if (!isOpened()) return
const store = useMaskEditorStore()
store.colorInput?.click()
}
}
],
init() {
@@ -101,12 +121,3 @@ app.registerExtension({
)
}
})
const changeBrushSize = async (sizeChanger: (oldSize: number) => number) => {
if (!isOpened()) return
const store = useMaskEditorStore()
const oldBrushSize = store.brushSettings.size
const newBrushSize = sizeChanger(oldBrushSize)
store.setBrushSize(newBrushSize)
}

View File

@@ -200,6 +200,9 @@
"Comfy_MaskEditor_BrushSize_Increase": {
"label": "Increase Brush Size in MaskEditor"
},
"Comfy_MaskEditor_ColorPicker": {
"label": "Open Color Picker in MaskEditor"
},
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Open Mask Editor for Selected Node"
},

View File

@@ -508,10 +508,12 @@
"title": "Choose your hardware setup",
"recommended": "RECOMMENDED",
"nvidiaSubtitle": "NVIDIA CUDA",
"amdSubtitle": "AMD ROCm™",
"cpuSubtitle": "CPU Mode",
"manualSubtitle": "Manual Setup",
"appleMetalDescription": "Leverages your Mac's GPU for faster speed and a better overall experience",
"nvidiaDescription": "Use your NVIDIA GPU with CUDA acceleration for the best performance.",
"amdDescription": "Use your AMD GPU with ROCm™ acceleration for the best performance.",
"cpuDescription": "Use CPU mode for compatibility when GPU acceleration is not available",
"manualDescription": "Configure ComfyUI manually for advanced setups or unsupported hardware"
},
@@ -1035,6 +1037,8 @@
"copyErrorMessage": "Copy error message",
"reportError": "Report error"
},
"toggleJobHistory": "Toggle Job History",
"jobHistory": "Job History",
"jobList": {
"undated": "Undated",
"sortMostRecent": "Most recent",
@@ -1137,6 +1141,7 @@
"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 Color Picker in MaskEditor": "Open Color Picker 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",
@@ -1698,6 +1703,7 @@
"pleaseSelectNodesToGroup": "Please select the nodes (or other groups) to create a group for",
"emptyCanvas": "Empty canvas",
"fileUploadFailed": "File upload failed",
"fileTooLarge": "File too large ({size} MB). Maximum supported size is {maxSize} MB",
"unableToGetModelFilePath": "Unable to get model file path",
"couldNotDetermineFileType": "Could not determine file type",
"errorLoadingModel": "Error loading model",

View File

@@ -4849,6 +4849,9 @@
"reference_images": {
"name": "reference_images",
"tooltip": "Up to 4 additional reference images."
},
"resolution": {
"name": "resolution"
}
},
"outputs": {
@@ -4881,6 +4884,9 @@
"reference_images": {
"name": "reference_images",
"tooltip": "Up to 6 additional reference images."
},
"resolution": {
"name": "resolution"
}
},
"outputs": {
@@ -4937,6 +4943,9 @@
"reference_images": {
"name": "reference_images",
"tooltip": "Up to 7 reference images."
},
"resolution": {
"name": "resolution"
}
},
"outputs": {
@@ -4961,6 +4970,9 @@
},
"duration": {
"name": "duration"
},
"resolution": {
"name": "resolution"
}
},
"outputs": {
@@ -4996,6 +5008,9 @@
"reference_images": {
"name": "reference_images",
"tooltip": "Up to 4 additional reference images."
},
"resolution": {
"name": "resolution"
}
},
"outputs": {
@@ -6531,7 +6546,7 @@
}
},
"Mahiro": {
"display_name": "Mahiro is so cute that she deserves a better guidance function!! (。・ω・。)",
"display_name": "Mahiro CFG",
"description": "Modify the guidance to scale more on the 'direction' of the positive prompt rather than the difference between the negative prompt.",
"inputs": {
"model": {

View File

@@ -80,6 +80,9 @@
"Comfy_Canvas_ToggleMinimap": {
"label": "画布切换小地图"
},
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "固定/取消固定选中项"
},
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
"label": "忽略/取消忽略选中节点"
},
@@ -92,9 +95,6 @@
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
"label": "固定/取消固定选中节点"
},
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "固定/取消固定选中项"
},
"Comfy_Canvas_Unlock": {
"label": "解锁画布"
},
@@ -290,6 +290,9 @@
"Workspace_ToggleBottomPanel": {
"label": "切换底部面板"
},
"Workspace_ToggleBottomPanel_Shortcuts": {
"label": "显示快捷键对话框"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "切换终端底部面板"
},
@@ -302,9 +305,6 @@
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
"label": "切换检视控制底部面板"
},
"Workspace_ToggleBottomPanel_Shortcuts": {
"label": "显示快捷键对话框"
},
"Workspace_ToggleFocusMode": {
"label": "切换焦点模式"
},
@@ -324,4 +324,4 @@
"label": "切换工作流侧边栏",
"tooltip": "工作流"
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -19,11 +19,11 @@
},
"Comfy-Desktop_WindowStyle": {
"name": "窗口样式",
"tooltip": "选择自定义选项以隐藏系统标题栏",
"options": {
"custom": "自定义",
"default": "默认"
},
"tooltip": "选择自定义选项以隐藏系统标题栏"
"default": "默认",
"custom": "自定义"
}
},
"Comfy_Canvas_BackgroundImage": {
"name": "画布背景图像",
@@ -46,9 +46,9 @@
"Comfy_Canvas_NavigationMode": {
"name": "画布导航模式",
"options": {
"Custom": "自定义",
"Standard (New)": "标准(新)",
"Drag Navigation": "拖动画布",
"Standard (New)": "标准(新)"
"Custom": "自定义"
}
},
"Comfy_Canvas_SelectionToolbox": {
@@ -81,14 +81,14 @@
},
"Comfy_Execution_PreviewMethod": {
"name": "实时预览",
"tooltip": "图像生成过程中实时预览。 \"默认\" 使用服务器 CLI 设置。",
"options": {
"auto": "自动",
"default": "默认",
"latent2rgb": "latent2rgb",
"none": "无",
"auto": "自动",
"latent2rgb": "latent2rgb",
"taesd": "taesd"
},
"tooltip": "图像生成过程中实时预览。 \"默认\" 使用服务器 CLI 设置。"
}
},
"Comfy_FloatRoundingPrecision": {
"name": "浮点组件四舍五入的小数位数 [0 = 自动]。",
@@ -106,9 +106,9 @@
"Comfy_Graph_LinkMarkers": {
"name": "连线中点标记",
"options": {
"Arrow": "箭头",
"None": "",
"Circle": "圆",
"None": ""
"Arrow": "箭头"
}
},
"Comfy_Graph_LiveSelection": {
@@ -128,25 +128,25 @@
"name": "释放连线时的操作",
"options": {
"context menu": "上下文菜单",
"no action": "无操作",
"search box": "搜索框"
"search box": "搜索框",
"no action": "无操作"
}
},
"Comfy_LinkRelease_ActionShift": {
"name": "释放连线时的操作Shift",
"options": {
"context menu": "上下文菜单",
"no action": "无操作",
"search box": "搜索框"
"search box": "搜索框",
"no action": "无操作"
}
},
"Comfy_LinkRenderMode": {
"name": "连线渲染样式",
"options": {
"Hidden": "隐藏",
"Straight": "直角线",
"Linear": "直线",
"Spline": "曲线",
"Straight": "直角线"
"Hidden": "隐藏"
}
},
"Comfy_Load3D_3DViewerEnable": {
@@ -159,11 +159,11 @@
},
"Comfy_Load3D_CameraType": {
"name": "摄像机类型",
"tooltip": "控制创建新的3D小部件时默认的相机是透视还是正交。这个默认设置仍然可以在创建后为每个小部件单独切换。",
"options": {
"orthographic": "正交",
"perspective": "透视"
},
"tooltip": "控制创建新的3D小部件时默认的相机是透视还是正交。这个默认设置仍然可以在创建后为每个小部件单独切换。"
"perspective": "透视",
"orthographic": "正交"
}
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "光照调整步长",
@@ -183,12 +183,12 @@
},
"Comfy_Load3D_PLYEngine": {
"name": "PLY 引擎",
"tooltip": "选择加载 PLY 文件的引擎。 \"threejs\" 使用原生 Three.js PLY 加载器(最适合网格 PLY。 \"fastply\" 使用专用于 ASCII 点云的 PLY 文件加载器。 \"sparkjs\" 使用 Spark.js 加载 3D 高斯泼溅 PLY 文件。",
"options": {
"threejs": "threejs",
"fastply": "fastply",
"sparkjs": "sparkjs",
"threejs": "threejs"
},
"tooltip": "选择加载 PLY 文件的引擎。 \"threejs\" 使用原生 Three.js PLY 加载器(最适合网格 PLY。 \"fastply\" 使用专用于 ASCII 点云的 PLY 文件加载器。 \"sparkjs\" 使用 Spark.js 加载 3D 高斯泼溅 PLY 文件。"
"sparkjs": "sparkjs"
}
},
"Comfy_Load3D_ShowGrid": {
"name": "显示网格",
@@ -211,11 +211,11 @@
},
"Comfy_ModelLibrary_NameFormat": {
"name": "在模型库树视图中显示的名称",
"tooltip": "选择“文件名”以在模型列表中显示原始文件名的简化视图(不带目录和“.safetensors”后缀名。选择“标题”以显示可配置的模型元数据标题。",
"options": {
"filename": "文件名",
"title": "标题"
},
"tooltip": "选择“文件名”以在模型列表中显示原始文件名的简化视图(不带目录和“.safetensors”后缀名。选择“标题”以显示可配置的模型元数据标题。"
}
},
"Comfy_Node_AllowImageSizeDraw": {
"name": "在图像预览下方显示宽度×高度"
@@ -266,9 +266,9 @@
"Comfy_NodeBadge_NodeSourceBadgeMode": {
"name": "节点源标签",
"options": {
"Hide built-in": "仅第三方",
"None": "无",
"Show all": "显示全部"
"Show all": "显示全部",
"Hide built-in": "仅第三方"
}
},
"Comfy_NodeBadge_ShowApiPricing": {
@@ -349,8 +349,8 @@
"Comfy_Sidebar_Style": {
"name": "侧边栏样式",
"options": {
"connected": "连接式",
"floating": "浮动式"
"floating": "浮动式",
"connected": "连接式"
}
},
"Comfy_Sidebar_UnifiedWidth": {
@@ -371,11 +371,11 @@
},
"Comfy_UseNewMenu": {
"name": "使用新菜单",
"tooltip": "选单列位置。在行动装置上,选单始终显示于顶端。",
"options": {
"Disabled": "禁用",
"Top": "顶部"
},
"tooltip": "选单列位置。在行动装置上,选单始终显示于顶端。"
}
},
"Comfy_Validation_Workflows": {
"name": "校验工作流"
@@ -390,11 +390,11 @@
},
"Comfy_WidgetControlMode": {
"name": "组件控制模式",
"tooltip": "控制组件值的更新时机(随机/增加/减少),可以在执行工作流之前或之后。",
"options": {
"after": "之",
"before": "之"
},
"tooltip": "控制组件值的更新时机(随机/增加/减少),可以在执行工作流之前或之后。"
"before": "之",
"after": "之"
}
},
"Comfy_Window_UnloadConfirmation": {
"name": "关闭窗口时显示确认"
@@ -402,8 +402,8 @@
"Comfy_Workflow_AutoSave": {
"name": "自动保存",
"options": {
"after delay": "延迟后",
"off": "关闭"
"off": "关闭",
"after delay": "延迟后"
}
},
"Comfy_Workflow_AutoSaveDelay": {

View File

@@ -1,4 +1,5 @@
import { toRaw } from 'vue'
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils'
import { nodeToLoad3dMap } from '@/composables/useLoad3d'
import { useLoad3dViewer } from '@/composables/useLoad3dViewer'
@@ -75,13 +76,20 @@ export class Load3dService {
const sourceModel = source.modelManager.currentModel
if (sourceModel) {
// Remove existing model from target scene before adding new one
const existingModel = target.getModelManager().currentModel
if (existingModel) {
target.getSceneManager().scene.remove(existingModel)
}
if (source.isSplatModel()) {
const originalURL = source.modelManager.originalURL
if (originalURL) {
await target.loadModel(originalURL)
}
} else {
const modelClone = sourceModel.clone()
// Use SkeletonUtils.clone for proper skeletal animation support
const modelClone = SkeletonUtils.clone(sourceModel)
target.getModelManager().currentModel = modelClone
target.getSceneManager().scene.add(modelClone)
@@ -105,6 +113,14 @@ export class Load3dService {
target.getModelManager().appliedTexture =
source.getModelManager().appliedTexture
}
// Copy animation state
if (source.hasAnimations()) {
target.animationManager.setupModelAnimations(
modelClone,
sourceOriginalModel
)
}
}
}

View File

@@ -73,6 +73,8 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
const tgpuRoot = ref<any>(null)
const colorInput = ref<HTMLInputElement | null>(null)
watch(maskCanvas, (canvas) => {
if (canvas) {
maskCtx.value = canvas.getContext('2d', { willReadFrequently: true })
@@ -113,7 +115,7 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
})
function setBrushSize(size: number): void {
brushSettings.value.size = _.clamp(size, 1, 500)
brushSettings.value.size = _.clamp(size, 1, 250)
}
function setBrushOpacity(opacity: number): void {
@@ -252,6 +254,8 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
tgpuRoot,
colorInput,
setBrushSize,
setBrushOpacity,
setBrushHardness,

View File

@@ -618,3 +618,13 @@ export const useQueueSettingsStore = defineStore('queueSettingsStore', {
batchCount: 1
})
})
export const useQueueUIStore = defineStore('queueUIStore', () => {
const isOverlayExpanded = ref(false)
function toggleOverlay() {
isOverlayExpanded.value = !isOverlayExpanded.value
}
return { isOverlayExpanded, toggleOverlay }
})