Compare commits

..

3 Commits

Author SHA1 Message Date
snomiao
ad295629cd [bugfix] Use ESLint.Plugin type assertion instead of 'as any' in eslint.config.ts
This resolves TypeScript type mismatch errors in the ESLint configuration
by properly typing the i18n plugin as ESLint.Plugin.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 07:47:03 +00:00
snomiao
4ee91e1244 [bugfix] Simplify pluginI18n type assertion to use 'as any'
Replace complex double type assertion with simpler 'as any' for better readability while maintaining type safety for the config object.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 09:55:56 +00:00
snomiao
53754bf99b [bugfix] Fix TypeScript type errors in eslint.config.ts
Resolves type compatibility issues between ESLint flat config and plugin type definitions that caused CI/local typecheck differences.

Changes:
- Spread pluginVue.configs['flat/recommended'] array properly with type assertion to Linter.Config[]
- Cast importX.flatConfigs to Linter.Config to resolve type incompatibilities
- Use type assertion chain for pluginI18n to work around legacy config type mismatches
- Import Linter type from 'eslint' for proper type assertions

These changes eliminate the need for @ts-expect-error and @ts-ignore directives while maintaining full type safety and runtime functionality.

Fixes #[issue-number-if-exists]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 08:57:36 +00:00
393 changed files with 25764 additions and 38252 deletions

View File

@@ -23,10 +23,10 @@ TEST_COMFYUI_DIR=/home/ComfyUI
# Whether to enable minification of the frontend code.
ENABLE_MINIFY=true
# Whether to disable proxying the `/templates` route. If true, allows you to
# serve templates from the ComfyUI_frontend/public/templates folder (for
# locally testing changes to templates). When false or nonexistent, the
# templates are served via the normal method from the server's python site
# Whether to disable proxying the `/templates` route. If true, allows you to
# serve templates from the ComfyUI_frontend/public/templates folder (for
# locally testing changes to templates). When false or nonexistent, the
# templates are served via the normal method from the server's python site
# packages.
DISABLE_TEMPLATES_PROXY=false
@@ -37,8 +37,3 @@ DISABLE_VUE_PLUGINS=false
# Algolia credentials required for developing with the new custom node manager.
ALGOLIA_APP_ID=4E0RO38HS8
ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579
# Sentry ENV vars replace with real ones for debugging
# SENTRY_AUTH_TOKEN=private-token # get from sentry
# SENTRY_ORG=comfy-org
# SENTRY_PROJECT=cloud-frontend-staging

View File

@@ -25,6 +25,3 @@ e3bb29ceb8174b8bbca9e48ec7d42cd540f40efa
# [refactor] Improve updates/notifications domain organization (#5590)
27ab355f9c73415dc39f4d3f512b02308f847801
# Migrate Tailwind styles to design-system package
9f19d8fb4bd22518879343b49c05634dca777df0

View File

@@ -1,116 +0,0 @@
name: Post Release Summary Comment
description: Post or update a PR comment summarizing release links with diff, derived versions, and optional extras.
author: ComfyUI Frontend Team
inputs:
issue-number:
description: Optional PR number override (defaults to the current pull request)
default: ''
version_file:
description: Path to the JSON file containing the current version (relative to repo root)
required: true
outputs:
prev_version:
description: Previous version derived from the parent commit
value: ${{ steps.build.outputs.prev_version }}
runs:
using: composite
steps:
- name: Build comment body
id: build
shell: bash
run: |
set -euo pipefail
VERSION_FILE="${{ inputs.version_file }}"
REPO="${{ github.repository }}"
if [[ -z "$VERSION_FILE" ]]; then
echo '::error::version_file input is required' >&2
exit 1
fi
PREV_JSON=$(git show HEAD^1:"$VERSION_FILE" 2>/dev/null || true)
if [[ -z "$PREV_JSON" ]]; then
echo "::error::Unable to read $VERSION_FILE from parent commit" >&2
exit 1
fi
PREV_VERSION=$(printf '%s' "$PREV_JSON" | node -pe "const data = JSON.parse(require('fs').readFileSync(0, 'utf8')); if (!data.version) { process.exit(1); } data.version")
if [[ -z "$PREV_VERSION" ]]; then
echo "::error::Unable to determine previous version from $VERSION_FILE" >&2
exit 1
fi
NEW_VERSION=$(node -pe "const fs=require('fs');const data=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));if(!data.version){process.exit(1);}data.version" "$VERSION_FILE")
if [[ -z "$NEW_VERSION" ]]; then
echo "::error::Unable to determine current version from $VERSION_FILE" >&2
exit 1
fi
MARKER='release-summary'
MESSAGE='Publish jobs finished successfully:'
LINKS_VALUE=''
case "$VERSION_FILE" in
package.json)
LINKS_VALUE=$'PyPI|https://pypi.org/project/comfyui-frontend-package/{{version}}/\n''npm types|https://npm.im/@comfyorg/comfyui-frontend-types@{{version}}'
;;
apps/desktop-ui/package.json)
MARKER='desktop-release-summary'
LINKS_VALUE='npm desktop UI|https://npm.im/@comfyorg/desktop-ui@{{version}}'
;;
esac
DIFF_PREFIX='v'
DIFF_LABEL='Diff'
DIFF_URL="https://github.com/${REPO}/compare/${DIFF_PREFIX}${PREV_VERSION}...${DIFF_PREFIX}${NEW_VERSION}"
COMMENT_FILE=$(mktemp)
{
printf '<!--%s:%s%s-->\n' "$MARKER" "$DIFF_PREFIX" "$NEW_VERSION"
printf '%s\n\n' "$MESSAGE"
printf -- '- %s: [%s%s...%s%s](%s)\n' "$DIFF_LABEL" "$DIFF_PREFIX" "$PREV_VERSION" "$DIFF_PREFIX" "$NEW_VERSION" "$DIFF_URL"
while IFS= read -r RAW_LINE; do
LINE=$(printf '%s' "$RAW_LINE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$LINE" ]] && continue
if [[ "$LINE" != *"|"* ]]; then
echo "::warning::Skipping malformed link entry: $LINE" >&2
continue
fi
LABEL=${LINE%%|*}
URL_TEMPLATE=${LINE#*|}
URL=${URL_TEMPLATE//\{\{version\}\}/$NEW_VERSION}
URL=${URL//\{\{prev_version\}\}/$PREV_VERSION}
printf -- '- %s: %s\n' "$LABEL" "$URL"
done <<< "$LINKS_VALUE"
printf '\n'
} > "$COMMENT_FILE"
{
echo "body<<'EOF'"
cat "$COMMENT_FILE"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
echo "prev_version=$PREV_VERSION" >> "$GITHUB_OUTPUT"
echo "marker_search=<!--$MARKER:" >> "$GITHUB_OUTPUT"
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
- name: Find existing comment
id: find
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: ${{ steps.build.outputs.marker_search }}
- name: Post or update comment
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-id: ${{ steps.find.outputs.comment-id }}
body: ${{ steps.build.outputs.body }}
edit-mode: replace

View File

@@ -17,9 +17,40 @@ concurrency:
cancel-in-progress: true
jobs:
claude-review:
wait-for-ci:
runs-on: ubuntu-latest
if: github.event.label.name == 'claude-review'
outputs:
should-proceed: ${{ steps.check-status.outputs.proceed }}
steps:
- name: Wait for other CI checks
uses: lewagon/wait-on-check-action@e106e5c43e8ca1edea6383a39a01c5ca495fd812
with:
ref: ${{ github.event.pull_request.head.sha }}
check-regexp: '^(lint-and-format|test|playwright-tests)'
allowed-conclusions: success,skipped,failure,cancelled,neutral,action_required,timed_out,stale
wait-interval: 30
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Check if we should proceed
id: check-status
run: |
CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("lint-and-format")) | {name, conclusion}')
if echo "$CHECK_RUNS" | grep -Eq '"conclusion": "(failure|cancelled|timed_out|action_required)"'; then
echo "Some CI checks failed - skipping Claude review"
echo "proceed=false" >> $GITHUB_OUTPUT
else
echo "All CI checks passed - proceeding with Claude review"
echo "proceed=true" >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
claude-review:
needs: wait-for-ci
if: needs.wait-for-ci.outputs.should-proceed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository

View File

@@ -1,10 +1,9 @@
---
name: Publish Desktop UI on PR Merge
on:
pull_request:
types: ['closed']
branches: [main, core/*]
types: [ closed ]
branches: [ main, core/* ]
paths:
- 'apps/desktop-ui/package.json'
@@ -58,26 +57,3 @@ jobs:
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
comment_desktop_publish:
name: Comment Desktop Publish Summary
needs:
- resolve
- publish
if: success()
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2
- name: Post desktop release summary comment
uses: ./.github/actions/comment-release-links
with:
issue-number: ${{ github.event.pull_request.number }}
version_file: apps/desktop-ui/package.json

View File

@@ -1,10 +1,9 @@
---
name: Release Draft Create
on:
pull_request:
types: ['closed']
branches: [main, core/*]
types: [ closed ]
branches: [ main, core/* ]
paths:
- 'package.json'
@@ -31,9 +30,7 @@ jobs:
- name: Get current version
id: current_version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Check if prerelease
id: check_prerelease
run: |
@@ -74,8 +71,7 @@ jobs:
name: dist-files
- name: Create release
id: create_release
uses: >-
softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -83,14 +79,9 @@ jobs:
dist.zip
tag_name: v${{ needs.build.outputs.version }}
target_commitish: ${{ github.event.pull_request.base.ref }}
make_latest: >-
${{ github.event.pull_request.base.ref == 'main' &&
needs.build.outputs.is_prerelease == 'false' }}
draft: >-
${{ github.event.pull_request.base.ref != 'main' ||
needs.build.outputs.is_prerelease == 'true' }}
prerelease: >-
${{ needs.build.outputs.is_prerelease == 'true' }}
make_latest: ${{ github.event.pull_request.base.ref == 'main' && needs.build.outputs.is_prerelease == 'false' }}
draft: ${{ github.event.pull_request.base.ref != 'main' || needs.build.outputs.is_prerelease == 'true' }}
prerelease: ${{ needs.build.outputs.is_prerelease == 'true' }}
generate_release_notes: true
publish_pypi:
@@ -119,8 +110,7 @@ jobs:
env:
COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }}
- name: Publish pypi package
uses: >-
pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist
@@ -132,28 +122,3 @@ jobs:
version: ${{ needs.build.outputs.version }}
ref: ${{ github.event.pull_request.merge_commit_sha }}
secrets: inherit
comment_release_summary:
name: Comment Release Summary
needs:
- draft_release
- publish_pypi
- publish_types
if: success()
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2
- name: Post release summary comment
uses: ./.github/actions/comment-release-links
with:
issue-number: ${{ github.event.pull_request.number }}
version_file: package.json

View File

@@ -142,3 +142,4 @@ jobs:
documentation
automated
draft: true
assignees: ${{ github.repository_owner }}

4
.gitignore vendored
View File

@@ -18,7 +18,6 @@ yarn.lock
.stylelintcache
node_modules
.pnpm-store
dist
dist-ssr
*.local
@@ -93,6 +92,3 @@ storybook-static
.github/instructions/nx.instructions.md
vite.config.*.timestamp*
vitest.config.*.timestamp*
# Weekly docs check output
/output.txt

View File

@@ -74,15 +74,8 @@ const config: StorybookConfig = {
'@': process.cwd() + '/src'
}
},
esbuild: {
// Prevent minification of identifiers to preserve _sfc_main
minifyIdentifiers: false,
keepNames: true
},
build: {
rollupOptions: {
// Disable tree-shaking for Storybook to prevent Vue SFC exports from being removed
treeshake: false,
onwarn: (warning, warn) => {
// Suppress specific warnings
if (

View File

@@ -75,15 +75,8 @@ const config: StorybookConfig = {
'@frontend-locales': process.cwd() + '/../../src/locales'
}
},
esbuild: {
// Prevent minification of identifiers to preserve _sfc_main
minifyIdentifiers: false,
keepNames: true
},
build: {
rollupOptions: {
// Disable tree-shaking for Storybook to prevent Vue SFC exports from being removed
treeshake: false,
onwarn: (warning, warn) => {
// Suppress specific warnings
if (

View File

@@ -1,206 +0,0 @@
<template>
<Select
:id="dropdownId"
v-model="selectedLocale"
:options="localeOptions"
option-label="label"
option-value="value"
:disabled="isSwitching"
:pt="dropdownPt"
:size="props.size"
class="language-selector"
@change="onLocaleChange"
>
<template #value="{ value }">
<span :class="valueClass">
<i class="pi pi-language" :class="iconClass" />
<span>{{ displayLabel(value as SupportedLocale) }}</span>
</span>
</template>
<template #option="{ option }">
<span :class="optionClass">
<i class="pi pi-language" :class="iconClass" />
<span class="leading-none">{{ option.label }}</span>
</span>
</template>
</Select>
</template>
<script setup lang="ts">
import Select from 'primevue/select'
import type { SelectChangeEvent } from 'primevue/select'
import { computed, ref, watch } from 'vue'
import { i18n, loadLocale, st } from '@/i18n'
type VariantKey = 'dark' | 'light'
type SizeKey = 'small' | 'large'
const props = withDefaults(
defineProps<{
variant?: VariantKey
size?: SizeKey
}>(),
{
variant: 'dark',
size: 'small'
}
)
const dropdownId = `language-select-${Math.random().toString(36).slice(2)}`
const LOCALES = [
['en', 'English'],
['zh', '中文'],
['zh-TW', '繁體中文'],
['ru', 'Русский'],
['ja', '日本語'],
['ko', '한국어'],
['fr', 'Français'],
['es', 'Español'],
['ar', 'عربي'],
['tr', 'Türkçe']
] as const satisfies ReadonlyArray<[string, string]>
type SupportedLocale = (typeof LOCALES)[number][0]
const SIZE_PRESETS = {
large: {
wrapper: 'px-3 py-1 min-w-[7rem]',
gap: 'gap-2',
valueText: 'text-xs',
optionText: 'text-sm',
icon: 'text-sm'
},
small: {
wrapper: 'px-2 py-0.5 min-w-[5rem]',
gap: 'gap-1',
valueText: 'text-[0.65rem]',
optionText: 'text-xs',
icon: 'text-xs'
}
} as const satisfies Record<SizeKey, Record<string, string>>
const VARIANT_PRESETS = {
light: {
root: 'bg-white/80 border border-neutral-200 text-neutral-700 rounded-full shadow-sm backdrop-blur hover:border-neutral-400 transition-colors focus-visible:ring-offset-2 focus-visible:ring-offset-white',
trigger: 'text-neutral-500 hover:text-neutral-700',
item: 'text-neutral-700 bg-transparent hover:bg-neutral-100 focus-visible:outline-none',
valueText: 'text-neutral-600',
optionText: 'text-neutral-600',
icon: 'text-neutral-500'
},
dark: {
root: 'bg-neutral-900/70 border border-neutral-700 text-neutral-200 rounded-full shadow-sm backdrop-blur hover:border-neutral-500 transition-colors focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-900',
trigger: 'text-neutral-400 hover:text-neutral-200',
item: 'text-neutral-200 bg-transparent hover:bg-neutral-800/80 focus-visible:outline-none',
valueText: 'text-neutral-100',
optionText: 'text-neutral-100',
icon: 'text-neutral-300'
}
} as const satisfies Record<VariantKey, Record<string, string>>
const selectedLocale = ref<string>(i18n.global.locale.value)
const isSwitching = ref(false)
const sizePreset = computed(() => SIZE_PRESETS[props.size as SizeKey])
const variantPreset = computed(
() => VARIANT_PRESETS[props.variant as VariantKey]
)
const dropdownPt = computed(() => ({
root: {
class: `${variantPreset.value.root} ${sizePreset.value.wrapper}`
},
trigger: {
class: variantPreset.value.trigger
},
item: {
class: `${variantPreset.value.item} ${sizePreset.value.optionText}`
}
}))
const valueClass = computed(() =>
[
'flex items-center font-medium uppercase tracking-wide leading-tight',
sizePreset.value.gap,
sizePreset.value.valueText,
variantPreset.value.valueText
].join(' ')
)
const optionClass = computed(() =>
[
'flex items-center leading-tight',
sizePreset.value.gap,
variantPreset.value.optionText,
sizePreset.value.optionText
].join(' ')
)
const iconClass = computed(() =>
[sizePreset.value.icon, variantPreset.value.icon].join(' ')
)
const localeOptions = computed(() =>
LOCALES.map(([value, fallback]) => ({
value,
label: st(`settings.Comfy_Locale.options.${value}`, fallback)
}))
)
const labelLookup = computed(() =>
localeOptions.value.reduce<Record<string, string>>((acc, option) => {
acc[option.value] = option.label
return acc
}, {})
)
function displayLabel(locale?: SupportedLocale) {
if (!locale) {
return st('settings.Comfy_Locale.name', 'Language')
}
return labelLookup.value[locale] ?? locale
}
watch(
() => i18n.global.locale.value,
(newLocale) => {
if (newLocale !== selectedLocale.value) {
selectedLocale.value = newLocale
}
}
)
async function onLocaleChange(event: SelectChangeEvent) {
const nextLocale = event.value as SupportedLocale | undefined
if (!nextLocale || nextLocale === i18n.global.locale.value) {
return
}
isSwitching.value = true
try {
await loadLocale(nextLocale)
i18n.global.locale.value = nextLocale
} catch (error) {
console.error(`Failed to change locale to "${nextLocale}"`, error)
selectedLocale.value = i18n.global.locale.value
} finally {
isSwitching.value = false
}
}
</script>
<style scoped>
@reference '../../assets/css/style.css';
:deep(.p-dropdown-panel .p-dropdown-item) {
@apply transition-colors;
}
:deep(.p-dropdown) {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-yellow/60 focus-visible:ring-offset-2;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<BaseViewTemplate dark hide-language-selector>
<BaseViewTemplate dark>
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
<div
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"

View File

@@ -1,7 +1,7 @@
<template>
<BaseViewTemplate dark>
<div class="flex items-center justify-center min-h-screen">
<div class="grid gap-8">
<div class="grid grid-rows-2 gap-8">
<!-- Top container: Logo -->
<div class="flex items-end justify-center">
<img

View File

@@ -1,15 +1,12 @@
<template>
<div
class="font-sans w-screen h-screen flex flex-col relative"
class="font-sans w-screen h-screen flex flex-col"
:class="[
dark
? 'text-neutral-300 bg-neutral-900 dark-theme'
: 'text-neutral-900 bg-neutral-300'
]"
>
<div v-if="showLanguageSelector" class="absolute top-6 right-6 z-10">
<LanguageSelector :variant="variant" />
</div>
<!-- Virtual top menu for native window (drag handle) -->
<div
v-show="isNativeWindow()"
@@ -23,20 +20,14 @@
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref } from 'vue'
import LanguageSelector from '@/components/common/LanguageSelector.vue'
import { nextTick, onMounted, ref } from 'vue'
import { electronAPI, isElectron, isNativeWindow } from '../../utils/envUtil'
const { dark = false, hideLanguageSelector = false } = defineProps<{
const { dark = false } = defineProps<{
dark?: boolean
hideLanguageSelector?: boolean
}>()
const variant = computed(() => (dark ? 'dark' : 'light'))
const showLanguageSelector = computed(() => !hideLanguageSelector)
const darkTheme = {
color: 'rgba(0, 0, 0, 0)',
symbolColor: '#d4d4d4'

View File

@@ -503,7 +503,7 @@ export class NodeReference {
for (const position of clickPositions) {
// Clear any selection first
await this.comfyPage.canvas.click({
position: { x: 250, y: 250 },
position: { x: 50, y: 50 },
force: true
})
await this.comfyPage.nextFrame()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -7,6 +7,7 @@ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import storybook from 'eslint-plugin-storybook'
import unusedImports from 'eslint-plugin-unused-imports'
import pluginVue from 'eslint-plugin-vue'
import type { ESLint, Linter } from 'eslint'
import { defineConfig } from 'eslint/config'
import globals from 'globals'
import {
@@ -89,21 +90,15 @@ export default defineConfig([
pluginJs.configs.recommended,
tseslintConfigs.recommended,
// Difference in typecheck on CI vs Local
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Bad types in the plugin
pluginVue.configs['flat/recommended'],
...(pluginVue.configs['flat/recommended'] as Linter.Config[]),
eslintPluginPrettierRecommended,
storybook.configs['flat/recommended'],
// @ts-expect-error Bad types in the plugin
importX.flatConfigs.recommended,
// @ts-expect-error Bad types in the plugin
importX.flatConfigs.typescript,
importX.flatConfigs.recommended as Linter.Config,
importX.flatConfigs.typescript as Linter.Config,
{
plugins: {
'unused-imports': unusedImports,
// @ts-expect-error Bad types in the plugin
'@intlify/vue-i18n': pluginI18n
'@intlify/vue-i18n': pluginI18n as ESLint.Plugin
},
rules: {
'@typescript-eslint/no-floating-promises': 'error',

14
global.d.ts vendored
View File

@@ -8,21 +8,7 @@ declare const __USE_PROD_CONFIG__: boolean
interface Window {
__CONFIG__: {
mixpanel_token?: string
require_whitelist?: boolean
subscription_required?: boolean
max_upload_size?: number
comfy_api_base_url?: string
comfy_platform_base_url?: string
firebase_config?: {
apiKey: string
authDomain: string
databaseURL?: string
projectId: string
storageBucket: string
messagingSenderId: string
appId: string
measurementId?: string
}
server_health_alert?: {
message: string
tooltip?: string

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.32.4",
"version": "1.31.1",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -16,7 +16,6 @@
"size:collect": "node scripts/size-collect.js",
"size:report": "node scripts/size-report.js",
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
"dev:cloud": "cross-env DEV_SERVER_COMFYUI_URL='https://testcloud.comfy.org/' nx serve",
"dev:desktop": "nx dev @comfyorg/desktop-ui",
"dev:electron": "nx serve --config vite.electron.config.mts",
"dev": "nx serve",
@@ -44,8 +43,7 @@
"test:browser": "pnpm exec nx e2e",
"test:unit": "nx run test",
"typecheck": "vue-tsc --noEmit",
"zipdist": "node scripts/zipdist.js",
"clean": "nx reset"
"zipdist": "node scripts/zipdist.js"
},
"devDependencies": {
"@eslint/js": "catalog:",
@@ -58,7 +56,6 @@
"@pinia/testing": "catalog:",
"@playwright/test": "catalog:",
"@prettier/plugin-oxc": "catalog:",
"@sentry/vite-plugin": "catalog:",
"@storybook/addon-docs": "catalog:",
"@storybook/vue3": "catalog:",
"@storybook/vue3-vite": "catalog:",

View File

@@ -49,43 +49,27 @@
--color-smoke-800: #8a8a8a;
--color-sand-100: #e1ded5;
--color-sand-200: #fff7d5;
--color-sand-200: #d6cfc2;
--color-sand-300: #888682;
--color-sand-400: #eed7ac;
--color-slate-100: #9c9eab;
--color-slate-200: #9fa2bd;
--color-slate-300: #5b5e7d;
--color-white: #ffffff;
--color-black: #000000;
--color-electric-400: #f0ff41;
--color-sapphire-700: #172dd7;
--color-brand-yellow: var(--color-electric-400);
--color-brand-blue: var(--color-sapphire-700);
--color-azure-300: #78bae9;
--color-azure-400: #31b9f4;
--color-azure-600: #0b8ce9;
--color-cobalt-800: #185a8b;
--color-jade-400: #47e469;
--color-jade-600: #00cd72;
--color-graphite-400: #9C9EAB;
--color-gold-400: #fcbf64;
--color-gold-500: #fdab34;
--color-gold-600: #fd9903;
--color-coral-500: #f75951;
--color-coral-600: #e04e48;
--color-coral-700: #b33a3a;
--color-magenta-300: #ceaac9;
--color-magenta-700: #6a246a;
--color-danger-100: #c02323;
--color-danger-200: #d62952;
@@ -117,11 +101,6 @@
var(--color-smoke-500) 50%,
transparent
);
--color-alpha-smoke-500-20: #c5c5c533;
--color-alpha-smoke-400-40: #d9d9d966;
--color-alpha-azure-600-30: #0b8ce94d;
--color-alpha-magenta-700-60: #6a246a99;
--color-alpha-magenta-300-60: #ceaac999;
/* PrimeVue pulled colors */
--color-muted: var(--p-text-muted-color);
@@ -158,10 +137,6 @@
--button-surface: var(--color-white);
--button-surface-contrast: var(--color-black);
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
--modal-card-button-surface: var(--color-smoke-300);
/* Code styling colors for help menu*/
--code-text-color: rgb(0 122 255 / 1);
--code-bg-color: rgb(96 165 250 / 0.2);
@@ -221,50 +196,6 @@
--text-secondary: var(--color-ash-500);
--text-primary: var(--color-charcoal-700);
--input-surface: rgb(0 0 0 / 0.15);
/* Semantic tokens - light mode */
--muted-foreground: var(--color-charcoal-200);
--base-foreground: var(--color-charcoal-800);
--brand-yellow: var(--color-electric-400);
--brand-blue: var(--color-sapphire-700);
--secondary-background: var(--color-smoke-200);
--secondary-background-hover: var(--color-smoke-200);
--secondary-background-selected: var(--color-smoke-600);
--base-background: var(--color-white);
--primary-background: var(--color-azure-400);
--primary-background-hover: var(--color-cobalt-800);
--destructive-background: var(--color-coral-500);
--destructive-background-hover: var(--color-coral-600);
--inverted-background-hover: var(--color-charcoal-600);
--warning-background: var(--color-gold-400);
--warning-background-hover: var(--color-gold-500);
--border-default: var(--color-smoke-600);
--border-subtle: var(--color-smoke-400);
--muted-background: var(--color-smoke-700);
--accent-background: var(--color-smoke-800);
/* Component/Node tokens from design system light */
--component-node-background: var(--color-white);
--component-node-border: var(--color-border-default);
--component-node-foreground: var(--base-foreground);
--component-node-foreground-secondary: var(--color-muted-foreground);
--component-node-widget-background: var(--secondary-background);
--component-node-widget-background-hovered: var(--secondary-background-hover);
--component-node-widget-background-selected: var(--secondary-background-selected);
--component-node-widget-background-disabled: var(--color-alpha-ash-500-20);
--component-node-widget-background-highlighted: var(--color-ash-500);
/* Default UI element color palette variables */
--palette-contrast-mix-color: #fff;
--palette-interface-panel-surface: var(--comfy-menu-bg);
--palette-interface-stroke: color-mix(in srgb, var(--interface-panel-surface) 75.5%, var(--contrast-mix-color));
--palette-interface-panel-box-shadow: 1px 1px 8px 0 rgb(0 0 0 / 0.4);
--palette-interface-panel-drop-shadow: 1px 1px 4px rgb(0 0 0 / 0.4);
--palette-interface-panel-hover-surface: color-mix(in srgb, var(--interface-panel-surface) 92.5%, var(--contrast-mix-color));
--palette-interface-panel-selected-surface: color-mix(in srgb, var(--interface-panel-surface) 87.5%, var(--contrast-mix-color));
--palette-interface-button-hover-surface: color-mix(in srgb, var(--interface-panel-surface) 82%, var(--contrast-mix-color));
}
.dark-theme {
@@ -284,10 +215,6 @@
--button-active-surface: var(--color-charcoal-600);
--button-icon: var(--color-smoke-800);
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
--modal-card-button-surface: var(--color-charcoal-300);
--dialog-surface: var(--color-neutral-700);
--interface-menu-component-surface-hovered: var(--color-charcoal-400);
@@ -313,7 +240,7 @@
--node-component-surface-highlight: var(--color-slate-100);
--node-component-surface-hovered: var(--color-charcoal-600);
--node-component-surface-selected: var(--color-charcoal-200);
--node-component-surface: var(--color-charcoal-600);
--node-component-surface: var(--color-charcoal-800);
--node-component-tooltip: var(--color-white);
--node-component-tooltip-border: var(--color-slate-300);
--node-component-tooltip-surface: var(--color-charcoal-800);
@@ -330,38 +257,6 @@
--text-primary: var(--color-white);
--input-surface: rgb(130 130 130 / 0.1);
/* Semantic tokens - dark mode */
--muted-foreground: var(--color-smoke-800);
--base-foreground: var(--color-white);
--brand-yellow: var(--color-electric-400);
--brand-blue: var(--color-sapphire-700);
--secondary-background: var(--color-charcoal-600);
--secondary-background-hover: var(--color-charcoal-400);
--secondary-background-selected: var(--color-charcoal-200);
--base-background: var(--color-charcoal-800);
--primary-background: var(--color-azure-600);
--primary-background-hover: var(--color-azure-400);
--destructive-background: var(--color-coral-700);
--destructive-background-hover: var(--color-coral-600);
--inverted-background-hover: var(--color-smoke-200);
--warning-background: var(--color-gold-600);
--warning-background-hover: var(--color-gold-500);
--border-default: var(--color-charcoal-200);
--border-subtle: var(--color-charcoal-300);
--muted-background: var(--color-charcoal-100);
--accent-background: var(--color-charcoal-100);
/* Component/Node tokens from design dark system */
--component-node-background: var(--color-charcoal-600);
--component-node-border: var(--color-charcoal-100);
--component-node-foreground: var(--base-foreground);
--component-node-foreground-secondary: var(--color-muted-foreground);
--component-node-widget-background: var(--secondary-background-hover);
--component-node-widget-background-hovered: var(--secondary-background-selected);
--component-node-widget-background-selected: var(--color-charcoal-100);
--component-node-widget-background-disabled: var(--color-alpha-charcoal-600-30);
--component-node-widget-background-highlighted: var(--color-graphite-400);
}
@theme inline {
@@ -371,8 +266,6 @@
--color-button-icon: var(--button-icon);
--color-button-surface: var(--button-surface);
--color-button-surface-contrast: var(--button-surface-contrast);
--color-subscription-button-gradient: var(--subscription-button-gradient);
--color-modal-card-button-surface: var(--modal-card-button-surface);
--color-dialog-surface: var(--dialog-surface);
--color-interface-menu-component-surface-hovered: var(
--interface-menu-component-surface-hovered
@@ -384,14 +277,6 @@
--interface-menu-keybind-surface-default
);
--color-interface-panel-surface: var(--interface-panel-surface);
--color-interface-panel-hover-surface: var(--interface-panel-hover-surface);
--color-interface-panel-selected-surface: var(
--interface-panel-selected-surface
);
--color-interface-button-hover-surface: var(
--interface-button-hover-surface
);
--color-comfy-menu-bg: var(--comfy-menu-bg);
--color-interface-stroke: var(--interface-stroke);
--color-nav-background: var(--nav-background);
--color-node-border: var(--node-border);
@@ -436,38 +321,6 @@
--color-text-secondary: var(--text-secondary);
--color-text-primary: var(--text-primary);
--color-input-surface: var(--input-surface);
/* Component/Node design tokens */
--color-component-node-background: var(--component-node-background);
--color-component-node-border: var(--component-node-border);
--color-component-node-foreground: var(--component-node-foreground);
--color-component-node-foreground-secondary: var(--component-node-foreground-secondary);
--color-component-node-widget-background: var(--component-node-widget-background);
--color-component-node-widget-background-hovered: var(--component-node-widget-background-hovered);
--color-component-node-widget-background-selected: var(--component-node-widget-background-selected);
--color-component-node-widget-background-disabled: var(--component-node-widget-background-disabled);
--color-component-node-widget-background-highlighted: var(--component-node-widget-background-highlighted);
/* Semantic tokens */
--color-base-foreground: var(--base-foreground);
--color-muted-foreground: var(--muted-foreground);
--color-base-background: var(--base-background);
--color-secondary-background: var(--secondary-background);
--color-secondary-background-hover: var(--secondary-background-hover);
--color-secondary-background-selected: var(--secondary-background-selected);
--color-primary-background: var(--primary-background);
--color-primary-background-hover: var(--primary-background-hover);
--color-destructive-background: var(--destructive-background);
--color-destructive-background-hover: var(--destructive-background-hover);
--color-inverted-background-hover: var(--inverted-background-hover);
--color-warning-background: var(--warning-background);
--color-warning-background-hover: var(--warning-background-hover);
--color-border-default: var(--border-default);
--color-border-subtle: var(--border-subtle);
--color-muted-background: var(--muted-background);
--color-accent-background: var(--accent-background);
--color-brand-yellow: var(--brand-yellow);
--color-brand-blue: var(--brand-blue);
}
@custom-variant dark-theme {
@@ -551,40 +404,36 @@ body {
.comfy-markdown {
/* We assign the textarea and the Tiptap editor to the same CSS grid area to stack them on top of one another. */
display: grid;
& > textarea,
.tiptap {
grid-area: 1 / 1 / 2 / 2;
}
}
& > textarea {
opacity: 0;
pointer-events: none;
}
.comfy-markdown > textarea {
grid-area: 1 / 1 / 2 / 2;
}
&.editing {
& > textarea {
opacity: 1;
pointer-events: all;
}
.comfy-markdown .tiptap {
grid-area: 1 / 1 / 2 / 2;
background-color: var(--comfy-input-bg);
color: var(--input-text);
overflow: hidden;
overflow-y: auto;
resize: none;
border: none;
box-sizing: border-box;
font-size: var(--comfy-textarea-font-size);
height: 100%;
padding: 0.5em;
}
.tiptap {
opacity: 0;
pointer-events: none;
}
}
.comfy-markdown.editing .tiptap {
display: none;
}
.tiptap {
overflow-y: auto;
font-size: var(--comfy-textarea-font-size);
.comfy-markdown .tiptap :first-child {
margin-top: 0;
}
:first-child {
margin-top: 0;
}
:last-child {
margin-bottom: 0;
}
}
.comfy-markdown .tiptap :last-child {
margin-bottom: 0;
}
.comfy-markdown .tiptap blockquote {
@@ -1261,6 +1110,31 @@ audio.comfy-audio.empty-audio-widget {
padding: var(--comfy-tree-explorer-item-padding) !important;
}
/* Load3d styles */
.comfy-load-3d,
.comfy-load-3d-animation,
.comfy-preview-3d,
.comfy-preview-3d-animation {
display: flex;
flex-direction: column;
background: transparent;
flex: 1;
position: relative;
overflow: hidden;
}
.comfy-load-3d canvas,
.comfy-load-3d-animation canvas,
.comfy-preview-3d canvas,
.comfy-preview-3d-animation canvas,
.comfy-load-3d-viewer canvas {
display: flex;
width: 100% !important;
height: 100% !important;
}
/* End of Load3d styles */
/* [Desktop] Electron window specific styles */
.app-drag {
app-region: drag;

View File

@@ -1,4 +0,0 @@
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.13727 11.2201L6.27454 14.4398" stroke="var(--pin-color, var(--color-smoke-800, #8a8a8a))" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" />
<path d="M6.28085 6.68385C6.21652 6.92342 6.08664 7.1403 5.9058 7.31009C5.72497 7.47989 5.50035 7.59587 5.25721 7.645L3.95569 7.91742C3.71254 7.96655 3.48793 8.08253 3.30709 8.25233C3.12626 8.42212 2.99637 8.639 2.93204 8.87857L2.80091 9.36797C2.75515 9.53876 2.7791 9.72073 2.86751 9.87385C2.95591 10.027 3.10153 10.1387 3.27231 10.1845L10.9997 12.255C11.1705 12.3008 11.3525 12.2768 11.5056 12.1884C11.6587 12.1 11.7705 11.9544 11.8162 11.7836L11.9474 11.2942C12.0114 11.0546 12.0074 10.8018 11.9357 10.5643C11.864 10.3269 11.7274 10.1141 11.5414 9.95001L10.5505 9.06333C10.3645 8.89921 10.2279 8.68646 10.1562 8.44899C10.0845 8.21153 10.0805 7.95877 10.1446 7.71913L10.7933 5.29787C10.8391 5.12709 10.9508 4.98148 11.1039 4.89307C11.2571 4.80466 11.439 4.78071 11.6098 4.82647C11.9514 4.91799 12.3153 4.87008 12.6216 4.69327C12.9278 4.51646 13.1513 4.22523 13.2428 3.88366C13.3343 3.54209 13.2864 3.17815 13.1096 2.8719C12.9328 2.56566 12.6416 2.34219 12.3 2.25067L7.1484 0.870299C6.80683 0.778775 6.44289 0.826689 6.13665 1.0035C5.8304 1.18031 5.60694 1.47154 5.51541 1.81311C5.42389 2.15468 5.4718 2.51862 5.64861 2.82487C5.82542 3.13111 6.11665 3.35458 6.45822 3.4461C6.62901 3.49186 6.77462 3.6036 6.86302 3.75672C6.95143 3.90984 6.97539 4.09181 6.92962 4.2626L6.28085 6.68385Z" fill="var(--handle-color, var(--color-gold-600, #fd9903))" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -4,13 +4,6 @@ import { addDynamicIconSelectors } from '@iconify/tailwind'
import { iconCollection } from './src/iconCollection'
export default {
theme: {
extend: {
boxShadow: {
interface: 'var(--interface-panel-box-shadow)'
}
}
},
plugins: [
addDynamicIconSelectors({
iconSets: {

File diff suppressed because it is too large Load Diff

230
pnpm-lock.yaml generated
View File

@@ -12,9 +12,15 @@ catalogs:
'@eslint/js':
specifier: ^9.35.0
version: 9.35.0
'@iconify-json/lucide':
specifier: ^1.1.178
version: 1.2.66
'@iconify/json':
specifier: ^2.2.380
version: 2.2.380
'@iconify/tailwind':
specifier: ^1.1.3
version: 1.2.0
'@intlify/eslint-plugin-vue-i18n':
specifier: ^4.1.0
version: 4.1.0
@@ -63,9 +69,6 @@ catalogs:
'@primevue/themes':
specifier: ^4.2.5
version: 4.2.5
'@sentry/vite-plugin':
specifier: ^4.6.0
version: 4.6.0
'@sentry/vue':
specifier: ^8.48.0
version: 8.48.0
@@ -495,9 +498,6 @@ importers:
'@prettier/plugin-oxc':
specifier: 'catalog:'
version: 0.0.4
'@sentry/vite-plugin':
specifier: 'catalog:'
version: 4.6.0
'@storybook/addon-docs':
specifier: 'catalog:'
version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
@@ -2772,78 +2772,14 @@ packages:
resolution: {integrity: sha512-csILVupc5RkrsTrncuUTGmlB56FQSFjXPYWG8I8yBTGlXEJ+o8oTuF6+55R4vbw3EIzBveXWi4kEBbnQlXW/eg==}
engines: {node: '>=14.18'}
'@sentry/babel-plugin-component-annotate@4.6.0':
resolution: {integrity: sha512-3soTX50JPQQ51FSbb4qvNBf4z/yP7jTdn43vMTp9E4IxvJ9HKJR7OEuKkCMszrZmWsVABXl02msqO7QisePdiQ==}
engines: {node: '>= 14'}
'@sentry/browser@8.48.0':
resolution: {integrity: sha512-fuuVULB5/1vI8NoIwXwR3xwhJJqk+y4RdSdajExGF7nnUDBpwUJyXsmYJnOkBO+oLeEs58xaCpotCKiPUNnE3g==}
engines: {node: '>=14.18'}
'@sentry/bundler-plugin-core@4.6.0':
resolution: {integrity: sha512-Fub2XQqrS258jjS8qAxLLU1k1h5UCNJ76i8m4qZJJdogWWaF8t00KnnTyp9TEDJzrVD64tRXS8+HHENxmeUo3g==}
engines: {node: '>= 14'}
'@sentry/cli-darwin@2.57.0':
resolution: {integrity: sha512-v1wYQU3BcCO+Z3OVxxO+EnaW4oQhuOza6CXeYZ0z5ftza9r0QQBLz3bcZKTVta86xraNm0z8GDlREwinyddOxQ==}
engines: {node: '>=10'}
os: [darwin]
'@sentry/cli-linux-arm64@2.57.0':
resolution: {integrity: sha512-Kh1jTsMV5Fy/RvB381N/woXe1qclRMqsG6kM3Gq6m6afEF/+k3PyQdNW3HXAola6d63EptokLtxPG2xjWQ+w9Q==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux, freebsd, android]
'@sentry/cli-linux-arm@2.57.0':
resolution: {integrity: sha512-uNHB8xyygqfMd1/6tFzl9NUkuVefg7jdZtM/vVCQVaF/rJLWZ++Wms+LLhYyKXKN8yd7J9wy7kTEl4Qu4jWbGQ==}
engines: {node: '>=10'}
cpu: [arm]
os: [linux, freebsd, android]
'@sentry/cli-linux-i686@2.57.0':
resolution: {integrity: sha512-EYXghoK/tKd0zqz+KD/ewXXE3u1HLCwG89krweveytBy/qw7M5z58eFvw+iGb1Vnbl1f/fRD0G4E0AbEsPfmpg==}
engines: {node: '>=10'}
cpu: [x86, ia32]
os: [linux, freebsd, android]
'@sentry/cli-linux-x64@2.57.0':
resolution: {integrity: sha512-CyZrP/ssHmAPLSzfd4ydy7icDnwmDD6o3QjhkWwVFmCd+9slSBMQxpIqpamZmrWE6X4R+xBRbSUjmdoJoZ5yMw==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux, freebsd, android]
'@sentry/cli-win32-arm64@2.57.0':
resolution: {integrity: sha512-wji/GGE4Lh5I/dNCsuVbg6fRvttvZRG6db1yPW1BSvQRh8DdnVy1CVp+HMqSq0SRy/S4z60j2u+m4yXMoCL+5g==}
engines: {node: '>=10'}
cpu: [arm64]
os: [win32]
'@sentry/cli-win32-i686@2.57.0':
resolution: {integrity: sha512-hWvzyD7bTPh3b55qvJ1Okg3Wbl0Km8xcL6KvS7gfBl6uss+I6RldmQTP0gJKdHSdf/QlJN1FK0b7bLnCB3wHsg==}
engines: {node: '>=10'}
cpu: [x86, ia32]
os: [win32]
'@sentry/cli-win32-x64@2.57.0':
resolution: {integrity: sha512-QWYV/Y0sbpDSTyA4XQBOTaid4a6H2Iwa1Z8UI+qNxFlk0ADSEgIqo2NrRHDU8iRnghTkecQNX1NTt/7mXN3f/A==}
engines: {node: '>=10'}
cpu: [x64]
os: [win32]
'@sentry/cli@2.57.0':
resolution: {integrity: sha512-oC4HPrVIX06GvUTgK0i+WbNgIA9Zl5YEcwf9N4eWFJJmjonr2j4SML9Hn2yNENbUWDgwepy4MLod3P8rM4bk/w==}
engines: {node: '>= 10'}
hasBin: true
'@sentry/core@8.48.0':
resolution: {integrity: sha512-VGwYgTfLpvJ5LRO5A+qWo1gpo6SfqaGXL9TOzVgBucAdpzbrYHpZ87sEarDVq/4275uk1b0S293/mfsskFczyw==}
engines: {node: '>=14.18'}
'@sentry/vite-plugin@4.6.0':
resolution: {integrity: sha512-fMR2d+EHwbzBa0S1fp45SNUTProxmyFBp+DeBWWQOSP9IU6AH6ea2rqrpMAnp/skkcdW4z4LSRrOEpMZ5rWXLw==}
engines: {node: '>= 14'}
'@sentry/vue@8.48.0':
resolution: {integrity: sha512-hqm9X7hz1vMQQB1HBYezrDBQihZk6e/MxWIG1wMJoClcBnD1Sh7y+D36UwaQlR4Gr/Ftiz+Bb0DxuAYHoUS4ow==}
engines: {node: '>=14.18'}
@@ -3750,10 +3686,6 @@ packages:
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
engines: {node: '>= 10.0.0'}
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
@@ -5027,9 +4959,6 @@ packages:
resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
engines: {node: '>=14.14'}
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -5110,10 +5039,6 @@ packages:
engines: {node: 20 || >=22}
hasBin: true
glob@9.3.5:
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
engines: {node: '>=16 || 14 >=14.17'}
global-directory@4.0.1:
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
engines: {node: '>=18'}
@@ -5243,10 +5168,6 @@ packages:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
@@ -5932,10 +5853,6 @@ packages:
magic-string@0.30.19:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
magic-string@0.30.8:
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
engines: {node: '>=12'}
magicast@0.3.5:
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
@@ -6156,10 +6073,6 @@ packages:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
minimatch@8.0.4:
resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==}
engines: {node: '>=16 || 14 >=14.17'}
minimatch@9.0.1:
resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -6175,10 +6088,6 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
minipass@4.2.8:
resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==}
engines: {node: '>=8'}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -6618,10 +6527,6 @@ packages:
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
progress@2.0.3:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
promise@7.3.1:
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
@@ -7521,9 +7426,6 @@ packages:
'@nuxt/kit':
optional: true
unplugin@1.0.1:
resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==}
unplugin@1.16.1:
resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==}
engines: {node: '>=14.0.0'}
@@ -7797,13 +7699,6 @@ packages:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
webpack-sources@3.3.3:
resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==}
engines: {node: '>=10.13.0'}
webpack-virtual-modules@0.5.0:
resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
@@ -10310,8 +10205,6 @@ snapshots:
'@sentry-internal/browser-utils': 8.48.0
'@sentry/core': 8.48.0
'@sentry/babel-plugin-component-annotate@4.6.0': {}
'@sentry/browser@8.48.0':
dependencies:
'@sentry-internal/browser-utils': 8.48.0
@@ -10320,74 +10213,8 @@ snapshots:
'@sentry-internal/replay-canvas': 8.48.0
'@sentry/core': 8.48.0
'@sentry/bundler-plugin-core@4.6.0':
dependencies:
'@babel/core': 7.27.1
'@sentry/babel-plugin-component-annotate': 4.6.0
'@sentry/cli': 2.57.0
dotenv: 16.6.1
find-up: 5.0.0
glob: 9.3.5
magic-string: 0.30.8
unplugin: 1.0.1
transitivePeerDependencies:
- encoding
- supports-color
'@sentry/cli-darwin@2.57.0':
optional: true
'@sentry/cli-linux-arm64@2.57.0':
optional: true
'@sentry/cli-linux-arm@2.57.0':
optional: true
'@sentry/cli-linux-i686@2.57.0':
optional: true
'@sentry/cli-linux-x64@2.57.0':
optional: true
'@sentry/cli-win32-arm64@2.57.0':
optional: true
'@sentry/cli-win32-i686@2.57.0':
optional: true
'@sentry/cli-win32-x64@2.57.0':
optional: true
'@sentry/cli@2.57.0':
dependencies:
https-proxy-agent: 5.0.1
node-fetch: 2.7.0
progress: 2.0.3
proxy-from-env: 1.1.0
which: 2.0.2
optionalDependencies:
'@sentry/cli-darwin': 2.57.0
'@sentry/cli-linux-arm': 2.57.0
'@sentry/cli-linux-arm64': 2.57.0
'@sentry/cli-linux-i686': 2.57.0
'@sentry/cli-linux-x64': 2.57.0
'@sentry/cli-win32-arm64': 2.57.0
'@sentry/cli-win32-i686': 2.57.0
'@sentry/cli-win32-x64': 2.57.0
transitivePeerDependencies:
- encoding
- supports-color
'@sentry/core@8.48.0': {}
'@sentry/vite-plugin@4.6.0':
dependencies:
'@sentry/bundler-plugin-core': 4.6.0
unplugin: 1.0.1
transitivePeerDependencies:
- encoding
- supports-color
'@sentry/vue@8.48.0(pinia@2.2.2(typescript@5.9.2)(vue@3.5.13(typescript@5.9.2)))(vue@3.5.13(typescript@5.9.2))':
dependencies:
'@sentry/browser': 8.48.0
@@ -11387,12 +11214,6 @@ snapshots:
address@1.2.2: {}
agent-base@6.0.2:
dependencies:
debug: 4.4.3
transitivePeerDependencies:
- supports-color
agent-base@7.1.4: {}
agentkeepalive@4.6.0:
@@ -12892,8 +12713,6 @@ snapshots:
jsonfile: 6.2.0
universalify: 2.0.1
fs.realpath@1.0.0: {}
fsevents@2.3.2:
optional: true
@@ -12988,13 +12807,6 @@ snapshots:
package-json-from-dist: 1.0.0
path-scurry: 2.0.0
glob@9.3.5:
dependencies:
fs.realpath: 1.0.0
minimatch: 8.0.4
minipass: 4.2.8
path-scurry: 1.11.1
global-directory@4.0.1:
dependencies:
ini: 4.1.1
@@ -13125,13 +12937,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
@@ -13821,10 +13626,6 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magic-string@0.30.8:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magicast@0.3.5:
dependencies:
'@babel/parser': 7.28.4
@@ -14230,10 +14031,6 @@ snapshots:
dependencies:
brace-expansion: 2.0.2
minimatch@8.0.4:
dependencies:
brace-expansion: 2.0.2
minimatch@9.0.1:
dependencies:
brace-expansion: 2.0.2
@@ -14248,8 +14045,6 @@ snapshots:
minimist@1.2.8: {}
minipass@4.2.8: {}
minipass@7.1.2: {}
minizlib@3.0.2:
@@ -14755,8 +14550,6 @@ snapshots:
process-nextick-args@2.0.1: {}
progress@2.0.3: {}
promise@7.3.1:
dependencies:
asap: 2.0.6
@@ -15896,13 +15689,6 @@ snapshots:
- rollup
- supports-color
unplugin@1.0.1:
dependencies:
acorn: 8.15.0
chokidar: 3.6.0
webpack-sources: 3.3.3
webpack-virtual-modules: 0.5.0
unplugin@1.16.1:
dependencies:
acorn: 8.15.0
@@ -16251,10 +16037,6 @@ snapshots:
webidl-conversions@7.0.0: {}
webpack-sources@3.3.3: {}
webpack-virtual-modules@0.5.0: {}
webpack-virtual-modules@0.6.2: {}
websocket-driver@0.7.4:

View File

@@ -24,7 +24,6 @@ catalog:
'@primevue/forms': ^4.2.5
'@primevue/icons': 4.2.5
'@primevue/themes': ^4.2.5
'@sentry/vite-plugin': ^4.6.0
'@sentry/vue': ^8.48.0
'@storybook/addon-docs': ^9.1.1
'@storybook/vue3': ^9.1.1
@@ -112,7 +111,6 @@ onlyBuiltDependencies:
- '@playwright/browser-chromium'
- '@playwright/browser-firefox'
- '@playwright/browser-webkit'
- '@sentry/cli'
- '@tailwindcss/oxide'
- esbuild
- nx

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

View File

@@ -16,10 +16,6 @@ import { computed, onMounted } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { t } from '@/i18n'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
@@ -27,8 +23,6 @@ import { electronAPI, isElectron } from './utils/envUtil'
const workspaceStore = useWorkspaceStore()
const conflictDetection = useConflictDetection()
const workflowStore = useWorkflowStore()
const dialogService = useDialogService()
const isLoading = computed<boolean>(() => workspaceStore.spinner)
const handleKey = (e: KeyboardEvent) => {
workspaceStore.shiftDown = e.shiftKey
@@ -54,26 +48,6 @@ onMounted(() => {
document.addEventListener('contextmenu', showContextMenu)
}
// Handle Vite preload errors (e.g., when assets are deleted after deployment)
window.addEventListener('vite:preloadError', async (_event) => {
// Auto-reload if app is not ready or there are no unsaved changes
if (!app.vueAppReady || !workflowStore.activeWorkflow?.isModified) {
window.location.reload()
} else {
// Show confirmation dialog if there are unsaved changes
await dialogService
.confirm({
title: t('g.vitePreloadErrorTitle'),
message: t('g.vitePreloadErrorMessage')
})
.then((confirmed) => {
if (confirmed) {
window.location.reload()
}
})
}
})
// Initialize conflict detection in background
// This runs async and doesn't block UI setup
void conflictDetection.initializeConflictDetection()

View File

@@ -22,7 +22,7 @@
},
"litegraph_base": {
"BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=",
"CLEAR_BACKGROUND_COLOR": "#141414",
"CLEAR_BACKGROUND_COLOR": "#222",
"NODE_TITLE_COLOR": "#999",
"NODE_SELECTED_TITLE_COLOR": "#FFF",
"NODE_TEXT_SIZE": 14,
@@ -52,7 +52,7 @@
"comfy_base": {
"fg-color": "#fff",
"bg-color": "#202020",
"comfy-menu-bg": "#171718",
"comfy-menu-bg": "#353535",
"comfy-menu-secondary-bg": "#303030",
"comfy-input-bg": "#222",
"input-text": "#ddd",

View File

@@ -68,12 +68,7 @@
"content-fg": "#222",
"content-hover-bg": "#adadad",
"content-hover-fg": "#222",
"bar-shadow": "rgba(16, 16, 16, 0.25) 0 0 0.5rem",
"interface-panel-box-shadow": "1px 1px 8px 0 rgba(0, 0, 0, 0.2)",
"interface-panel-drop-shadow": "1px 1px 4px rgba(0, 0, 0, 0.4)",
"interface-panel-hover-surface": "var(--color-gray-200)",
"interface-panel-selected-surface": "color-mix(in srgb, var(--interface-panel-surface) 78%, var(--contrast-mix-color))",
"contrast-mix-color": "#000"
"bar-shadow": "rgba(16, 16, 16, 0.25) 0 0 0.5rem"
}
}
}

View File

@@ -1,13 +1,12 @@
<template>
<div v-if="!workspaceStore.focusMode" class="ml-1 flex gap-x-0.5 pt-1">
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-1">
<div class="min-w-0 flex-1">
<SubgraphBreadcrumb />
</div>
<div
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-[var(--interface-stroke)] px-2 shadow-interface"
class="actionbar-container pointer-events-auto mx-1 flex h-12 items-center rounded-lg px-2 shadow-md"
>
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
@@ -25,7 +24,6 @@ import { onMounted, ref } from 'vue'
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
@@ -50,5 +48,6 @@ onMounted(() => {
<style scoped>
.actionbar-container {
background-color: var(--comfy-menu-bg);
border: 1px solid var(--p-panel-border-color);
}
</style>

View File

@@ -48,7 +48,6 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { cn } from '@/utils/tailwindUtil'
import ComfyRunButton from './ComfyRunButton'
@@ -133,15 +132,6 @@ watch(visible, async (newVisible) => {
}
})
/**
* Track run button handle drag start using mousedown on the drag handle.
*/
useEventListener(dragHandleRef, 'mousedown', () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'actionbar_run_handle_drag_start'
})
})
const lastDragState = ref({
x: x.value,
y: y.value,
@@ -268,9 +258,7 @@ const panelClass = computed(() =>
cn(
'actionbar pointer-events-auto z1000',
isDragging.value && 'select-none pointer-events-none',
isDocked.value
? 'p-0 static mr-2 border-none bg-transparent'
: 'fixed shadow-interface'
isDocked.value ? 'p-0 static mr-2 border-none bg-transparent' : 'fixed'
)
)
</script>

View File

@@ -100,7 +100,7 @@ import BatchCountEdit from '../BatchCountEdit.vue'
const workspaceStore = useWorkspaceStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => {
@@ -118,9 +118,6 @@ const queueModeMenuItemLookup = computed(() => {
label: `${t('menu.run')} (${t('menu.onChange')})`,
tooltip: t('menu.onChangeTooltip'),
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_mode_option_run_on_change_selected'
})
queueMode.value = 'change'
}
}
@@ -131,9 +128,6 @@ const queueModeMenuItemLookup = computed(() => {
label: `${t('menu.run')} (${t('menu.instant')})`,
tooltip: t('menu.instantTooltip'),
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_mode_option_run_instant_selected'
})
queueMode.value = 'instant'
}
}
@@ -164,18 +158,11 @@ const queuePrompt = async (e: Event) => {
? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt'
if (batchCount.value > 1) {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_run_multiple_batches_submitted'
})
if (isCloud) {
useTelemetry()?.trackRunButton({ subscribe_to_run: false })
}
await commandStore.execute(commandId, {
metadata: {
subscribe_to_run: false,
trigger_source: 'button'
}
})
await commandStore.execute(commandId)
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="subgraph-breadcrumb w-auto drop-shadow-[var(--interface-panel-drop-shadow)]"
class="subgraph-breadcrumb w-auto drop-shadow-md"
:class="{
'subgraph-breadcrumb-collapse': collapseTabs,
'subgraph-breadcrumb-overflow': overflowingTabs
@@ -40,7 +40,6 @@ import { computed, onUpdated, ref, watch } from 'vue'
import SubgraphBreadcrumbItem from '@/components/breadcrumb/SubgraphBreadcrumbItem.vue'
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
import { useTelemetry } from '@/platform/telemetry'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
@@ -74,9 +73,6 @@ const items = computed(() => {
const items = navigationStore.navigationStack.map<MenuItem>((subgraph) => ({
label: subgraph.name,
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'breadcrumb_subgraph_item_selected'
})
const canvas = useCanvasStore().getCanvas()
if (!canvas.graph) throw new TypeError('Canvas has no graph')
@@ -101,9 +97,6 @@ const home = computed(() => ({
key: 'root',
isBlueprint: isBlueprint.value,
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'breadcrumb_subgraph_root_selected'
})
const canvas = useCanvasStore().getCanvas()
if (!canvas.graph) throw new TypeError('Canvas has no graph')
@@ -208,8 +201,8 @@ onUpdated(() => {
:deep(.p-breadcrumb-separator),
:deep(.p-breadcrumb-item) {
@apply h-12;
border-top: 1px solid var(--interface-stroke);
border-bottom: 1px solid var(--interface-stroke);
border-top: 1px solid var(--p-panel-border-color);
border-bottom: 1px solid var(--p-panel-border-color);
background-color: var(--comfy-menu-bg);
}
@@ -221,7 +214,7 @@ onUpdated(() => {
@apply rounded-l-lg;
/* Then collapse the root workflow */
flex-shrink: 5000;
border-left: 1px solid var(--interface-stroke);
border-left: 1px solid var(--p-panel-border-color);
.p-breadcrumb-item-link {
padding-left: var(--p-breadcrumb-item-padding);
@@ -232,7 +225,7 @@ onUpdated(() => {
@apply rounded-r-lg;
/* Then collapse the active item */
flex-shrink: 1;
border-right: 1px solid var(--interface-stroke);
border-right: 1px solid var(--p-panel-border-color);
}
:deep(.p-breadcrumb-item-link:hover),

View File

@@ -21,8 +21,6 @@
@keyup.enter="blurInputElement"
@keyup.escape="cancelEditing"
@click.stop
@pointerdown.stop.capture
@pointermove.stop.capture
/>
</div>
</template>

View File

@@ -56,7 +56,7 @@ describe('UserAvatar', () => {
const avatar = wrapper.findComponent(Avatar)
expect(avatar.exists()).toBe(true)
expect(avatar.props('image')).toBeNull()
expect(avatar.props('icon')).toBe('icon-[lucide--user]')
expect(avatar.props('icon')).toBe('pi pi-user')
})
it('renders with default icon when provided photo Url is null', () => {
@@ -67,7 +67,7 @@ describe('UserAvatar', () => {
const avatar = wrapper.findComponent(Avatar)
expect(avatar.exists()).toBe(true)
expect(avatar.props('image')).toBeNull()
expect(avatar.props('icon')).toBe('icon-[lucide--user]')
expect(avatar.props('icon')).toBe('pi pi-user')
})
it('falls back to icon when image fails to load', async () => {
@@ -82,7 +82,7 @@ describe('UserAvatar', () => {
avatar.vm.$emit('error')
await nextTick()
expect(avatar.props('icon')).toBe('icon-[lucide--user]')
expect(avatar.props('icon')).toBe('pi pi-user')
})
it('uses provided ariaLabel', () => {

View File

@@ -1,9 +1,7 @@
<template>
<Avatar
class="bg-interface-panel-selected-surface"
:image="photoUrl ?? undefined"
:icon="hasAvatar ? undefined : 'icon-[lucide--user]'"
:pt:icon:class="{ 'size-4': !hasAvatar }"
:icon="hasAvatar ? undefined : 'pi pi-user'"
shape="circle"
:aria-label="ariaLabel ?? $t('auth.login.userAvatar')"
@error="handleImageError"

View File

@@ -68,17 +68,17 @@
</template>
</MultiSelect>
<!-- Runs On Filter -->
<!-- License Filter -->
<MultiSelect
v-model="selectedRunsOnObjects"
:label="runsOnFilterLabel"
:options="runsOnOptions"
v-model="selectedLicenseObjects"
:label="licenseFilterLabel"
:options="licenseOptions"
:show-search-box="true"
:show-selected-count="true"
:show-clear-button="true"
>
<template #icon>
<i class="icon-[lucide--server]" />
<i class="icon-[lucide--file-text]" />
</template>
</MultiSelect>
</div>
@@ -146,7 +146,7 @@
size="compact"
variant="ghost"
rounded="lg"
class="hover:bg-base-background"
class="hover:bg-white dark-theme:hover:bg-zinc-800"
>
<template #top>
<CardTop ratio="landscape">
@@ -180,7 +180,7 @@
variant="ghost"
rounded="lg"
:data-testid="`template-workflow-${template.name}`"
class="hover:bg-base-background"
class="hover:bg-white dark-theme:hover:bg-zinc-800"
@mouseenter="hoveredTemplate = template.name"
@mouseleave="hoveredTemplate = null"
@click="onLoadWorkflow(template)"
@@ -325,7 +325,7 @@
size="compact"
variant="ghost"
rounded="lg"
class="hover:bg-base-background"
class="hover:bg-white dark-theme:hover:bg-zinc-800"
>
<template #top>
<CardTop ratio="square">
@@ -382,7 +382,7 @@
<script setup lang="ts">
import { useAsyncState } from '@vueuse/core'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
import { computed, onBeforeUnmount, provide, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
@@ -403,8 +403,6 @@ import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
import { useLazyPagination } from '@/composables/useLazyPagination'
import { useTemplateFiltering } from '@/composables/useTemplateFiltering'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useTemplateWorkflows } from '@/platform/workflow/templates/composables/useTemplateWorkflows'
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
@@ -414,34 +412,10 @@ import { createGridStyle } from '@/utils/gridUtil'
const { t } = useI18n()
const { onClose: originalOnClose } = defineProps<{
const { onClose } = defineProps<{
onClose: () => void
}>()
// Track session time for telemetry
const sessionStartTime = ref<number>(0)
const templateWasSelected = ref(false)
onMounted(() => {
sessionStartTime.value = Date.now()
})
// Wrap onClose to track session end
const onClose = () => {
if (isCloud) {
const timeSpentSeconds = Math.floor(
(Date.now() - sessionStartTime.value) / 1000
)
useTelemetry()?.trackTemplateLibraryClosed({
template_selected: templateWasSelected.value,
time_spent_seconds: timeSpentSeconds
})
}
originalOnClose()
}
provide(OnCloseKey, onClose)
// Workflow templates store and composable
@@ -528,12 +502,12 @@ const {
searchQuery,
selectedModels,
selectedUseCases,
selectedRunsOn,
selectedLicenses,
sortBy,
filteredTemplates,
availableModels,
availableUseCases,
availableRunsOn,
availableLicenses,
filteredCount,
totalCount,
resetFilters
@@ -561,15 +535,15 @@ const selectedUseCaseObjects = computed({
}
})
const selectedRunsOnObjects = computed({
const selectedLicenseObjects = computed({
get() {
return selectedRunsOn.value.map((runsOn) => ({
name: runsOn,
value: runsOn
return selectedLicenses.value.map((license) => ({
name: license,
value: license
}))
},
set(value: { name: string; value: string }[]) {
selectedRunsOn.value = value.map((item) => item.value)
selectedLicenses.value = value.map((item) => item.value)
}
})
@@ -602,10 +576,10 @@ const useCaseOptions = computed(() =>
}))
)
const runsOnOptions = computed(() =>
availableRunsOn.value.map((runsOn) => ({
name: runsOn,
value: runsOn
const licenseOptions = computed(() =>
availableLicenses.value.map((license) => ({
name: license,
value: license
}))
)
@@ -634,14 +608,14 @@ const useCaseFilterLabel = computed(() => {
}
})
const runsOnFilterLabel = computed(() => {
if (selectedRunsOnObjects.value.length === 0) {
return t('templateWorkflows.runsOnFilter', 'Runs On')
} else if (selectedRunsOnObjects.value.length === 1) {
return selectedRunsOnObjects.value[0].name
const licenseFilterLabel = computed(() => {
if (selectedLicenseObjects.value.length === 0) {
return t('templateWorkflows.licenseFilter', 'License')
} else if (selectedLicenseObjects.value.length === 1) {
return selectedLicenseObjects.value[0].name
} else {
return t('templateWorkflows.runsOnSelected', {
count: selectedRunsOnObjects.value.length
return t('templateWorkflows.licensesSelected', {
count: selectedLicenseObjects.value.length
})
}
})
@@ -708,7 +682,7 @@ watch(
sortBy,
selectedModels,
selectedUseCases,
selectedRunsOn
selectedLicenses
],
() => {
resetPagination()
@@ -726,7 +700,6 @@ const onLoadWorkflow = async (template: any) => {
template.name,
getEffectiveSourceModule(template)
)
templateWasSelected.value = true
onClose()
} finally {
loadingTemplate.value = null

View File

@@ -61,7 +61,6 @@ import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useTelemetry } from '@/platform/telemetry'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
@@ -87,33 +86,18 @@ const repoOwner = 'comfyanonymous'
const repoName = 'ComfyUI'
const reportContent = ref('')
const reportOpen = ref(false)
/**
* Open the error report content and track telemetry.
*/
const showReport = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'error_dialog_show_report_clicked'
})
reportOpen.value = true
}
const toast = useToast()
const { t } = useI18n()
const systemStatsStore = useSystemStatsStore()
const telemetry = useTelemetry()
const title = computed<string>(
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
)
/**
* Open contact support flow from error dialog and track telemetry.
*/
const showContactSupport = async () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'help_feedback',
is_external: true,
source: 'error_dialog'
})
await useCommandStore().execute('Comfy.ContactSupport')
}

View File

@@ -96,7 +96,7 @@
<small class="text-center text-muted">
{{ t('auth.apiKey.helpText') }}
<a
:href="`${comfyPlatformBaseUrl}/login`"
:href="`${COMFY_PLATFORM_BASE_URL}/login`"
target="_blank"
class="cursor-pointer text-blue-500"
>
@@ -145,15 +145,11 @@
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import Message from 'primevue/message'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { getComfyPlatformBaseUrl } from '@/config/comfyApi'
import {
configValueOrDefault,
remoteConfig
} from '@/platform/remoteConfig/remoteConfig'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import type { SignInData, SignUpData } from '@/schemas/signInSchema'
import { isHostWhitelisted, normalizeHost } from '@/utils/hostWhitelist'
import { isInChina } from '@/utils/networkUtil'
@@ -172,13 +168,6 @@ const isSecureContext = window.isSecureContext
const isSignIn = ref(true)
const showApiKeyForm = ref(false)
const ssoAllowed = isHostWhitelisted(normalizeHost(window.location.hostname))
const comfyPlatformBaseUrl = computed(() =>
configValueOrDefault(
remoteConfig.value,
'comfy_platform_base_url',
getComfyPlatformBaseUrl()
)
)
const toggleState = () => {
isSignIn.value = !isSignIn.value

View File

@@ -43,10 +43,8 @@ import Tag from 'primevue/tag'
import { onBeforeUnmount, ref } from 'vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useTelemetry } from '@/platform/telemetry'
const authActions = useFirebaseAuthActions()
const telemetry = useTelemetry()
const {
amount,
@@ -63,11 +61,8 @@ const didClickBuyNow = ref(false)
const loading = ref(false)
const handleBuyNow = async () => {
const creditAmount = editable ? customAmount.value : amount
telemetry?.trackApiCreditTopupButtonPurchaseClicked(creditAmount)
loading.value = true
await authActions.purchaseCredits(creditAmount)
await authActions.purchaseCredits(editable ? customAmount.value : amount)
loading.value = false
didClickBuyNow.value = true
}

View File

@@ -11,8 +11,6 @@
import Button from 'primevue/button'
import { computed } from 'vue'
import { useTelemetry } from '@/platform/telemetry'
const props = defineProps<{
errorMessage: string
repoOwner: string
@@ -21,13 +19,7 @@ const props = defineProps<{
const queryString = computed(() => props.errorMessage + ' is:issue')
/**
* Open GitHub issues search and track telemetry.
*/
const openGitHubIssues = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'error_dialog_find_existing_issues_clicked'
})
const query = encodeURIComponent(queryString.value)
const url = `https://github.com/${props.repoOwner}/${props.repoName}/issues?q=${query}`
window.open(url, '_blank')

View File

@@ -15,7 +15,7 @@
<UserCredit text-class="text-3xl font-bold" />
<Skeleton v-if="loading" width="2rem" height="2rem" />
<Button
v-else-if="isActiveSubscription"
v-else
:label="$t('credits.purchaseCredits')"
:loading="loading"
@click="handlePurchaseCreditsClick"
@@ -92,13 +92,6 @@
icon="pi pi-question-circle"
@click="handleFaqClick"
/>
<Button
:label="$t('subscription.partnerNodesCredits')"
text
severity="secondary"
icon="pi pi-question-circle"
@click="handleOpenPartnerNodesInfo"
/>
<Button
:label="$t('credits.messageSupport')"
text
@@ -123,8 +116,6 @@ import { computed, ref, watch } from 'vue'
import UserCredit from '@/components/common/UserCredit.vue'
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
@@ -141,8 +132,6 @@ const dialogService = useDialogService()
const authStore = useFirebaseAuthStore()
const authActions = useFirebaseAuthActions()
const commandStore = useCommandStore()
const telemetry = useTelemetry()
const { isActiveSubscription } = useSubscription()
const loading = computed(() => authStore.loading)
const balanceLoading = computed(() => authStore.isFetchingBalance)
@@ -164,8 +153,6 @@ watch(
)
const handlePurchaseCreditsClick = () => {
// Track purchase credits entry from Settings > Credits panel
useTelemetry()?.trackAddApiCreditButtonClicked()
dialogService.showTopUpCreditsDialog()
}
@@ -174,11 +161,6 @@ const handleCreditsHistoryClick = async () => {
}
const handleMessageSupport = async () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'help_feedback',
is_external: true,
source: 'credits_panel'
})
await commandStore.execute('Comfy.ContactSupport')
}
@@ -186,12 +168,5 @@ const handleFaqClick = () => {
window.open('https://docs.comfy.org/tutorials/api-nodes/faq', '_blank')
}
const handleOpenPartnerNodesInfo = () => {
window.open(
'https://docs.comfy.org/tutorials/api-nodes/overview#api-nodes',
'_blank'
)
}
const creditHistory = ref<CreditHistoryItemData[]>([])
</script>

View File

@@ -96,7 +96,6 @@ import Message from 'primevue/message'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, ref } from 'vue'
import { useTelemetry } from '@/platform/telemetry'
import type { AuditLog } from '@/services/customerEventsService'
import {
EventType,
@@ -160,9 +159,6 @@ const loadEvents = async () => {
if (response.totalPages) {
pagination.value.totalPages = response.totalPages
}
// Check if a pending top-up has completed
useTelemetry()?.checkForCompletedTopup(response.events)
} else {
error.value = customerEventService.error.value || 'Failed to load events'
}

View File

@@ -9,7 +9,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import { getComfyPlatformBaseUrl } from '@/config/comfyApi'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import ApiKeyForm from './ApiKeyForm.vue'
@@ -111,7 +111,7 @@ describe('ApiKeyForm', () => {
const helpText = wrapper.find('small')
expect(helpText.text()).toContain('Need an API key?')
expect(helpText.find('a').attributes('href')).toBe(
`${getComfyPlatformBaseUrl()}/login`
`${COMFY_PLATFORM_BASE_URL}/login`
)
})
})

View File

@@ -48,7 +48,7 @@
<small class="text-muted">
{{ t('auth.apiKey.helpText') }}
<a
:href="`${comfyPlatformBaseUrl}/login`"
:href="`${COMFY_PLATFORM_BASE_URL}/login`"
target="_blank"
class="cursor-pointer text-blue-500"
>
@@ -88,11 +88,7 @@ import Message from 'primevue/message'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { getComfyPlatformBaseUrl } from '@/config/comfyApi'
import {
configValueOrDefault,
remoteConfig
} from '@/platform/remoteConfig/remoteConfig'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import { apiKeySchema } from '@/schemas/signInSchema'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
@@ -100,13 +96,6 @@ import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const authStore = useFirebaseAuthStore()
const apiKeyStore = useApiKeyAuthStore()
const loading = computed(() => authStore.loading)
const comfyPlatformBaseUrl = computed(() =>
configValueOrDefault(
remoteConfig.value,
'comfy_platform_base_url',
getComfyPlatformBaseUrl()
)
)
const { t } = useI18n()

View File

@@ -2,14 +2,14 @@
<Button
ref="buttonRef"
severity="secondary"
class="group h-8 rounded-none! bg-comfy-menu-bg p-0 transition-none! hover:rounded-lg! hover:bg-interface-button-hover-surface!"
class="group h-8 rounded-none! bg-interface-panel-surface p-0 transition-none! hover:rounded-lg! hover:bg-button-hover-surface!"
:style="buttonStyles"
@click="toggle"
>
<template #default>
<div class="flex items-center gap-1 pr-0.5">
<div
class="rounded-lg bg-interface-panel-selected-surface p-2 group-hover:bg-interface-button-hover-surface"
class="rounded-lg bg-button-active-surface p-2 group-hover:bg-button-hover-surface"
>
<i :class="currentModeIcon" class="block h-4 w-4" />
</div>
@@ -114,7 +114,7 @@ const popoverPt = computed(() => ({
content: {
class: [
'mb-2 text-text-primary',
'shadow-lg border border-interface-stroke',
'shadow-lg border border-node-border',
'bg-nav-background',
'rounded-lg',
'p-2 px-3',

View File

@@ -3,10 +3,8 @@
<!-- If load immediately, the top-level splitter stateKey won't be correctly
synced with the stateStorage (localStorage). -->
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady">
<template v-if="showUI" #workflow-tabs>
<TryVueNodeBanner />
<template v-if="showUI && workflowTabsPosition === 'Topbar'" #workflow-tabs>
<div
v-if="workflowTabsPosition === 'Topbar'"
class="workflow-tabs-container pointer-events-auto relative h-9.5 w-full"
>
<!-- Native drag area for Electron -->
@@ -154,8 +152,6 @@ import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isNativeWindow } from '@/utils/envUtil'
import TryVueNodeBanner from '../topbar/TryVueNodeBanner.vue'
const emit = defineEmits<{
ready: []
}>()

View File

@@ -10,10 +10,8 @@
></div>
<ButtonGroup
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-interface-stroke bg-comfy-menu-bg p-2"
:style="{
...stringifiedMinimapStyles.buttonGroupStyles
}"
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
:style="stringifiedMinimapStyles.buttonGroupStyles"
@wheel="canvasInteractions.handleWheel"
>
<CanvasModeSelector
@@ -28,7 +26,7 @@
icon="pi pi-expand"
:aria-label="fitViewTooltip"
:style="stringifiedMinimapStyles.buttonStyles"
class="h-8 w-8 bg-comfy-menu-bg p-0 hover:bg-interface-button-hover-surface!"
class="h-8 w-8 bg-interface-panel-surface p-0 hover:bg-button-hover-surface!"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
>
<template #icon>
@@ -63,7 +61,7 @@
data-testid="toggle-minimap-button"
:style="stringifiedMinimapStyles.buttonStyles"
:class="minimapButtonClass"
@click="onMinimapToggleClick"
@click="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
>
<template #icon>
<i class="icon-[lucide--map] h-4 w-4" />
@@ -84,7 +82,7 @@
:aria-label="linkVisibilityAriaLabel"
data-testid="toggle-link-visibility-button"
:style="stringifiedMinimapStyles.buttonStyles"
@click="onLinkVisibilityToggleClick"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
>
<template #icon>
<i class="icon-[lucide--route-off] h-4 w-4" />
@@ -103,7 +101,6 @@ import { useI18n } from 'vue-i18n'
import { useZoomControls } from '@/composables/useZoomControls'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
@@ -166,18 +163,18 @@ const minimapCommandText = computed(() =>
// Computed properties for button classes and states
const zoomButtonClass = computed(() => [
'bg-comfy-menu-bg',
isModalVisible.value ? 'not-active:bg-interface-panel-selected-surface!' : '',
'hover:bg-interface-button-hover-surface!',
'bg-interface-panel-surface',
isModalVisible.value ? 'not-active:bg-button-active-surface!' : '',
'hover:bg-button-hover-surface!',
'p-0',
'h-8',
'w-15'
])
const minimapButtonClass = computed(() => ({
'bg-comfy-menu-bg': true,
'hover:bg-interface-button-hover-surface!': true,
'not-active:bg-interface-panel-selected-surface!': settingStore.get(
'bg-interface-panel-surface': true,
'hover:bg-button-hover-surface!': true,
'not-active:bg-button-active-surface!': settingStore.get(
'Comfy.Minimap.Visible'
),
'p-0': true,
@@ -209,9 +206,9 @@ const linkVisibilityAriaLabel = computed(() =>
: t('graphCanvasMenu.hideLinks')
)
const linkVisibleClass = computed(() => [
'bg-comfy-menu-bg',
linkHidden.value ? 'not-active:bg-interface-panel-selected-surface!' : '',
'hover:bg-interface-button-hover-surface!',
'bg-interface-panel-surface',
linkHidden.value ? 'not-active:bg-button-active-surface!' : '',
'hover:bg-button-hover-surface!',
'p-0',
'w-8',
'h-8'
@@ -221,26 +218,6 @@ onMounted(() => {
canvasStore.initScaleSync()
})
/**
* Track minimap toggle button click and execute the command.
*/
const onMinimapToggleClick = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'graph_menu_minimap_toggle_clicked'
})
void commandStore.execute('Comfy.Canvas.ToggleMinimap')
}
/**
* Track hide/show links button click and execute the command.
*/
const onLinkVisibilityToggleClick = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'graph_menu_hide_links_toggle_clicked'
})
void commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')
}
onBeforeUnmount(() => {
canvasStore.cleanupScaleSync()
})

View File

@@ -4,7 +4,7 @@
class="absolute right-0 bottom-[62px] z-1300 flex w-[250px] justify-center border-0! bg-inherit!"
>
<div
class="w-4/5 rounded-lg border border-interface-stroke bg-interface-panel-surface p-2 text-text-primary shadow-lg select-none"
class="w-4/5 rounded-lg border border-node-border bg-interface-panel-surface p-2 text-text-primary shadow-lg select-none"
:style="filteredMinimapStyles"
@click.stop
>

View File

@@ -92,7 +92,7 @@ describe('BypassButton', () => {
const button = wrapper.find('button')
expect(button.classes()).not.toContain(
'dark-theme:[&:not(:active)]:!bg-charcoal-600'
'dark-theme:[&:not(:active)]:!bg-[#262729]'
)
})

View File

@@ -7,11 +7,11 @@
severity="secondary"
text
data-testid="bypass-button"
class="hover:bg-secondary-background"
class="hover:bg-[#E7E6E6] hover:dark-theme:bg-charcoal-600"
@click="toggleBypass"
>
<template #icon>
<i class="icon-[lucide--redo-dot] h-4 w-4" />
<i class="icon-[lucide--ban] h-4 w-4" />
</template>
</Button>
</template>

View File

@@ -4,7 +4,7 @@
value: t('selectionToolbox.executeButton.tooltip'),
showDelay: 1000
}"
class="size-8 bg-azure-400 !p-0 dark-theme:bg-azure-600"
class="size-8 bg-[#31B9F4] !p-0 dark-theme:bg-[#0B8CE9]"
text
@mouseenter="() => handleMouseEnter()"
@mouseleave="() => handleMouseLeave()"

View File

@@ -7,7 +7,7 @@
data-testid="info-button"
text
severity="secondary"
@click="onInfoClick"
@click="toggleHelp"
>
<i class="icon-[lucide--info] h-4 w-4" />
</Button>
@@ -17,17 +17,6 @@
import Button from 'primevue/button'
import { useSelectionState } from '@/composables/graph/useSelectionState'
import { useTelemetry } from '@/platform/telemetry'
const { showNodeHelp: toggleHelp } = useSelectionState()
/**
* Track node info button click and toggle node help.
*/
const onInfoClick = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'selection_toolbox_node_info_opened'
})
toggleHelp()
}
</script>

View File

@@ -27,9 +27,9 @@
:severity="option.badge === 'new' ? 'info' : 'secondary'"
:value="t(option.badge)"
:class="{
'rounded-4xl bg-azure-400 dark-theme:bg-azure-600':
'rounded-4xl bg-[#31B9F4] dark-theme:bg-[#0B8CE9]':
option.badge === 'new',
'rounded-4xl bg-slate-100 dark-theme:bg-black':
'rounded-4xl bg-[#9C9EAB] dark-theme:bg-[#000]':
option.badge === 'deprecated',
'h-4 gap-2.5 px-1 text-[9px] text-white uppercase': true
}"

View File

@@ -143,8 +143,8 @@ onMounted(() => {
widget.options.selectOn ?? ['focus', 'click'],
() => {
const lgCanvas = canvasStore.canvas
lgCanvas?.selectNode(widgetState.widget.node)
lgCanvas?.bringToFront(widgetState.widget.node)
lgCanvas?.selectNode(widget.node)
lgCanvas?.bringToFront(widget.node)
}
)
})

View File

@@ -27,33 +27,7 @@ const props = defineProps<{
const executionStore = useExecutionStore()
const isParentNodeExecuting = ref(true)
const formattedText = computed(() => {
const src = modelValue.value
// Turn [[label|url]] into placeholders to avoid interfering with linkifyHtml
const tokens: { label: string; url: string }[] = []
const holed = src.replace(
/\[\[([^|\]]+)\|([^\]]+)\]\]/g,
(_m, label, url) => {
tokens.push({ label: String(label), url: String(url) })
return `__LNK${tokens.length - 1}__`
}
)
// Keep current behavior (auto-link bare URLs + \n -> <br>)
let html = nl2br(linkifyHtml(holed))
// Restore placeholders as <a>...</a> (minimal escaping + http default)
html = html.replace(/__LNK(\d+)__/g, (_m, i) => {
const { label, url } = tokens[+i]
const safeHref = url.replace(/"/g, '&quot;')
const safeLabel = label.replace(/</g, '&lt;').replace(/>/g, '&gt;')
return /^https?:\/\//i.test(url)
? `<a href="${safeHref}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`
: safeLabel
})
return html
})
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
let parentNodeId: NodeId | null = null
onMounted(() => {

View File

@@ -138,14 +138,13 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, nextTick, onMounted, ref } from 'vue'
import type { CSSProperties, Component } from 'vue'
import { useI18n } from 'vue-i18n'
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import type { ReleaseNote } from '@/platform/updates/common/releaseService'
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
import { useCommandStore } from '@/stores/commandStore'
@@ -197,7 +196,6 @@ const { t, locale } = useI18n()
const releaseStore = useReleaseStore()
const commandStore = useCommandStore()
const settingStore = useSettingStore()
const telemetry = useTelemetry()
// Emits
const emit = defineEmits<{
@@ -209,7 +207,6 @@ const isSubmenuVisible = ref(false)
const submenuRef = ref<HTMLElement | null>(null)
const submenuStyle = ref<CSSProperties>({})
let hoverTimeout: number | null = null
const openedAt = ref<number>(Date.now())
// Computed
const hasReleases = computed(() => releaseStore.releases.length > 0)
@@ -229,7 +226,6 @@ const moreItems = computed<MenuItem[]>(() => {
label: t('helpCenter.desktopUserGuide'),
visible: isElectron(),
action: () => {
trackResourceClick('docs', true)
const docsUrl =
electronAPI().getPlatform() === 'darwin'
? EXTERNAL_LINKS.DESKTOP_GUIDE_MACOS
@@ -285,7 +281,6 @@ const menuItems = computed<MenuItem[]>(() => {
icon: 'pi pi-book',
label: t('helpCenter.docs'),
action: () => {
trackResourceClick('docs', true)
openExternalLink(EXTERNAL_LINKS.DOCS)
emit('close')
}
@@ -296,7 +291,6 @@ const menuItems = computed<MenuItem[]>(() => {
icon: 'pi pi-discord',
label: 'Discord',
action: () => {
trackResourceClick('discord', true)
openExternalLink(EXTERNAL_LINKS.DISCORD)
emit('close')
}
@@ -307,7 +301,6 @@ const menuItems = computed<MenuItem[]>(() => {
icon: 'pi pi-github',
label: t('helpCenter.github'),
action: () => {
trackResourceClick('github', true)
openExternalLink(EXTERNAL_LINKS.GITHUB)
emit('close')
}
@@ -318,7 +311,6 @@ const menuItems = computed<MenuItem[]>(() => {
icon: 'pi pi-question-circle',
label: t('helpCenter.helpFeedback'),
action: () => {
trackResourceClick('help_feedback', false)
void commandStore.execute('Comfy.ContactSupport')
emit('close')
}
@@ -334,7 +326,6 @@ const menuItems = computed<MenuItem[]>(() => {
label: t('helpCenter.managerExtension'),
showRedDot: shouldShowManagerRedDot.value,
action: async () => {
trackResourceClick('manager', false)
await useManagerState().openManager({
initialTab: ManagerTab.All,
showToastOnLegacyError: false
@@ -358,23 +349,6 @@ const menuItems = computed<MenuItem[]>(() => {
})
// Utility Functions
const trackResourceClick = (
resourceType:
| 'docs'
| 'discord'
| 'github'
| 'help_feedback'
| 'manager'
| 'release_notes',
isExternal: boolean
): void => {
telemetry?.trackHelpResourceClicked({
resource_type: resourceType,
is_external: isExternal,
source: 'help_center'
})
}
const openExternalLink = (url: string): void => {
window.open(url, '_blank', 'noopener,noreferrer')
}
@@ -530,7 +504,6 @@ const onReinstall = (): void => {
}
const onReleaseClick = (release: ReleaseNote): void => {
trackResourceClick('release_notes', true)
void releaseStore.handleShowChangelog(release.version)
const versionAnchor = formatVersionAnchor(release.version)
const changelogUrl = `${getChangelogUrl()}#${versionAnchor}`
@@ -539,7 +512,6 @@ const onReleaseClick = (release: ReleaseNote): void => {
}
const onUpdate = (_: ReleaseNote): void => {
trackResourceClick('docs', true)
openExternalLink(EXTERNAL_LINKS.UPDATE_GUIDE)
emit('close')
}
@@ -554,16 +526,10 @@ const getChangelogUrl = (): string => {
// Lifecycle
onMounted(async () => {
telemetry?.trackHelpCenterOpened({ source: 'sidebar' })
if (!hasReleases.value) {
await releaseStore.fetchReleases()
}
})
onBeforeUnmount(() => {
const timeSpentSeconds = Math.round((Date.now() - openedAt.value) / 1000)
telemetry?.trackHelpCenterClosed({ time_spent_seconds: timeSpentSeconds })
})
</script>
<style scoped>

View File

@@ -4,11 +4,13 @@
:width="size"
:height="size"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.8193 0.600586C15.1248 0.600586 15.3296 0.70893 15.459 0.881836C15.5914 1.05888 15.6471 1.33774 15.5527 1.66895L14.8037 4.30176C14.7063 4.64386 14.4729 4.97024 14.1641 5.21191C13.8544 5.45415 13.496 5.58984 13.1699 5.58984H13.1689L9.5791 5.59668H7.90625C7.52654 5.59668 7.19496 5.84986 7.09082 6.21289L5.69434 11.0889C5.63007 11.3133 5.66134 11.5534 5.77734 11.7529L5.83203 11.8359C5.99177 12.0491 6.24252 12.1758 6.50977 12.1758H6.51074L8.88281 12.1709H11.4971C11.7643 12.171 11.9541 12.254 12.084 12.3906L12.1357 12.4521C12.2685 12.6295 12.3249 12.9089 12.2305 13.2402L11.4805 15.8721C11.383 16.2144 11.1498 16.5415 10.8408 16.7832C10.5314 17.0252 10.1736 17.161 9.84766 17.1611H9.84668L6.25684 17.168H3.64258C3.33762 17.1679 3.13349 17.0588 3.00391 16.8857C2.87135 16.7087 2.81482 16.43 2.90918 16.0986L3.39551 14.3887C3.46841 14.1327 3.41794 13.8576 3.25879 13.6445V13.6436C3.09901 13.4303 2.84745 13.3037 2.58008 13.3037H1.18066C0.875088 13.3037 0.670398 13.1953 0.541016 13.0225C0.408483 12.8451 0.351891 12.5655 0.446289 12.2344L2.11914 6.38965L2.30371 5.74707V5.74609C2.40139 5.40341 2.63456 5.07671 2.94336 4.83496C3.25302 4.59258 3.61143 4.45705 3.9375 4.45703H5.6123C5.94484 4.45703 6.24083 4.26316 6.37891 3.9707L6.42773 3.83984L6.98145 1.89551C7.07894 1.55317 7.31212 1.22614 7.62109 0.984375C7.93074 0.742127 8.2892 0.606445 8.61523 0.606445H8.61621L12.1982 0.600586H14.8193Z"
v-bind="attributes"
:stroke="color"
stroke-width="1"
/>
</svg>
</template>
@@ -20,18 +22,11 @@ interface Props {
size?: number | string
color?: string
class?: string
mode?: 'outline' | 'fill'
}
const {
size = 16,
color = 'currentColor',
mode = 'outline',
class: className
} = defineProps<Props>()
const iconClass = computed(() => className || '')
const attributes = computed(() => ({
stroke: mode === 'outline' ? color : undefined,
strokeWidth: mode === 'outline' ? 1 : undefined,
fill: mode === 'fill' ? color : 'none'
}))
</script>

View File

@@ -1,48 +1,71 @@
<template>
<div
class="widget-expands relative h-full w-full"
class="relative h-full w-full"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
>
<Load3DScene
v-if="node"
ref="load3DSceneRef"
:initialize-load3d="initializeLoad3d"
:cleanup="cleanup"
:loading="loading"
:loading-message="loadingMessage"
:on-model-drop="isPreview ? undefined : handleModelDrop"
:is-preview="isPreview"
:node="node"
:input-spec="inputSpec"
:background-color="backgroundColor"
:show-grid="showGrid"
:light-intensity="lightIntensity"
:fov="fov"
:camera-type="cameraType"
:show-preview="showPreview"
:background-image="backgroundImage"
:up-direction="upDirection"
:material-mode="materialMode"
:edge-threshold="edgeThreshold"
@material-mode-change="listenMaterialModeChange"
@background-color-change="listenBackgroundColorChange"
@light-intensity-change="listenLightIntensityChange"
@fov-change="listenFOVChange"
@camera-type-change="listenCameraTypeChange"
@show-grid-change="listenShowGridChange"
@show-preview-change="listenShowPreviewChange"
@background-image-change="listenBackgroundImageChange"
@up-direction-change="listenUpDirectionChange"
@edge-threshold-change="listenEdgeThresholdChange"
@recording-status-change="listenRecordingStatusChange"
/>
<Load3DControls
:input-spec="inputSpec"
:background-color="backgroundColor"
:show-grid="showGrid"
:show-preview="showPreview"
:light-intensity="lightIntensity"
:show-light-intensity-button="showLightIntensityButton"
:fov="fov"
:show-f-o-v-button="showFOVButton"
:show-preview-button="showPreviewButton"
:camera-type="cameraType"
:has-background-image="hasBackgroundImage"
:up-direction="upDirection"
:material-mode="materialMode"
:edge-threshold="edgeThreshold"
@update-background-image="handleBackgroundImageUpdate"
@switch-camera="switchCamera"
@toggle-grid="toggleGrid"
@update-background-color="handleBackgroundColorChange"
@update-light-intensity="handleUpdateLightIntensity"
@toggle-preview="togglePreview"
@update-f-o-v="handleUpdateFOV"
@update-up-direction="handleUpdateUpDirection"
@update-material-mode="handleUpdateMaterialMode"
@update-edge-threshold="handleUpdateEdgeThreshold"
@export-model="handleExportModel"
/>
<div class="pointer-events-none absolute top-0 left-0 h-full w-full">
<Load3DControls
v-model:scene-config="sceneConfig"
v-model:model-config="modelConfig"
v-model:camera-config="cameraConfig"
v-model:light-config="lightConfig"
@update-background-image="handleBackgroundImageUpdate"
@export-model="handleExportModel"
/>
<AnimationControls
v-if="animations && animations.length > 0"
v-model:animations="animations"
v-model:playing="playing"
v-model:selected-speed="selectedSpeed"
v-model:selected-animation="selectedAnimation"
/>
</div>
<div
v-if="enable3DViewer && node"
v-if="enable3DViewer"
class="pointer-events-auto absolute top-12 right-2 z-20"
>
<ViewerControls :node="node as LGraphNode" />
<ViewerControls :node="node" />
</div>
<div
v-if="!isPreview"
v-if="showRecordingControls"
class="pointer-events-auto absolute right-2 z-20"
:class="{
'top-12': !enable3DViewer,
@@ -50,9 +73,10 @@
}"
>
<RecordingControls
v-model:is-recording="isRecording"
v-model:has-recording="hasRecording"
v-model:recording-duration="recordingDuration"
:node="node"
:is-recording="isRecording"
:has-recording="hasRecording"
:recording-duration="recordingDuration"
@start-recording="handleStartRecording"
@stop-recording="handleStopRecording"
@export-recording="handleExportRecording"
@@ -63,79 +87,250 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import type { Ref } from 'vue'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
import Load3DScene from '@/components/load3d/Load3DScene.vue'
import AnimationControls from '@/components/load3d/controls/AnimationControls.vue'
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
import ViewerControls from '@/components/load3d/controls/ViewerControls.vue'
import { useLoad3d } from '@/composables/useLoad3d'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import type {
CameraType,
Load3DNodeType,
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import { app } from '@/scripts/app'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComponentWidget } from '@/scripts/domWidget'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
const props = defineProps<{
widget: ComponentWidget<string[]> | SimplifiedWidget
nodeId?: NodeId
const { t } = useI18n()
const { widget } = defineProps<{
widget: ComponentWidget<string[]>
}>()
function isComponentWidget(
widget: ComponentWidget<string[]> | SimplifiedWidget
): widget is ComponentWidget<string[]> {
return 'node' in widget && widget.node !== undefined
}
const inputSpec = widget.inputSpec as CustomInputSpec
const node = ref<LGraphNode | null>(null)
if (isComponentWidget(props.widget)) {
node.value = props.widget.node
} else if (props.nodeId) {
onMounted(() => {
node.value = app.rootGraph?.getNodeById(props.nodeId!) || null
})
}
const node = widget.node
const type = inputSpec.type as Load3DNodeType
const backgroundColor = ref('#000000')
const showGrid = ref(true)
const showPreview = ref(false)
const lightIntensity = ref(5)
const showLightIntensityButton = ref(true)
const fov = ref(75)
const showFOVButton = ref(true)
const cameraType = ref<CameraType>('perspective')
const hasBackgroundImage = ref(false)
const backgroundImage = ref('')
const upDirection = ref<UpDirection>('original')
const materialMode = ref<MaterialMode>('original')
const edgeThreshold = ref(85)
const load3DSceneRef = ref<InstanceType<typeof Load3DScene> | null>(null)
const {
// configs
sceneConfig,
modelConfig,
cameraConfig,
lightConfig,
// other state
isRecording,
isPreview,
hasRecording,
recordingDuration,
animations,
playing,
selectedSpeed,
selectedAnimation,
loading,
loadingMessage,
// Methods
initializeLoad3d,
handleMouseEnter,
handleMouseLeave,
handleStartRecording,
handleStopRecording,
handleExportRecording,
handleClearRecording,
handleBackgroundImageUpdate,
handleExportModel,
handleModelDrop,
cleanup
} = useLoad3d(node as Ref<LGraphNode | null>)
const isRecording = ref(false)
const hasRecording = ref(false)
const recordingDuration = ref(0)
const showRecordingControls = ref(!inputSpec.isPreview)
const enable3DViewer = computed(() =>
useSettingStore().get('Comfy.Load3D.3DViewerEnable')
)
const showPreviewButton = computed(() => {
return !type.includes('Preview')
})
const handleMouseEnter = () => {
if (load3DSceneRef.value?.load3d) {
load3DSceneRef.value.load3d.updateStatusMouseOnScene(true)
}
}
const handleMouseLeave = () => {
if (load3DSceneRef.value?.load3d) {
load3DSceneRef.value.load3d.updateStatusMouseOnScene(false)
}
}
const handleStartRecording = async () => {
if (load3DSceneRef.value?.load3d) {
await load3DSceneRef.value.load3d.startRecording()
isRecording.value = true
}
}
const handleStopRecording = () => {
if (load3DSceneRef.value?.load3d) {
load3DSceneRef.value.load3d.stopRecording()
isRecording.value = false
recordingDuration.value = load3DSceneRef.value.load3d.getRecordingDuration()
hasRecording.value = recordingDuration.value > 0
}
}
const handleExportRecording = () => {
if (load3DSceneRef.value?.load3d) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const filename = `${timestamp}-scene-recording.mp4`
load3DSceneRef.value.load3d.exportRecording(filename)
}
}
const handleClearRecording = () => {
if (load3DSceneRef.value?.load3d) {
load3DSceneRef.value.load3d.clearRecording()
hasRecording.value = false
recordingDuration.value = 0
}
}
const switchCamera = () => {
cameraType.value =
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
showFOVButton.value = cameraType.value === 'perspective'
node.properties['Camera Type'] = cameraType.value
}
const togglePreview = (value: boolean) => {
showPreview.value = value
node.properties['Show Preview'] = showPreview.value
}
const toggleGrid = (value: boolean) => {
showGrid.value = value
node.properties['Show Grid'] = showGrid.value
}
const handleUpdateLightIntensity = (value: number) => {
lightIntensity.value = value
node.properties['Light Intensity'] = lightIntensity.value
}
const handleBackgroundImageUpdate = async (file: File | null) => {
if (!file) {
hasBackgroundImage.value = false
backgroundImage.value = ''
node.properties['Background Image'] = ''
return
}
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim() ? `3d/${resourceFolder.trim()}` : '3d'
backgroundImage.value = await Load3dUtils.uploadFile(file, subfolder)
node.properties['Background Image'] = backgroundImage.value
}
const handleUpdateFOV = (value: number) => {
fov.value = value
node.properties['FOV'] = fov.value
}
const handleUpdateEdgeThreshold = (value: number) => {
edgeThreshold.value = value
node.properties['Edge Threshold'] = edgeThreshold.value
}
const handleBackgroundColorChange = (value: string) => {
backgroundColor.value = value
node.properties['Background Color'] = value
}
const handleUpdateUpDirection = (value: UpDirection) => {
upDirection.value = value
node.properties['Up Direction'] = value
}
const handleUpdateMaterialMode = (value: MaterialMode) => {
materialMode.value = value
node.properties['Material Mode'] = value
}
const handleExportModel = async (format: string) => {
if (!load3DSceneRef.value?.load3d) {
useToastStore().addAlert(t('toastMessages.no3dSceneToExport'))
return
}
try {
await load3DSceneRef.value.load3d.exportModel(format)
} catch (error) {
console.error('Error exporting model:', error)
useToastStore().addAlert(
t('toastMessages.failedToExportModel', {
format: format.toUpperCase()
})
)
}
}
const listenMaterialModeChange = (mode: MaterialMode) => {
materialMode.value = mode
showLightIntensityButton.value = mode === 'original'
}
const listenUpDirectionChange = (value: UpDirection) => {
upDirection.value = value
}
const listenEdgeThresholdChange = (value: number) => {
edgeThreshold.value = value
}
const listenRecordingStatusChange = (value: boolean) => {
isRecording.value = value
if (!value && load3DSceneRef.value?.load3d) {
recordingDuration.value = load3DSceneRef.value.load3d.getRecordingDuration()
hasRecording.value = recordingDuration.value > 0
}
}
const listenBackgroundColorChange = (value: string) => {
backgroundColor.value = value
}
const listenLightIntensityChange = (value: number) => {
lightIntensity.value = value
}
const listenFOVChange = (value: number) => {
fov.value = value
}
const listenCameraTypeChange = (value: CameraType) => {
cameraType.value = value
showFOVButton.value = cameraType.value === 'perspective'
}
const listenShowGridChange = (value: boolean) => {
showGrid.value = value
}
const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
const listenBackgroundImageChange = (value: string) => {
backgroundImage.value = value
if (backgroundImage.value && backgroundImage.value !== '') {
hasBackgroundImage.value = true
}
}
</script>

View File

@@ -0,0 +1,336 @@
<template>
<div
class="relative h-full w-full"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<Load3DAnimationScene
ref="load3DAnimationSceneRef"
:node="node"
:input-spec="inputSpec"
:background-color="backgroundColor"
:show-grid="showGrid"
:light-intensity="lightIntensity"
:fov="fov"
:camera-type="cameraType"
:show-preview="showPreview"
:show-f-o-v-button="showFOVButton"
:show-light-intensity-button="showLightIntensityButton"
:playing="playing"
:selected-speed="selectedSpeed"
:selected-animation="selectedAnimation"
:background-image="backgroundImage"
:up-direction="upDirection"
:material-mode="materialMode"
@material-mode-change="listenMaterialModeChange"
@background-color-change="listenBackgroundColorChange"
@light-intensity-change="listenLightIntensityChange"
@fov-change="listenFOVChange"
@camera-type-change="listenCameraTypeChange"
@show-grid-change="listenShowGridChange"
@show-preview-change="listenShowPreviewChange"
@background-image-change="listenBackgroundImageChange"
@animation-list-change="animationListChange"
@up-direction-change="listenUpDirectionChange"
@recording-status-change="listenRecordingStatusChange"
/>
<div class="pointer-events-none absolute top-0 left-0 h-full w-full">
<Load3DControls
:input-spec="inputSpec"
:background-color="backgroundColor"
:show-grid="showGrid"
:show-preview="showPreview"
:light-intensity="lightIntensity"
:show-light-intensity-button="showLightIntensityButton"
:fov="fov"
:show-f-o-v-button="showFOVButton"
:show-preview-button="showPreviewButton"
:camera-type="cameraType"
:has-background-image="hasBackgroundImage"
:up-direction="upDirection"
:material-mode="materialMode"
@update-background-image="handleBackgroundImageUpdate"
@switch-camera="switchCamera"
@toggle-grid="toggleGrid"
@update-background-color="handleBackgroundColorChange"
@update-light-intensity="handleUpdateLightIntensity"
@toggle-preview="togglePreview"
@update-f-o-v="handleUpdateFOV"
@update-up-direction="handleUpdateUpDirection"
@update-material-mode="handleUpdateMaterialMode"
/>
<Load3DAnimationControls
:animations="animations"
:playing="playing"
@toggle-play="togglePlay"
@speed-change="speedChange"
@animation-change="animationChange"
/>
</div>
<div
v-if="showRecordingControls"
class="pointer-events-auto absolute top-12 right-2 z-20"
>
<RecordingControls
:node="node"
:is-recording="isRecording"
:has-recording="hasRecording"
:recording-duration="recordingDuration"
@start-recording="handleStartRecording"
@stop-recording="handleStopRecording"
@export-recording="handleExportRecording"
@clear-recording="handleClearRecording"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import Load3DAnimationControls from '@/components/load3d/Load3DAnimationControls.vue'
import Load3DAnimationScene from '@/components/load3d/Load3DAnimationScene.vue'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import type {
AnimationItem,
CameraType,
Load3DAnimationNodeType,
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComponentWidget } from '@/scripts/domWidget'
const { widget } = defineProps<{
widget: ComponentWidget<string[]>
}>()
const inputSpec = widget.inputSpec as CustomInputSpec
const node = widget.node
const type = inputSpec.type as Load3DAnimationNodeType
const backgroundColor = ref('#000000')
const showGrid = ref(true)
const showPreview = ref(false)
const lightIntensity = ref(5)
const showLightIntensityButton = ref(true)
const fov = ref(75)
const showFOVButton = ref(true)
const cameraType = ref<'perspective' | 'orthographic'>('perspective')
const hasBackgroundImage = ref(false)
const animations = ref<AnimationItem[]>([])
const playing = ref(false)
const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const backgroundImage = ref('')
const isRecording = ref(false)
const hasRecording = ref(false)
const recordingDuration = ref(0)
const showRecordingControls = ref(!inputSpec.isPreview)
const showPreviewButton = computed(() => {
return !type.includes('Preview')
})
const load3DAnimationSceneRef = ref<InstanceType<
typeof Load3DAnimationScene
> | null>(null)
const handleMouseEnter = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
sceneRef.load3d.updateStatusMouseOnScene(true)
}
}
const handleMouseLeave = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
sceneRef.load3d.updateStatusMouseOnScene(false)
}
}
const handleStartRecording = async () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
await sceneRef.load3d.startRecording()
isRecording.value = true
}
}
const handleStopRecording = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
sceneRef.load3d.stopRecording()
isRecording.value = false
recordingDuration.value = sceneRef.load3d.getRecordingDuration()
hasRecording.value = recordingDuration.value > 0
}
}
const handleExportRecording = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const filename = `${timestamp}-animation-recording.mp4`
sceneRef.load3d.exportRecording(filename)
}
}
const handleClearRecording = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
sceneRef.load3d.clearRecording()
hasRecording.value = false
recordingDuration.value = 0
}
}
const listenRecordingStatusChange = (value: boolean) => {
isRecording.value = value
if (!value) {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
recordingDuration.value = sceneRef.load3d.getRecordingDuration()
hasRecording.value = recordingDuration.value > 0
}
}
}
const switchCamera = () => {
cameraType.value =
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
showFOVButton.value = cameraType.value === 'perspective'
node.properties['Camera Type'] = cameraType.value
}
const togglePreview = (value: boolean) => {
showPreview.value = value
node.properties['Show Preview'] = showPreview.value
}
const toggleGrid = (value: boolean) => {
showGrid.value = value
node.properties['Show Grid'] = showGrid.value
}
const handleUpdateLightIntensity = (value: number) => {
lightIntensity.value = value
node.properties['Light Intensity'] = lightIntensity.value
}
const handleBackgroundImageUpdate = async (file: File | null) => {
if (!file) {
hasBackgroundImage.value = false
backgroundImage.value = ''
node.properties['Background Image'] = ''
return
}
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim() ? `3d/${resourceFolder.trim()}` : '3d'
backgroundImage.value = await Load3dUtils.uploadFile(file, subfolder)
node.properties['Background Image'] = backgroundImage.value
}
const handleUpdateFOV = (value: number) => {
fov.value = value
node.properties['FOV'] = fov.value
}
const materialMode = ref<MaterialMode>('original')
const upDirection = ref<UpDirection>('original')
const handleUpdateUpDirection = (value: UpDirection) => {
upDirection.value = value
node.properties['Up Direction'] = value
}
const handleUpdateMaterialMode = (value: MaterialMode) => {
materialMode.value = value
node.properties['Material Mode'] = value
}
const handleBackgroundColorChange = (value: string) => {
backgroundColor.value = value
node.properties['Background Color'] = value
}
const togglePlay = (value: boolean) => {
playing.value = value
}
const speedChange = (value: number) => {
selectedSpeed.value = value
}
const animationChange = (value: number) => {
selectedAnimation.value = value
}
const animationListChange = (value: any) => {
animations.value = value
}
const listenMaterialModeChange = (mode: MaterialMode) => {
materialMode.value = mode
showLightIntensityButton.value = mode === 'original'
}
const listenUpDirectionChange = (value: UpDirection) => {
upDirection.value = value
}
const listenBackgroundColorChange = (value: string) => {
backgroundColor.value = value
}
const listenLightIntensityChange = (value: number) => {
lightIntensity.value = value
}
const listenFOVChange = (value: number) => {
fov.value = value
}
const listenCameraTypeChange = (value: CameraType) => {
cameraType.value = value
showFOVButton.value = cameraType.value === 'perspective'
}
const listenShowGridChange = (value: boolean) => {
showGrid.value = value
}
const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
const listenBackgroundImageChange = (value: string) => {
backgroundImage.value = value
if (backgroundImage.value && backgroundImage.value !== '') {
hasBackgroundImage.value = true
}
}
</script>

View File

@@ -15,6 +15,7 @@
option-label="name"
option-value="value"
class="w-24"
@change="speedChange"
/>
<Select
@@ -23,6 +24,7 @@
option-label="name"
option-value="index"
class="w-32"
@change="animationChange"
/>
</div>
</template>
@@ -30,13 +32,23 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Select from 'primevue/select'
import { ref, watch } from 'vue'
type Animation = { name: string; index: number }
const props = defineProps<{
animations: Array<{ name: string; index: number }>
playing: boolean
}>()
const animations = defineModel<Animation[]>('animations')
const playing = defineModel<boolean>('playing')
const selectedSpeed = defineModel<number>('selectedSpeed')
const selectedAnimation = defineModel<number>('selectedAnimation')
const emit = defineEmits<{
(e: 'togglePlay', value: boolean): void
(e: 'speedChange', value: number): void
(e: 'animationChange', value: number): void
}>()
const animations = ref(props.animations)
const playing = ref(props.playing)
const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const speedOptions = [
{ name: '0.1x', value: 0.1 },
@@ -46,7 +58,24 @@ const speedOptions = [
{ name: '2x', value: 2 }
]
watch(
() => props.animations,
(newVal) => {
animations.value = newVal
}
)
const togglePlay = () => {
playing.value = !playing.value
emit('togglePlay', playing.value)
}
const speedChange = () => {
emit('speedChange', selectedSpeed.value)
}
const animationChange = () => {
emit('animationChange', selectedAnimation.value)
}
</script>

View File

@@ -0,0 +1,208 @@
<template>
<Load3DScene
ref="load3DSceneRef"
:node="node"
:input-spec="inputSpec"
:background-color="backgroundColor"
:show-grid="showGrid"
:light-intensity="lightIntensity"
:fov="fov"
:camera-type="cameraType"
:show-preview="showPreview"
:extra-listeners="animationListeners"
:background-image="backgroundImage"
:up-direction="upDirection"
:material-mode="materialMode"
@material-mode-change="listenMaterialModeChange"
@background-color-change="listenBackgroundColorChange"
@light-intensity-change="listenLightIntensityChange"
@fov-change="listenFOVChange"
@camera-type-change="listenCameraTypeChange"
@show-grid-change="listenShowGridChange"
@show-preview-change="listenShowPreviewChange"
@recording-status-change="listenRecordingStatusChange"
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import Load3DScene from '@/components/load3d/Load3DScene.vue'
import type Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import type {
CameraType,
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
const props = defineProps<{
node: any
inputSpec: CustomInputSpec
backgroundColor: string
showGrid: boolean
lightIntensity: number
fov: number
cameraType: CameraType
showPreview: boolean
materialMode: MaterialMode
upDirection: UpDirection
showFOVButton: boolean
showLightIntensityButton: boolean
playing: boolean
selectedSpeed: number
selectedAnimation: number
backgroundImage: string
}>()
const node = ref(props.node)
const backgroundColor = ref(props.backgroundColor)
const showPreview = ref(props.showPreview)
const fov = ref(props.fov)
const lightIntensity = ref(props.lightIntensity)
const cameraType = ref(props.cameraType)
const showGrid = ref(props.showGrid)
const upDirection = ref(props.upDirection)
const materialMode = ref(props.materialMode)
const showFOVButton = ref(props.showFOVButton)
const showLightIntensityButton = ref(props.showLightIntensityButton)
const load3DSceneRef = ref<InstanceType<typeof Load3DScene> | null>(null)
watch(
() => props.cameraType,
(newValue) => {
cameraType.value = newValue
}
)
watch(
() => props.showGrid,
(newValue) => {
showGrid.value = newValue
}
)
watch(
() => props.backgroundColor,
(newValue) => {
backgroundColor.value = newValue
}
)
watch(
() => props.lightIntensity,
(newValue) => {
lightIntensity.value = newValue
}
)
watch(
() => props.fov,
(newValue) => {
fov.value = newValue
}
)
watch(
() => props.upDirection,
(newValue) => {
upDirection.value = newValue
}
)
watch(
() => props.materialMode,
(newValue) => {
materialMode.value = newValue
}
)
watch(
() => props.showPreview,
(newValue) => {
showPreview.value = newValue
}
)
watch(
() => props.playing,
(newValue) => {
const load3d = load3DSceneRef.value?.load3d as Load3dAnimation | null
load3d?.toggleAnimation(newValue)
}
)
watch(
() => props.selectedSpeed,
(newValue) => {
const load3d = load3DSceneRef.value?.load3d as Load3dAnimation | null
load3d?.setAnimationSpeed(newValue)
}
)
watch(
() => props.selectedAnimation,
(newValue) => {
const load3d = load3DSceneRef.value?.load3d as Load3dAnimation | null
load3d?.updateSelectedAnimation(newValue)
}
)
const emit = defineEmits<{
(e: 'animationListChange', animationList: string): void
(e: 'materialModeChange', materialMode: MaterialMode): void
(e: 'backgroundColorChange', color: string): void
(e: 'lightIntensityChange', lightIntensity: number): void
(e: 'fovChange', fov: number): void
(e: 'cameraTypeChange', cameraType: CameraType): void
(e: 'showGridChange', showGrid: boolean): void
(e: 'showPreviewChange', showPreview: boolean): void
(e: 'upDirectionChange', direction: UpDirection): void
(e: 'recording-status-change', status: boolean): void
}>()
const listenMaterialModeChange = (mode: MaterialMode) => {
materialMode.value = mode
showLightIntensityButton.value = mode === 'original'
}
const listenBackgroundColorChange = (value: string) => {
backgroundColor.value = value
}
const listenLightIntensityChange = (value: number) => {
lightIntensity.value = value
}
const listenFOVChange = (value: number) => {
fov.value = value
}
const listenCameraTypeChange = (value: CameraType) => {
cameraType.value = value
showFOVButton.value = cameraType.value === 'perspective'
}
const listenShowGridChange = (value: boolean) => {
showGrid.value = value
}
const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
const listenRecordingStatusChange = (value: boolean) => {
emit('recording-status-change', value)
}
const animationListeners = {
animationListChange: (newValue: any) => {
emit('animationListChange', newValue)
}
}
defineExpose({
load3DSceneRef
})
</script>

View File

@@ -1,10 +1,6 @@
<template>
<div
class="pointer-events-auto absolute top-12 left-2 z-20 flex flex-col rounded-lg bg-smoke-700/30"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
@wheel.stop
>
<div class="show-menu relative">
<Button class="p-button-rounded p-button-text" @click="toggleMenu">
@@ -24,9 +20,7 @@
@click="selectCategory(category)"
>
<i :class="getCategoryIcon(category)" />
<span class="whitespace-nowrap text-white">{{
$t(categoryLabels[category])
}}</span>
<span class="text-white">{{ t(categoryLabels[category]) }}</span>
</Button>
</div>
</div>
@@ -34,49 +28,71 @@
<div v-show="activeCategory" class="rounded-lg bg-smoke-700/30">
<SceneControls
v-if="showSceneControls"
v-if="activeCategory === 'scene'"
ref="sceneControlsRef"
v-model:show-grid="sceneConfig!.showGrid"
v-model:background-color="sceneConfig!.backgroundColor"
v-model:background-image="sceneConfig!.backgroundImage"
v-model:background-render-mode="sceneConfig!.backgroundRenderMode"
v-model:fov="cameraConfig!.fov"
:background-color="backgroundColor"
:show-grid="showGrid"
:has-background-image="hasBackgroundImage"
@toggle-grid="handleToggleGrid"
@update-background-color="handleBackgroundColorChange"
@update-background-image="handleBackgroundImageUpdate"
/>
<ModelControls
v-if="showModelControls"
v-if="activeCategory === 'model'"
ref="modelControlsRef"
v-model:material-mode="modelConfig!.materialMode"
v-model:up-direction="modelConfig!.upDirection"
:input-spec="inputSpec"
:up-direction="upDirection"
:material-mode="materialMode"
:edge-threshold="edgeThreshold"
@update-up-direction="handleUpdateUpDirection"
@update-material-mode="handleUpdateMaterialMode"
@update-edge-threshold="handleUpdateEdgeThreshold"
/>
<CameraControls
v-if="showCameraControls"
v-if="activeCategory === 'camera'"
ref="cameraControlsRef"
v-model:camera-type="cameraConfig!.cameraType"
v-model:fov="cameraConfig!.fov"
:camera-type="cameraType"
:fov="fov"
:show-f-o-v-button="showFOVButton"
@switch-camera="switchCamera"
@update-f-o-v="handleUpdateFOV"
/>
<LightControls
v-if="showLightControls"
v-if="activeCategory === 'light'"
ref="lightControlsRef"
v-model:light-intensity="lightConfig!.intensity"
v-model:material-mode="modelConfig!.materialMode"
:light-intensity="lightIntensity"
:show-light-intensity-button="showLightIntensityButton"
@update-light-intensity="handleUpdateLightIntensity"
/>
<ExportControls
v-if="showExportControls"
v-if="activeCategory === 'export'"
ref="exportControlsRef"
@export-model="handleExportModel"
/>
</div>
<div v-if="showPreviewButton">
<Button class="p-button-rounded p-button-text" @click="togglePreview">
<i
v-tooltip.right="{ value: t('load3d.previewOutput'), showDelay: 300 }"
:class="[
'pi',
showPreview ? 'pi-eye' : 'pi-eye-slash',
'text-lg text-white'
]"
/>
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import CameraControls from '@/components/load3d/controls/CameraControls.vue'
import ExportControls from '@/components/load3d/controls/ExportControls.vue'
@@ -84,16 +100,31 @@ import LightControls from '@/components/load3d/controls/LightControls.vue'
import ModelControls from '@/components/load3d/controls/ModelControls.vue'
import SceneControls from '@/components/load3d/controls/SceneControls.vue'
import type {
CameraConfig,
LightConfig,
ModelConfig,
SceneConfig
CameraType,
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
const sceneConfig = defineModel<SceneConfig>('sceneConfig')
const modelConfig = defineModel<ModelConfig>('modelConfig')
const cameraConfig = defineModel<CameraConfig>('cameraConfig')
const lightConfig = defineModel<LightConfig>('lightConfig')
const vTooltip = Tooltip
const props = defineProps<{
inputSpec: CustomInputSpec
backgroundColor: string
showGrid: boolean
showPreview: boolean
lightIntensity: number
showLightIntensityButton: boolean
fov: number
showFOVButton: boolean
showPreviewButton: boolean
cameraType: CameraType
hasBackgroundImage?: boolean
upDirection: UpDirection
materialMode: MaterialMode
edgeThreshold?: number
}>()
const isMenuOpen = ref(false)
const activeCategory = ref<string>('scene')
@@ -106,25 +137,14 @@ const categoryLabels: Record<string, string> = {
}
const availableCategories = computed(() => {
return ['scene', 'model', 'camera', 'light', 'export']
})
const baseCategories = ['scene', 'model', 'camera', 'light']
const showSceneControls = computed(
() => activeCategory.value === 'scene' && !!sceneConfig.value
)
const showModelControls = computed(
() => activeCategory.value === 'model' && !!modelConfig.value
)
const showCameraControls = computed(
() => activeCategory.value === 'camera' && !!cameraConfig.value
)
const showLightControls = computed(
() =>
activeCategory.value === 'light' &&
!!lightConfig.value &&
!!modelConfig.value
)
const showExportControls = computed(() => activeCategory.value === 'export')
if (!props.inputSpec.isAnimation) {
return [...baseCategories, 'export']
}
return baseCategories
})
const toggleMenu = () => {
isMenuOpen.value = !isMenuOpen.value
@@ -148,14 +168,73 @@ const getCategoryIcon = (category: string) => {
}
const emit = defineEmits<{
(e: 'switchCamera'): void
(e: 'toggleGrid', value: boolean): void
(e: 'updateBackgroundColor', color: string): void
(e: 'updateLightIntensity', value: number): void
(e: 'updateFOV', value: number): void
(e: 'togglePreview', value: boolean): void
(e: 'updateBackgroundImage', file: File | null): void
(e: 'updateUpDirection', direction: UpDirection): void
(e: 'updateMaterialMode', mode: MaterialMode): void
(e: 'updateEdgeThreshold', value: number): void
(e: 'exportModel', format: string): void
}>()
const backgroundColor = ref(props.backgroundColor)
const showGrid = ref(props.showGrid)
const showPreview = ref(props.showPreview)
const lightIntensity = ref(props.lightIntensity)
const upDirection = ref(props.upDirection || 'original')
const materialMode = ref(props.materialMode || 'original')
const showLightIntensityButton = ref(props.showLightIntensityButton)
const fov = ref(props.fov)
const showFOVButton = ref(props.showFOVButton)
const showPreviewButton = ref(props.showPreviewButton)
const hasBackgroundImage = ref(props.hasBackgroundImage)
const edgeThreshold = ref(props.edgeThreshold)
const switchCamera = () => {
emit('switchCamera')
}
const togglePreview = () => {
showPreview.value = !showPreview.value
emit('togglePreview', showPreview.value)
}
const handleToggleGrid = (value: boolean) => {
emit('toggleGrid', value)
}
const handleBackgroundColorChange = (value: string) => {
emit('updateBackgroundColor', value)
}
const handleBackgroundImageUpdate = (file: File | null) => {
emit('updateBackgroundImage', file)
}
const handleUpdateUpDirection = (direction: UpDirection) => {
emit('updateUpDirection', direction)
}
const handleUpdateMaterialMode = (mode: MaterialMode) => {
emit('updateMaterialMode', mode)
}
const handleUpdateEdgeThreshold = (value: number) => {
emit('updateEdgeThreshold', value)
}
const handleUpdateLightIntensity = (value: number) => {
emit('updateLightIntensity', value)
}
const handleUpdateFOV = (value: number) => {
emit('updateFOV', value)
}
const handleExportModel = (format: string) => {
emit('exportModel', format)
}
@@ -168,6 +247,101 @@ const closeSlider = (e: MouseEvent) => {
}
}
watch(
() => props.upDirection,
(newValue) => {
if (newValue) {
upDirection.value = newValue
}
}
)
watch(
() => props.backgroundColor,
(newValue) => {
backgroundColor.value = newValue
}
)
watch(
() => props.fov,
(newValue) => {
fov.value = newValue
}
)
watch(
() => props.lightIntensity,
(newValue) => {
lightIntensity.value = newValue
}
)
watch(
() => props.showFOVButton,
(newValue) => {
showFOVButton.value = newValue
}
)
watch(
() => props.showLightIntensityButton,
(newValue) => {
showLightIntensityButton.value = newValue
}
)
watch(
() => props.upDirection,
(newValue) => {
upDirection.value = newValue
}
)
watch(
() => props.materialMode,
(newValue) => {
materialMode.value = newValue
}
)
watch(
() => props.showPreviewButton,
(newValue) => {
showPreviewButton.value = newValue
}
)
watch(
() => props.showPreview,
(newValue) => {
showPreview.value = newValue
}
)
watch(
() => props.hasBackgroundImage,
(newValue) => {
hasBackgroundImage.value = newValue
}
)
watch(
() => props.materialMode,
(newValue) => {
if (newValue) {
materialMode.value = newValue
}
}
)
watch(
() => props.edgeThreshold,
(newValue) => {
edgeThreshold.value = newValue
}
)
onMounted(() => {
document.addEventListener('click', closeSlider)
})

View File

@@ -1,72 +1,238 @@
<template>
<div
ref="container"
class="relative h-full w-full"
data-capture-wheel="true"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
@mousedown.stop
@mousemove.stop
@mouseup.stop
@contextmenu.stop.prevent
@dragover.prevent.stop="handleDragOver"
@dragleave.stop="handleDragLeave"
@drop.prevent.stop="handleDrop"
>
<LoadingOverlay
ref="loadingOverlayRef"
:loading="loading"
:loading-message="loadingMessage"
/>
<div
v-if="!isPreview && isDragging"
class="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
>
<div
class="rounded-lg border-2 border-dashed border-blue-400 bg-blue-500/20 px-6 py-4 text-lg font-medium text-blue-100"
>
{{ dragMessage }}
</div>
</div>
<div ref="container" class="comfy-load-3d relative h-full w-full">
<LoadingOverlay ref="loadingOverlayRef" />
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { onMounted, onUnmounted, ref, toRaw, watch } from 'vue'
import LoadingOverlay from '@/components/load3d/LoadingOverlay.vue'
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
import type Load3d from '@/extensions/core/load3d/Load3d'
import type Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import type {
CameraType,
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { useLoad3dService } from '@/services/load3dService'
const props = defineProps<{
initializeLoad3d: (containerRef: HTMLElement) => Promise<void>
cleanup: () => void
loading: boolean
loadingMessage: string
onModelDrop?: (file: File) => void | Promise<void>
isPreview: boolean
node: LGraphNode
inputSpec: CustomInputSpec
backgroundColor: string
showGrid: boolean
lightIntensity: number
fov: number
cameraType: CameraType
showPreview: boolean
backgroundImage: string
upDirection: UpDirection
materialMode: MaterialMode
edgeThreshold?: number
extraListeners?: Record<string, (value: any) => void>
}>()
const container = ref<HTMLElement | null>(null)
const node = ref(props.node)
const load3d = ref<Load3d | Load3dAnimation | null>(null)
const loadingOverlayRef = ref<InstanceType<typeof LoadingOverlay> | null>(null)
const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
useLoad3dDrag({
onModelDrop: async (file) => {
if (props.onModelDrop) {
await props.onModelDrop(file)
}
},
disabled: computed(() => props.isPreview)
const eventConfig = {
materialModeChange: (value: string) =>
emit('materialModeChange', value as MaterialMode),
backgroundColorChange: (value: string) =>
emit('backgroundColorChange', value),
lightIntensityChange: (value: number) => emit('lightIntensityChange', value),
fovChange: (value: number) => emit('fovChange', value),
cameraTypeChange: (value: string) =>
emit('cameraTypeChange', value as CameraType),
showGridChange: (value: boolean) => emit('showGridChange', value),
showPreviewChange: (value: boolean) => emit('showPreviewChange', value),
backgroundImageChange: (value: string) =>
emit('backgroundImageChange', value),
backgroundImageLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.loadingBackgroundImage')),
backgroundImageLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
upDirectionChange: (value: string) =>
emit('upDirectionChange', value as UpDirection),
edgeThresholdChange: (value: number) => emit('edgeThresholdChange', value),
modelLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.loadingModel')),
modelLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
materialLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.switchingMaterialMode')),
materialLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
exportLoadingStart: (message: string) => {
loadingOverlayRef.value?.startLoading(message || t('load3d.exportingModel'))
},
exportLoadingEnd: () => {
loadingOverlayRef.value?.endLoading()
},
recordingStatusChange: (value: boolean) =>
emit('recordingStatusChange', value)
} as const
watch(
() => props.showPreview,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.togglePreview(newValue)
}
}
)
watch(
() => props.cameraType,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.toggleCamera(newValue)
}
}
)
watch(
() => props.fov,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setFOV(newValue)
}
}
)
watch(
() => props.lightIntensity,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setLightIntensity(newValue)
}
}
)
watch(
() => props.showGrid,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.toggleGrid(newValue)
}
}
)
watch(
() => props.backgroundColor,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setBackgroundColor(newValue)
}
}
)
watch(
() => props.backgroundImage,
async (newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
await rawLoad3d.setBackgroundImage(newValue)
}
}
)
watch(
() => props.upDirection,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setUpDirection(newValue)
}
}
)
watch(
() => props.materialMode,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setMaterialMode(newValue)
}
}
)
watch(
() => props.edgeThreshold,
(newValue) => {
if (load3d.value && newValue) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setEdgeThreshold(newValue)
}
}
)
const emit = defineEmits<{
(e: 'materialModeChange', materialMode: MaterialMode): void
(e: 'backgroundColorChange', color: string): void
(e: 'lightIntensityChange', lightIntensity: number): void
(e: 'fovChange', fov: number): void
(e: 'cameraTypeChange', cameraType: CameraType): void
(e: 'showGridChange', showGrid: boolean): void
(e: 'showPreviewChange', showPreview: boolean): void
(e: 'backgroundImageChange', backgroundImage: string): void
(e: 'upDirectionChange', upDirection: UpDirection): void
(e: 'edgeThresholdChange', threshold: number): void
(e: 'recordingStatusChange', status: boolean): void
}>()
const handleEvents = (action: 'add' | 'remove') => {
if (!load3d.value) return
Object.entries(eventConfig).forEach(([event, handler]) => {
const method = `${action}EventListener` as const
load3d.value?.[method](event, handler)
})
if (props.extraListeners) {
Object.entries(props.extraListeners).forEach(([event, handler]) => {
const method = `${action}EventListener` as const
load3d.value?.[method](event, handler)
})
}
}
onMounted(() => {
if (container.value) {
void props.initializeLoad3d(container.value)
load3d.value = useLoad3dService().registerLoad3d(
node.value as LGraphNode,
container.value,
props.inputSpec
)
}
handleEvents('add')
})
onUnmounted(() => {
props.cleanup()
handleEvents('remove')
useLoad3dService().removeLoad3d(node.value as LGraphNode)
})
defineExpose({
load3d
})
</script>

View File

@@ -9,22 +9,9 @@
<div ref="mainContentRef" class="relative flex-1">
<div
ref="containerRef"
class="absolute h-full w-full"
class="comfy-load-3d-viewer absolute h-full w-full"
@resize="viewer.handleResize"
@dragover.prevent.stop="handleDragOver"
@dragleave.stop="handleDragLeave"
@drop.prevent.stop="handleDrop"
/>
<div
v-if="isDragging"
class="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
>
<div
class="rounded-lg border-2 border-dashed border-blue-400 bg-blue-500/20 px-6 py-4 text-lg font-medium text-blue-100"
>
{{ dragMessage }}
</div>
</div>
</div>
<div class="flex w-72 flex-col">
@@ -34,8 +21,6 @@
<SceneControls
v-model:background-color="viewer.backgroundColor.value"
v-model:show-grid="viewer.showGrid.value"
v-model:background-render-mode="viewer.backgroundRenderMode.value"
v-model:fov="viewer.fov.value"
:has-background-image="viewer.hasBackgroundImage.value"
@update-background-image="viewer.handleBackgroundImageUpdate"
/>
@@ -90,7 +75,6 @@ import ExportControls from '@/components/load3d/controls/viewer/ViewerExportCont
import LightControls from '@/components/load3d/controls/viewer/ViewerLightControls.vue'
import ModelControls from '@/components/load3d/controls/viewer/ViewerModelControls.vue'
import SceneControls from '@/components/load3d/controls/viewer/ViewerSceneControls.vue'
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { useLoad3dService } from '@/services/load3dService'
@@ -108,14 +92,6 @@ const mutationObserver = ref<MutationObserver | null>(null)
const viewer = useLoad3dService().getOrCreateViewer(toRaw(props.node))
const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
useLoad3dDrag({
onModelDrop: async (file) => {
await viewer.handleModelDrop(file)
},
disabled: viewer.isPreview
})
onMounted(async () => {
const source = useLoad3dService().getLoad3d(props.node)
if (source && containerRef.value) {

View File

@@ -1,8 +1,8 @@
<template>
<Transition name="fade">
<div
v-if="loading"
class="bg-opacity-50 absolute inset-0 z-50 flex items-center justify-center bg-black"
v-if="modelLoading"
class="absolute inset-0 z-50 flex items-center justify-center bg-black/50"
>
<div class="flex flex-col items-center">
<div class="spinner" />
@@ -15,10 +15,29 @@
</template>
<script setup lang="ts">
defineProps<{
loading: boolean
loadingMessage: string
}>()
import { nextTick, ref } from 'vue'
import { t } from '@/i18n'
const modelLoading = ref(false)
const loadingMessage = ref('')
const startLoading = async (message?: string) => {
loadingMessage.value = message || t('load3d.loadingModel')
modelLoading.value = true
await nextTick()
}
const endLoading = async () => {
await new Promise((resolve) => setTimeout(resolve, 100))
modelLoading.value = false
}
defineExpose({
startLoading,
endLoading
})
</script>
<style scoped>

View File

@@ -3,33 +3,114 @@
<Button class="p-button-rounded p-button-text" @click="switchCamera">
<i
v-tooltip.right="{
value: $t('load3d.switchCamera'),
value: t('load3d.switchCamera'),
showDelay: 300
}"
:class="['pi', 'pi-camera', 'text-lg text-white']"
:class="['pi', getCameraIcon, 'text-lg text-white']"
/>
</Button>
<PopupSlider
v-if="showFOVButton"
v-model="fov"
:tooltip-text="$t('load3d.fov')"
/>
<div v-if="showFOVButton" class="show-fov relative">
<Button class="p-button-rounded p-button-text" @click="toggleFOV">
<i
v-tooltip.right="{ value: t('load3d.fov'), showDelay: 300 }"
class="pi pi-expand text-lg text-white"
/>
</Button>
<div
v-show="showFOV"
class="absolute top-0 left-12 rounded-lg bg-black/50 p-4 shadow-lg"
style="width: 150px"
>
<Slider
v-model="fov"
class="w-full"
:min="10"
:max="150"
:step="1"
@change="updateFOV"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import { computed } from 'vue'
import Slider from 'primevue/slider'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import PopupSlider from '@/components/load3d/controls/PopupSlider.vue'
import type { CameraType } from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
const cameraType = defineModel<CameraType>('cameraType')
const fov = defineModel<number>('fov')
const showFOVButton = computed(() => cameraType.value === 'perspective')
const vTooltip = Tooltip
const props = defineProps<{
cameraType: CameraType
fov: number
showFOVButton: boolean
}>()
const emit = defineEmits<{
(e: 'switchCamera'): void
(e: 'updateFOV', value: number): void
}>()
const cameraType = ref(props.cameraType)
const fov = ref(props.fov)
const showFOVButton = ref(props.showFOVButton)
const showFOV = ref(false)
watch(
() => props.fov,
(newValue) => {
fov.value = newValue
}
)
watch(
() => props.showFOVButton,
(newValue) => {
showFOVButton.value = newValue
}
)
watch(
() => props.cameraType,
(newValue) => {
cameraType.value = newValue
}
)
const switchCamera = () => {
cameraType.value =
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
emit('switchCamera')
}
const toggleFOV = () => {
showFOV.value = !showFOV.value
}
const updateFOV = () => {
emit('updateFOV', fov.value)
}
const getCameraIcon = computed(() => {
return props.cameraType === 'perspective' ? 'pi-camera' : 'pi-camera'
})
const closeCameraSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('.show-fov')) {
showFOV.value = false
}
}
onMounted(() => {
document.addEventListener('click', closeCameraSlider)
})
onUnmounted(() => {
document.removeEventListener('click', closeCameraSlider)
})
</script>

View File

@@ -7,7 +7,7 @@
>
<i
v-tooltip.right="{
value: $t('load3d.exportModel'),
value: t('load3d.exportModel'),
showDelay: 300
}"
class="pi pi-download text-lg text-white"
@@ -33,9 +33,14 @@
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import { onMounted, onUnmounted, ref } from 'vue'
import { t } from '@/i18n'
const vTooltip = Tooltip
const emit = defineEmits<{
(e: 'exportModel', format: string): void
}>()

View File

@@ -7,7 +7,7 @@
>
<i
v-tooltip.right="{
value: $t('load3d.lightIntensity'),
value: t('load3d.lightIntensity'),
showDelay: 300
}"
class="pi pi-sun text-lg text-white"
@@ -24,6 +24,7 @@
:min="lightIntensityMinimum"
:max="lightIntensityMaximum"
:step="lightAdjustmentIncrement"
@change="updateLightIntensity"
/>
</div>
</div>
@@ -31,19 +32,27 @@
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import Slider from 'primevue/slider'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import type { MaterialMode } from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
const lightIntensity = defineModel<number>('lightIntensity')
const materialMode = defineModel<MaterialMode>('materialMode')
const vTooltip = Tooltip
const showLightIntensityButton = computed(
() => materialMode.value === 'original'
)
const props = defineProps<{
lightIntensity: number
showLightIntensityButton: boolean
}>()
const emit = defineEmits<{
(e: 'updateLightIntensity', value: number): void
}>()
const lightIntensity = ref(props.lightIntensity)
const showLightIntensityButton = ref(props.showLightIntensityButton)
const showLightIntensity = ref(false)
const lightIntensityMaximum = useSettingStore().get(
@@ -56,10 +65,28 @@ const lightAdjustmentIncrement = useSettingStore().get(
'Comfy.Load3D.LightAdjustmentIncrement'
)
watch(
() => props.lightIntensity,
(newValue) => {
lightIntensity.value = newValue
}
)
watch(
() => props.showLightIntensityButton,
(newValue) => {
showLightIntensityButton.value = newValue
}
)
const toggleLightIntensity = () => {
showLightIntensity.value = !showLightIntensity.value
}
const updateLightIntensity = () => {
emit('updateLightIntensity', lightIntensity.value)
}
const closeLightSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement

View File

@@ -22,7 +22,7 @@
:class="{ 'bg-blue-500': upDirection === direction }"
@click="selectUpDirection(direction)"
>
{{ direction.toUpperCase() }}
{{ formatOption(direction) }}
</Button>
</div>
</div>
@@ -49,7 +49,7 @@
<Button
v-for="mode in materialModes"
:key="mode"
class="p-button-text whitespace-nowrap text-white"
class="p-button-text text-white"
:class="{ 'bg-blue-500': materialMode === mode }"
@click="selectMaterialMode(mode)"
>
@@ -58,24 +58,75 @@
</div>
</div>
</div>
<div v-if="materialMode === 'lineart'" class="show-edge-threshold relative">
<Button
class="p-button-rounded p-button-text"
@click="toggleEdgeThreshold"
>
<i
v-tooltip.right="{
value: t('load3d.edgeThreshold'),
showDelay: 300
}"
class="pi pi-sliders-h text-lg text-white"
/>
</Button>
<div
v-show="showEdgeThreshold"
class="absolute top-0 left-12 rounded-lg bg-black/50 p-4 shadow-lg"
style="width: 150px"
>
<label class="mb-1 block text-xs text-white"
>{{ t('load3d.edgeThreshold') }}: {{ edgeThreshold }}°</label
>
<Slider
v-model="edgeThreshold"
class="w-full"
:min="0"
:max="120"
:step="1"
@change="updateEdgeThreshold"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import Slider from 'primevue/slider'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import type {
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
const materialMode = defineModel<MaterialMode>('materialMode')
const upDirection = defineModel<UpDirection>('upDirection')
const vTooltip = Tooltip
const props = defineProps<{
inputSpec: CustomInputSpec
upDirection: UpDirection
materialMode: MaterialMode
edgeThreshold?: number
}>()
const emit = defineEmits<{
(e: 'updateUpDirection', direction: UpDirection): void
(e: 'updateMaterialMode', mode: MaterialMode): void
(e: 'updateEdgeThreshold', value: number): void
}>()
const upDirection = ref(props.upDirection || 'original')
const materialMode = ref(props.materialMode || 'original')
const edgeThreshold = ref(props.edgeThreshold || 85)
const showUpDirection = ref(false)
const showMaterialMode = ref(false)
const showEdgeThreshold = ref(false)
const upDirections: UpDirection[] = [
'original',
@@ -95,26 +146,65 @@ const materialModes = computed(() => {
//'depth' disable for now
]
if (!props.inputSpec.isAnimation && !props.inputSpec.isPreview) {
modes.push('lineart')
}
return modes
})
watch(
() => props.upDirection,
(newValue) => {
if (newValue) {
upDirection.value = newValue
}
}
)
watch(
() => props.materialMode,
(newValue) => {
if (newValue) {
materialMode.value = newValue
}
}
)
watch(
() => props.edgeThreshold,
(newValue) => {
// @ts-expect-error fixme ts strict error
edgeThreshold.value = newValue
}
)
const toggleUpDirection = () => {
showUpDirection.value = !showUpDirection.value
showMaterialMode.value = false
showEdgeThreshold.value = false
}
const selectUpDirection = (direction: UpDirection) => {
upDirection.value = direction
emit('updateUpDirection', direction)
showUpDirection.value = false
}
const formatOption = (option: string) => {
if (option === 'original') return 'Original'
return option.toUpperCase()
}
const toggleMaterialMode = () => {
showMaterialMode.value = !showMaterialMode.value
showUpDirection.value = false
showEdgeThreshold.value = false
}
const selectMaterialMode = (mode: MaterialMode) => {
materialMode.value = mode
emit('updateMaterialMode', mode)
showMaterialMode.value = false
}
@@ -122,6 +212,16 @@ const formatMaterialMode = (mode: MaterialMode) => {
return t(`load3d.materialModes.${mode}`)
}
const toggleEdgeThreshold = () => {
showEdgeThreshold.value = !showEdgeThreshold.value
showUpDirection.value = false
showMaterialMode.value = false
}
const updateEdgeThreshold = () => {
emit('updateEdgeThreshold', edgeThreshold.value)
}
const closeSceneSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement
@@ -132,6 +232,10 @@ const closeSceneSlider = (e: MouseEvent) => {
if (!target.closest('.show-material-mode')) {
showMaterialMode.value = false
}
if (!target.closest('.show-edge-threshold')) {
showEdgeThreshold.value = false
}
}
onMounted(() => {

View File

@@ -1,64 +0,0 @@
<template>
<div class="relative show-slider">
<Button class="p-button-rounded p-button-text" @click="toggleSlider">
<i
v-tooltip.right="{ value: tooltipText, showDelay: 300 }"
:class="['pi', icon, 'text-lg text-white']"
/>
</Button>
<div
v-show="showSlider"
class="absolute top-0 left-12 rounded-lg bg-black/50 p-4 shadow-lg w-[150px]"
>
<Slider
v-model="value"
class="w-full"
:min="min"
:max="max"
:step="step"
/>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Slider from 'primevue/slider'
import { onMounted, onUnmounted, ref } from 'vue'
const {
icon = 'pi-expand',
min = 10,
max = 150,
step = 1
} = defineProps<{
icon?: string
tooltipText: string
min?: number
max?: number
step?: number
}>()
const value = defineModel<number>()
const showSlider = ref(false)
const toggleSlider = () => {
showSlider.value = !showSlider.value
}
const closeSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('.show-slider')) {
showSlider.value = false
}
}
onMounted(() => {
document.addEventListener('click', closeSlider)
})
onUnmounted(() => {
document.removeEventListener('click', closeSlider)
})
</script>

View File

@@ -1,6 +1,18 @@
<template>
<div class="relative rounded-lg bg-smoke-700/30">
<div class="flex flex-col gap-2">
<Button
class="p-button-rounded p-button-text"
@click="resizeNodeMatchOutput"
>
<i
v-tooltip.right="{
value: t('load3d.resizeNodeMatchOutput'),
showDelay: 300
}"
class="pi pi-window-maximize text-lg text-white"
/>
</Button>
<Button
class="p-button-rounded p-button-text"
:class="{
@@ -12,8 +24,8 @@
<i
v-tooltip.right="{
value: isRecording
? $t('load3d.stopRecording')
: $t('load3d.startRecording'),
? t('load3d.stopRecording')
: t('load3d.startRecording'),
showDelay: 300
}"
:class="[
@@ -27,11 +39,11 @@
<Button
v-if="hasRecording && !isRecording"
class="p-button-rounded p-button-text"
@click="handleExportRecording"
@click="exportRecording"
>
<i
v-tooltip.right="{
value: $t('load3d.exportRecording'),
value: t('load3d.exportRecording'),
showDelay: 300
}"
class="pi pi-download text-lg text-white"
@@ -41,11 +53,11 @@
<Button
v-if="hasRecording && !isRecording"
class="p-button-rounded p-button-text"
@click="handleClearRecording"
@click="clearRecording"
>
<i
v-tooltip.right="{
value: $t('load3d.clearRecording'),
value: t('load3d.clearRecording'),
showDelay: 300
}"
class="pi pi-trash text-lg text-white"
@@ -53,7 +65,7 @@
</Button>
<div
v-if="recordingDuration && recordingDuration > 0 && !isRecording"
v-if="recordingDuration > 0 && !isRecording"
class="mt-1 text-center text-xs text-white"
>
{{ formatDuration(recordingDuration) }}
@@ -63,11 +75,21 @@
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
const hasRecording = defineModel<boolean>('hasRecording')
const isRecording = defineModel<boolean>('isRecording')
const recordingDuration = defineModel<number>('recordingDuration')
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useLoad3dService } from '@/services/load3dService'
const vTooltip = Tooltip
const { hasRecording, isRecording, node, recordingDuration } = defineProps<{
hasRecording: boolean
isRecording: boolean
node: LGraphNode
recordingDuration: number
}>()
const emit = defineEmits<{
(e: 'startRecording'): void
@@ -76,19 +98,49 @@ const emit = defineEmits<{
(e: 'clearRecording'): void
}>()
const resizeNodeMatchOutput = () => {
const outputWidth = node.widgets?.find((w) => w.name === 'width')
const outputHeight = node.widgets?.find((w) => w.name === 'height')
if (outputWidth && outputHeight && outputHeight.value && outputWidth.value) {
const [oldWidth, oldHeight] = node.size
const scene = node.widgets?.find((w) => w.name === 'image')
const sceneHeight = scene?.computedHeight
if (sceneHeight) {
const sceneWidth = oldWidth - 20
const outputRatio = Number(outputHeight.value) / Number(outputWidth.value)
const expectSceneHeight = sceneWidth * outputRatio
node.setSize([oldWidth, oldHeight + (expectSceneHeight - sceneHeight)])
node.graph?.setDirtyCanvas(true, true)
const load3d = useLoad3dService().getLoad3d(node as LGraphNode)
if (load3d) {
load3d.refreshViewport()
}
}
}
}
const toggleRecording = () => {
if (isRecording.value) {
if (isRecording) {
emit('stopRecording')
} else {
emit('startRecording')
}
}
const handleExportRecording = () => {
const exportRecording = () => {
emit('exportRecording')
}
const handleClearRecording = () => {
const clearRecording = () => {
emit('clearRecording')
}

View File

@@ -6,7 +6,7 @@
@click="toggleGrid"
>
<i
v-tooltip.right="{ value: $t('load3d.showGrid'), showDelay: 300 }"
v-tooltip.right="{ value: t('load3d.showGrid'), showDelay: 300 }"
class="pi pi-table text-lg text-white"
/>
</Button>
@@ -15,7 +15,7 @@
<Button class="p-button-rounded p-button-text" @click="openColorPicker">
<i
v-tooltip.right="{
value: $t('load3d.backgroundColor'),
value: t('load3d.backgroundColor'),
showDelay: 300
}"
class="pi pi-palette text-lg text-white"
@@ -36,7 +36,7 @@
<Button class="p-button-rounded p-button-text" @click="openImagePicker">
<i
v-tooltip.right="{
value: $t('load3d.uploadBackgroundImage'),
value: t('load3d.uploadBackgroundImage'),
showDelay: 300
}"
class="pi pi-image text-lg text-white"
@@ -51,28 +51,6 @@
</Button>
</div>
<div v-if="hasBackgroundImage">
<Button
class="p-button-rounded p-button-text"
:class="{ 'p-button-outlined': backgroundRenderMode === 'panorama' }"
@click="toggleBackgroundRenderMode"
>
<i
v-tooltip.right="{
value: $t('load3d.panoramaMode'),
showDelay: 300
}"
class="pi pi-globe text-lg text-white"
/>
</Button>
</div>
<PopupSlider
v-if="hasBackgroundImage && backgroundRenderMode === 'panorama'"
v-model="fov"
:tooltip-text="$t('load3d.fov')"
/>
<div v-if="hasBackgroundImage">
<Button
class="p-button-rounded p-button-text"
@@ -80,7 +58,7 @@
>
<i
v-tooltip.right="{
value: $t('load3d.removeBackgroundImage'),
value: t('load3d.removeBackgroundImage'),
showDelay: 300
}"
class="pi pi-times text-lg text-white"
@@ -91,37 +69,60 @@
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import { computed, ref } from 'vue'
import { ref, watch } from 'vue'
import PopupSlider from '@/components/load3d/controls/PopupSlider.vue'
import type { BackgroundRenderModeType } from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
const vTooltip = Tooltip
const props = defineProps<{
backgroundColor: string
showGrid: boolean
hasBackgroundImage?: boolean
}>()
const emit = defineEmits<{
(e: 'toggleGrid', value: boolean): void
(e: 'updateBackgroundColor', color: string): void
(e: 'updateBackgroundImage', file: File | null): void
}>()
const showGrid = defineModel<boolean>('showGrid')
const backgroundColor = defineModel<string>('backgroundColor')
const backgroundImage = defineModel<string>('backgroundImage')
const backgroundRenderMode = defineModel<BackgroundRenderModeType>(
'backgroundRenderMode',
{ default: 'tiled' }
)
const fov = defineModel<number>('fov')
const hasBackgroundImage = computed(
() => backgroundImage.value && backgroundImage.value !== ''
)
const backgroundColor = ref(props.backgroundColor)
const showGrid = ref(props.showGrid)
const hasBackgroundImage = ref(props.hasBackgroundImage)
const colorPickerRef = ref<HTMLInputElement | null>(null)
const imagePickerRef = ref<HTMLInputElement | null>(null)
watch(
() => props.backgroundColor,
(newValue) => {
backgroundColor.value = newValue
}
)
watch(
() => props.showGrid,
(newValue) => {
showGrid.value = newValue
}
)
watch(
() => props.hasBackgroundImage,
(newValue) => {
hasBackgroundImage.value = newValue
}
)
const toggleGrid = () => {
showGrid.value = !showGrid.value
emit('toggleGrid', showGrid.value)
}
const updateBackgroundColor = (color: string) => {
backgroundColor.value = color
emit('updateBackgroundColor', color)
}
const openColorPicker = () => {
@@ -143,9 +144,4 @@ const uploadBackgroundImage = (event: Event) => {
const removeBackgroundImage = () => {
emit('updateBackgroundImage', null)
}
const toggleBackgroundRenderMode = () => {
backgroundRenderMode.value =
backgroundRenderMode.value === 'panorama' ? 'tiled' : 'panorama'
}
</script>

Some files were not shown because too many files have changed in this diff Show More