Compare commits
18 Commits
feat/nativ
...
v1.47.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99a2320a42 | ||
|
|
2b0bcda41f | ||
|
|
b9112f9bd7 | ||
|
|
b5b124fa9e | ||
|
|
e138d17459 | ||
|
|
f212c7d409 | ||
|
|
1d5801d6ef | ||
|
|
193f23e8c2 | ||
|
|
eaa6776559 | ||
|
|
afd42525fe | ||
|
|
0c392e53a2 | ||
|
|
46526cfabd | ||
|
|
fefbe7843c | ||
|
|
6445690ed3 | ||
|
|
603914e78f | ||
|
|
c7797b201e | ||
|
|
aa68573a6e | ||
|
|
79acf7be5e |
@@ -65,6 +65,7 @@
|
||||
],
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-self-assign": "allow",
|
||||
"no-unreachable": "error",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-private-class-members": "off",
|
||||
"no-useless-rename": "off",
|
||||
@@ -73,12 +74,14 @@
|
||||
"import/namespace": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
|
||||
"jest/expect-expect": "off",
|
||||
"jest/no-conditional-expect": "off",
|
||||
"jest/no-disabled-tests": "off",
|
||||
"jest/no-standalone-expect": "off",
|
||||
"jest/valid-title": "off",
|
||||
"vitest/expect-expect": "off",
|
||||
"vitest/no-conditional-expect": "off",
|
||||
"vitest/no-disabled-tests": "off",
|
||||
"vitest/no-standalone-expect": "off",
|
||||
"vitest/valid-title": "off",
|
||||
"vitest/require-to-throw-message": "off",
|
||||
"typescript/no-this-alias": "off",
|
||||
"typescript/no-useless-default-assignment": "off",
|
||||
"typescript/no-unnecessary-parameter-property-assignment": "off",
|
||||
"typescript/no-unsafe-declaration-merging": "off",
|
||||
"typescript/no-unused-vars": "off",
|
||||
|
||||
@@ -15,7 +15,9 @@ test.describe('Download page @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Download Comfy — Run AI Locally')
|
||||
await expect(page).toHaveTitle(
|
||||
'Download Comfy Desktop — Run AI on Your Hardware'
|
||||
)
|
||||
})
|
||||
|
||||
test('CloudBannerSection is visible with cloud link', async ({ page }) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 56 KiB |
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "Comfy",
|
||||
"short_name": "Comfy",
|
||||
"id": "/",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#211927",
|
||||
"background_color": "#211927",
|
||||
"display": "standalone"
|
||||
"display": "standalone",
|
||||
"id": "/",
|
||||
"start_url": "/"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 8.8 KiB |
@@ -21,7 +21,7 @@ export const Default: Story = {
|
||||
args: {
|
||||
title: 'Product',
|
||||
links: [
|
||||
{ label: 'Local', href: '/local' },
|
||||
{ label: 'Desktop', href: '/download' },
|
||||
{ label: 'Cloud', href: '/cloud' },
|
||||
{ label: 'API', href: '/api' },
|
||||
{ label: 'Enterprise', href: '/enterprise' }
|
||||
|
||||
@@ -12,9 +12,9 @@ const meta: Meta<typeof ProductCard> = {
|
||||
})
|
||||
],
|
||||
args: {
|
||||
title: 'Comfy\nLocal',
|
||||
title: 'Comfy\nDesktop',
|
||||
description: 'Run ComfyUI on your own hardware.',
|
||||
cta: 'SEE LOCAL FEATURES',
|
||||
cta: 'SEE DESKTOP FEATURES',
|
||||
href: '#',
|
||||
bg: 'bg-primary-warm-gray'
|
||||
}
|
||||
@@ -31,9 +31,9 @@ export const AllCards: Story = {
|
||||
template: `
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<ProductCard
|
||||
title="Comfy\nLocal"
|
||||
title="Comfy\nDesktop"
|
||||
description="Run ComfyUI on your own hardware."
|
||||
cta="SEE LOCAL FEATURES"
|
||||
cta="SEE DESKTOP FEATURES"
|
||||
href="#"
|
||||
bg="bg-primary-warm-gray"
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Locale } from '../../../i18n/translations'
|
||||
import { computed } from 'vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import type { Platform } from '../../../composables/useDownloadUrl'
|
||||
import {
|
||||
downloadUrls,
|
||||
useDownloadUrl
|
||||
@@ -18,13 +19,15 @@ const { locale = 'en', class: customClass = '' } = defineProps<{
|
||||
|
||||
const { downloadUrl, platform, showFallback } = useDownloadUrl()
|
||||
|
||||
const ICONS = {
|
||||
const label = computed(() => t('download.hero.downloadLocal', locale))
|
||||
|
||||
const ICONS: Record<Platform, string> = {
|
||||
windows: '/icons/os/windows.svg',
|
||||
mac: '/icons/os/apple.svg'
|
||||
} as const
|
||||
}
|
||||
|
||||
interface ButtonSpec {
|
||||
key: string
|
||||
key: Platform
|
||||
href: string
|
||||
icon: string
|
||||
ariaLabel?: string
|
||||
@@ -41,19 +44,18 @@ const buttons = computed<ButtonSpec[]>(() => {
|
||||
]
|
||||
}
|
||||
if (showFallback.value) {
|
||||
const label = t('download.hero.downloadLocal', locale)
|
||||
return [
|
||||
{
|
||||
key: 'windows',
|
||||
href: downloadUrls.windows,
|
||||
icon: ICONS.windows,
|
||||
ariaLabel: `${label} — Windows`
|
||||
ariaLabel: `${label.value} — Windows`
|
||||
},
|
||||
{
|
||||
key: 'mac',
|
||||
href: downloadUrls.macArm,
|
||||
icon: ICONS.mac,
|
||||
ariaLabel: `${label} — macOS`
|
||||
ariaLabel: `${label.value} — macOS`
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -77,11 +79,8 @@ const buttons = computed<ButtonSpec[]>(() => {
|
||||
:src="btn.icon"
|
||||
alt=""
|
||||
class="ppformula-text-center size-5 -translate-y-0.75"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="ppformula-text-center">{{
|
||||
t('download.hero.downloadLocal', locale)
|
||||
}}</span>
|
||||
<span class="ppformula-text-center">{{ label }}</span>
|
||||
</span>
|
||||
</BrandButton>
|
||||
</template>
|
||||
|
||||
@@ -7,13 +7,13 @@ export const downloadUrls = {
|
||||
macArm: 'https://download.comfy.org/mac/dmg/arm64'
|
||||
} as const
|
||||
|
||||
type DetectedPlatform = 'windows' | 'mac' | null
|
||||
export type Platform = 'windows' | 'mac'
|
||||
|
||||
function isMobile(ua: string): boolean {
|
||||
return /iphone|ipad|ipod|android/.test(ua)
|
||||
}
|
||||
|
||||
function detectPlatform(ua: string): DetectedPlatform {
|
||||
function detectPlatform(ua: string): Platform | null {
|
||||
if (isMobile(ua)) return null
|
||||
if (ua.includes('win')) return 'windows'
|
||||
if (ua.includes('macintosh') || ua.includes('mac os x')) return 'mac'
|
||||
@@ -23,7 +23,7 @@ function detectPlatform(ua: string): DetectedPlatform {
|
||||
// TODO: Only Windows x64 and macOS arm64 are available today.
|
||||
// When Linux and/or macIntel builds are added, extend detection and URLs here.
|
||||
export function useDownloadUrl() {
|
||||
const platform = ref<DetectedPlatform>(null)
|
||||
const platform = ref<Platform | null>(null)
|
||||
const detected = ref(false)
|
||||
const isMobileUa = ref(false)
|
||||
|
||||
|
||||
@@ -174,16 +174,16 @@ const translations = {
|
||||
'zh-CN': '掌控每个模型、每个节点、每个步骤、每个输出。'
|
||||
},
|
||||
'products.local.title': {
|
||||
en: 'Comfy\nLocal',
|
||||
'zh-CN': 'Comfy\n本地版'
|
||||
en: 'Comfy\nDesktop',
|
||||
'zh-CN': 'Comfy\n桌面版'
|
||||
},
|
||||
'products.local.description': {
|
||||
en: 'Run ComfyUI on your own hardware.',
|
||||
'zh-CN': '在您自己的硬件上运行 ComfyUI。'
|
||||
},
|
||||
'products.local.cta': {
|
||||
en: 'SEE LOCAL FEATURES',
|
||||
'zh-CN': '查看本地版属性'
|
||||
en: 'SEE DESKTOP FEATURES',
|
||||
'zh-CN': '查看桌面版属性'
|
||||
},
|
||||
'products.cloud.title': {
|
||||
en: 'Comfy\nCloud',
|
||||
@@ -1057,18 +1057,18 @@ const translations = {
|
||||
'zh-CN': 'Cloud 与本地运行 ComfyUI 有什么区别?'
|
||||
},
|
||||
'cloud.faq.2.a': {
|
||||
en: 'Cloud runs on powerful remote GPUs and is accessible from any device. Local runs entirely on your computer, giving you full control and offline use.',
|
||||
en: 'Cloud runs on powerful remote GPUs and is accessible from any device. Comfy Desktop runs entirely on your computer, giving you full control and offline use.',
|
||||
'zh-CN':
|
||||
'Cloud 在强大的远程 GPU 上运行,可从任何设备访问。本地版完全在您的电脑上运行,提供完全控制和离线使用。'
|
||||
'Cloud 在强大的远程 GPU 上运行,可从任何设备访问。Comfy 桌面版完全在您的电脑上运行,提供完全控制和离线使用。'
|
||||
},
|
||||
'cloud.faq.3.q': {
|
||||
en: 'Which version should I choose, Comfy Cloud or local ComfyUI (self-hosted)?',
|
||||
'zh-CN': '我应该选择 Comfy Cloud 还是本地 ComfyUI(自托管)?'
|
||||
en: 'Which version should I choose, Comfy Cloud or Comfy Desktop?',
|
||||
'zh-CN': '我应该选择 Comfy Cloud 还是 Comfy 桌面版?'
|
||||
},
|
||||
'cloud.faq.3.a': {
|
||||
en: "Comfy Cloud has zero setup, is easy to share with your team, and is faster than most GPUs you can run on a desktop workstation. You can immediately run the best models and workflows from the community on Comfy Cloud.\nLocal ComfyUI is infinitely customizable, works offline, and you don't need to worry about queue times. However, depending on what you want to create, you might need to have a good GPU and some amount of technical knowledge to install community-created custom nodes.",
|
||||
en: "Comfy Cloud has zero setup, is easy to share with your team, and is faster than most GPUs you can run on a desktop workstation. You can immediately run the best models and workflows from the community on Comfy Cloud.\nComfy Desktop is infinitely customizable, works offline, and you don't need to worry about queue times. However, depending on what you want to create, you might need to have a good GPU and some amount of technical knowledge to install community-created custom nodes.",
|
||||
'zh-CN':
|
||||
'Comfy Cloud 无需任何设置,方便与团队共享,比大多数桌面工作站 GPU 更快。您可以立即在 Comfy Cloud 上运行社区中最好的模型和工作流。\n本地 ComfyUI 可以无限定制,支持离线工作,无需担心排队时间。但根据您的创作需求,可能需要一块好的 GPU 以及一定的技术知识来安装社区创建的自定义节点。'
|
||||
'Comfy Cloud 无需任何设置,方便与团队共享,比大多数桌面工作站 GPU 更快。您可以立即在 Comfy Cloud 上运行社区中最好的模型和工作流。\nComfy 桌面版可以无限定制,支持离线工作,无需担心排队时间。但根据您的创作需求,可能需要一块好的 GPU 以及一定的技术知识来安装社区创建的自定义节点。'
|
||||
},
|
||||
'cloud.faq.4.q': {
|
||||
en: 'Do I need a GPU or a strong computer to use Comfy Cloud?',
|
||||
@@ -1091,9 +1091,9 @@ const translations = {
|
||||
'zh-CN': '我可以在 Comfy Cloud 上使用现有的工作流吗?'
|
||||
},
|
||||
'cloud.faq.6.a': {
|
||||
en: 'Yes, your workflows work across Local and Cloud. Just note that only the most popular custom nodes are supported for now, but more will be added soon.',
|
||||
en: 'Yes, your workflows work across Desktop and Cloud. Just note that only the most popular custom nodes are supported for now, but more will be added soon.',
|
||||
'zh-CN':
|
||||
'可以,您的工作流在本地和云端都能使用。请注意,目前仅支持最热门的自定义节点,但很快会添加更多。'
|
||||
'可以,您的工作流在桌面版和云端都能使用。请注意,目前仅支持最热门的自定义节点,但很快会添加更多。'
|
||||
},
|
||||
'cloud.faq.7.q': {
|
||||
en: 'Are all ComfyUI extensions and custom nodes supported?',
|
||||
@@ -1145,9 +1145,9 @@ const translations = {
|
||||
'zh-CN': '合作伙伴节点积分和我的 Cloud 订阅有什么区别?'
|
||||
},
|
||||
'cloud.faq.12.a': {
|
||||
en: 'Comfy Cloud has a credit system that is used for both Partner nodes (formerly API nodes) and running workflows on cloud.\n1. Partner Nodes (Pay-as-you-go): These nodes (formerly called API nodes) run third-party models via API calls and can be used on both Comfy Cloud and Local/Self-Hosted ComfyUI. Each node has its own usage cost, determined by the API provider, and we directly match their pricing.\n2. Running workflows on cloud: Exclusive to Comfy Cloud, you get a set amount of credits per month, with the amount differing based on your plan. More credits can be topped up anytime. Credits are only used up for GPU time while workflows are running — not while editing or building them. No idle costs, no setup, and no infrastructure to manage.',
|
||||
en: 'Comfy Cloud has a credit system that is used for both Partner nodes (formerly API nodes) and running workflows on cloud.\n1. Partner Nodes (Pay-as-you-go): These nodes (formerly called API nodes) run third-party models via API calls and can be used on both Comfy Cloud and Comfy Desktop. Each node has its own usage cost, determined by the API provider, and we directly match their pricing.\n2. Running workflows on cloud: Exclusive to Comfy Cloud, you get a set amount of credits per month, with the amount differing based on your plan. More credits can be topped up anytime. Credits are only used up for GPU time while workflows are running — not while editing or building them. No idle costs, no setup, and no infrastructure to manage.',
|
||||
'zh-CN':
|
||||
'Comfy Cloud 有一个积分系统,用于合作伙伴节点(原 API 节点)和在云端运行工作流。\n1. 合作伙伴节点(按需付费):这些节点(原称 API 节点)通过 API 调用运行第三方模型,可在 Comfy Cloud 和本地/自托管 ComfyUI 上使用。每个节点有其自身的使用成本,由 API 提供商决定,我们直接匹配他们的定价。\n2. 在云端运行工作流:Comfy Cloud 专属,您每月获得一定数量的积分,数量根据您的计划而不同。积分可随时充值。积分仅在工作流运行时用于 GPU 时间——编辑或构建时不消耗。无闲置成本,无需设置,无需管理基础设施。'
|
||||
'Comfy Cloud 有一个积分系统,用于合作伙伴节点(原 API 节点)和在云端运行工作流。\n1. 合作伙伴节点(按需付费):这些节点(原称 API 节点)通过 API 调用运行第三方模型,可在 Comfy Cloud 和 Comfy 桌面版上使用。每个节点有其自身的使用成本,由 API 提供商决定,我们直接匹配他们的定价。\n2. 在云端运行工作流:Comfy Cloud 专属,您每月获得一定数量的积分,数量根据您的计划而不同。积分可随时充值。积分仅在工作流运行时用于 GPU 时间——编辑或构建时不消耗。无闲置成本,无需设置,无需管理基础设施。'
|
||||
},
|
||||
'cloud.faq.13.q': {
|
||||
en: 'Can I cancel my subscription?',
|
||||
@@ -1411,9 +1411,9 @@ const translations = {
|
||||
'zh-CN': '合作伙伴节点'
|
||||
},
|
||||
'pricing.included.feature8.description': {
|
||||
en: 'Run <strong>proprietary models</strong> through Comfy\'s <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">Partner Nodes</a>, such as Nano Banana. The amount of credits each node uses depends on the model and parameters you set in the node, but these credits are the same ones that your monthly subscription comes with. These credits can also be used across Comfy Cloud and local ComfyUI. Read more about Partner nodes <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">here</a>.',
|
||||
en: 'Run <strong>proprietary models</strong> through Comfy\'s <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">Partner Nodes</a>, such as Nano Banana. The amount of credits each node uses depends on the model and parameters you set in the node, but these credits are the same ones that your monthly subscription comes with. These credits can also be used across Comfy Cloud and Comfy Desktop. Read more about Partner nodes <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">here</a>.',
|
||||
'zh-CN':
|
||||
'通过 Comfy 的<a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">合作伙伴节点</a>运行<strong>专有模型</strong>,如 Nano Banana。每个节点消耗的积分取决于所用模型和参数设置,且与月度订阅积分通用。积分可在 Comfy Cloud 和本地 ComfyUI 间通用。了解更多关于合作伙伴节点的信息请点击<a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">此处</a>。'
|
||||
'通过 Comfy 的<a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">合作伙伴节点</a>运行<strong>专有模型</strong>,如 Nano Banana。每个节点消耗的积分取决于所用模型和参数设置,且与月度订阅积分通用。积分可在 Comfy Cloud 和 Comfy 桌面版间通用。了解更多关于合作伙伴节点的信息请点击<a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">此处</a>。'
|
||||
},
|
||||
'pricing.included.feature9.title': {
|
||||
en: 'Job queue',
|
||||
|
||||
@@ -73,7 +73,7 @@ const websiteJsonLd = {
|
||||
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" sizes="48x48" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#211927" />
|
||||
|
||||
@@ -11,9 +11,9 @@ import { t } from '../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Download Comfy — Run AI Locally"
|
||||
title="Download Comfy Desktop — Run AI on Your Hardware"
|
||||
description={t('download.hero.subtitle', 'en')}
|
||||
keywords={['comfyui app', 'comfyui desktop app', 'comfy ui application', 'comfyui download', 'download comfyui', 'comfyui windows', 'comfyui mac', 'comfyui linux', 'comfyui local']}
|
||||
keywords={['comfyui app', 'comfyui desktop app', 'comfyui desktop', 'comfy ui application', 'comfyui download', 'download comfyui', 'comfyui windows', 'comfyui mac', 'comfyui linux']}
|
||||
>
|
||||
<CloudBannerSection />
|
||||
<HeroSection client:load />
|
||||
|
||||
@@ -11,7 +11,7 @@ import { t } from '../../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="下载 — Comfy"
|
||||
title="下载 Comfy 桌面版 — 在您的硬件上运行 AI"
|
||||
description={t('download.hero.subtitle', 'zh-CN')}
|
||||
keywords={['comfyui app', 'comfyui desktop app', 'comfyui download', 'ComfyUI 下载', 'ComfyUI 桌面应用', 'ComfyUI 应用', 'ComfyUI Windows', 'ComfyUI macOS', 'ComfyUI Linux']}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,8 @@ import posthog from 'posthog-js'
|
||||
|
||||
import { createPostHogBeforeSend } from '@comfyorg/shared-frontend-utils/piiUtil'
|
||||
|
||||
import type { Platform } from '@/composables/useDownloadUrl'
|
||||
|
||||
const POSTHOG_KEY =
|
||||
import.meta.env.PUBLIC_POSTHOG_KEY ??
|
||||
'phc_iKfK86id4xVYws9LybMje0h44eGtfwFgRPIBehmy8rO'
|
||||
@@ -39,7 +41,7 @@ export function capturePageview() {
|
||||
}
|
||||
}
|
||||
|
||||
export function captureDownloadClick(platform: string) {
|
||||
export function captureDownloadClick(platform: Platform) {
|
||||
if (!initialized) return
|
||||
try {
|
||||
posthog.capture('website:download_button_clicked', { platform })
|
||||
|
||||
61
browser_tests/assets/missing/node_replacement_same_type.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "E2E_OldSampler",
|
||||
"pos": [100, 100],
|
||||
"size": [400, 262],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "model", "type": "MODEL", "link": null },
|
||||
{ "name": "positive", "type": "CONDITIONING", "link": null },
|
||||
{ "name": "negative", "type": "CONDITIONING", "link": null },
|
||||
{ "name": "latent_image", "type": "LATENT", "link": null }
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": { "Node name for S&R": "E2E_OldSampler" },
|
||||
"widgets_values": [42, 20, 7, "euler", "normal"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "E2E_OldSampler",
|
||||
"pos": [520, 100],
|
||||
"size": [400, 262],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "model", "type": "MODEL", "link": null },
|
||||
{ "name": "positive", "type": "CONDITIONING", "link": null },
|
||||
{ "name": "negative", "type": "CONDITIONING", "link": null },
|
||||
{ "name": "latent_image", "type": "LATENT", "link": null }
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": { "Node name for S&R": "E2E_OldSampler" },
|
||||
"widgets_values": [43, 20, 7, "euler", "normal"]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": { "ds": { "scale": 1, "offset": [0, 0] } },
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -51,20 +51,6 @@ export class FeatureFlagHelper {
|
||||
})
|
||||
}
|
||||
|
||||
async setServerFlags(flags: Record<string, unknown>): Promise<void> {
|
||||
await this.page.evaluate((flagMap: Record<string, unknown>) => {
|
||||
const api = window.app!.api
|
||||
api.serverFeatureFlags.value = {
|
||||
...api.serverFeatureFlags.value,
|
||||
...flagMap
|
||||
}
|
||||
}, flags)
|
||||
}
|
||||
|
||||
async setServerFlag(name: string, value: unknown): Promise<void> {
|
||||
await this.setServerFlags({ [name]: value })
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock server feature flags via route interception on /api/features.
|
||||
*/
|
||||
|
||||
@@ -70,6 +70,7 @@ export const TestIds = {
|
||||
missingModelImportUnsupported: 'missing-model-import-unsupported',
|
||||
missingMediaGroup: 'error-group-missing-media',
|
||||
swapNodesGroup: 'error-group-swap-nodes',
|
||||
swapNodeGroupCount: 'swap-node-group-count',
|
||||
missingMediaRow: 'missing-media-row',
|
||||
missingMediaLocateButton: 'missing-media-locate-button',
|
||||
publishTabPanel: 'publish-tab-panel',
|
||||
@@ -136,7 +137,8 @@ export const TestIds = {
|
||||
colorPickerCurrentColor: 'color-picker-current-color',
|
||||
colorBlue: 'blue',
|
||||
colorRed: 'red',
|
||||
convertSubgraph: 'convert-to-subgraph-button'
|
||||
convertSubgraph: 'convert-to-subgraph-button',
|
||||
bypass: 'bypass-button'
|
||||
},
|
||||
menu: {
|
||||
moreMenuContent: 'more-menu-content'
|
||||
|
||||
@@ -190,6 +190,16 @@ test.describe('Color Palette', { tag: ['@screenshot', '@settings'] }, () => {
|
||||
'custom-color-palette-obsidian-dark.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Palette can modify @vue-nodes color', async ({ comfyPage }) => {
|
||||
const node = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
const getColor = () =>
|
||||
node.body.evaluate((el) => getComputedStyle(el).backgroundColor)
|
||||
|
||||
const initialColor = await getColor()
|
||||
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'solarized')
|
||||
await expect.poll(getColor).not.toEqual(initialColor)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe(
|
||||
|
||||
@@ -48,6 +48,36 @@ test.describe('Node replacement', { tag: ['@node', '@ui'] }, () => {
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Shows direct row label and locate action for a single replacement group', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const swapGroup = getSwapNodesGroup(comfyPage.page)
|
||||
const rowLabel = swapGroup.getByRole('button', {
|
||||
name: 'E2E_OldSampler',
|
||||
exact: true
|
||||
})
|
||||
|
||||
await expect(rowLabel).toBeVisible()
|
||||
await expect(
|
||||
swapGroup.getByRole('button', {
|
||||
name: 'Locate node on canvas',
|
||||
exact: true
|
||||
})
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
swapGroup.getByTestId(TestIds.dialogs.swapNodeGroupCount)
|
||||
).toHaveCount(0)
|
||||
|
||||
await comfyPage.canvasOps.pan({ x: -800, y: -800 })
|
||||
const offsetBeforeLocate = await comfyPage.canvasOps.getOffset()
|
||||
|
||||
await rowLabel.click()
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.canvasOps.getOffset())
|
||||
.not.toEqual(offsetBeforeLocate)
|
||||
})
|
||||
|
||||
test('Replace Node replaces a single group in-place', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
@@ -116,6 +146,55 @@ test.describe('Node replacement', { tag: ['@node', '@ui'] }, () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Same-type replacement group', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.VueNodes.Enabled',
|
||||
mode.vueNodesEnabled
|
||||
)
|
||||
await setupNodeReplacement(comfyPage, mockNodeReplacementsSingle)
|
||||
await loadWorkflowAndOpenErrorsTab(
|
||||
comfyPage,
|
||||
'missing/node_replacement_same_type'
|
||||
)
|
||||
})
|
||||
|
||||
test('Groups same-type replacement rows behind the title disclosure', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const swapGroup = getSwapNodesGroup(comfyPage.page)
|
||||
const countBadge = swapGroup.getByTestId(
|
||||
TestIds.dialogs.swapNodeGroupCount
|
||||
)
|
||||
const childRows = swapGroup.getByRole('listitem')
|
||||
const expandButton = swapGroup.getByRole('button', {
|
||||
name: 'Expand E2E_OldSampler',
|
||||
exact: true
|
||||
})
|
||||
|
||||
await expect(expandButton).toBeVisible()
|
||||
await expect(countBadge).toHaveText('2')
|
||||
await expect(childRows).toHaveCount(0)
|
||||
|
||||
await expandButton.click()
|
||||
await expect(childRows).toHaveCount(2)
|
||||
await expect(
|
||||
swapGroup.getByRole('button', {
|
||||
name: 'E2E_OldSampler',
|
||||
exact: true
|
||||
})
|
||||
).toHaveCount(2)
|
||||
|
||||
await swapGroup
|
||||
.getByRole('button', {
|
||||
name: 'Collapse E2E_OldSampler',
|
||||
exact: true
|
||||
})
|
||||
.click()
|
||||
await expect(childRows).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Multi-type replacement', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
|
||||
@@ -309,50 +309,6 @@ test.describe('Node search box V2 extended', { tag: '@node' }, () => {
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Empty graph defaults', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.featureFlags.setServerFlag(
|
||||
'node_library_essentials_enabled',
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
test('Defaults to Essentials when graph is empty', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
await comfyPage.nodeOps.clearGraph()
|
||||
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(0)
|
||||
|
||||
await searchBoxV2.open()
|
||||
|
||||
const essentialsBtn = searchBoxV2.rootCategoryButton(
|
||||
RootCategory.Essentials
|
||||
)
|
||||
await expect(essentialsBtn).toBeVisible()
|
||||
await expect(essentialsBtn).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
|
||||
test('Defaults to Most Relevant when graph has nodes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
||||
.toBeGreaterThan(0)
|
||||
|
||||
await searchBoxV2.open()
|
||||
|
||||
await expect(searchBoxV2.categoryButton('most-relevant')).toHaveAttribute(
|
||||
'aria-current',
|
||||
'true'
|
||||
)
|
||||
await expect(
|
||||
searchBoxV2.rootCategoryButton(RootCategory.Essentials)
|
||||
).toHaveAttribute('aria-pressed', 'false')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Search behavior', () => {
|
||||
test('Search narrows results progressively', async ({ comfyPage }) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
|
||||
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
@@ -129,23 +129,18 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
}) => {
|
||||
// A group + a KSampler node
|
||||
await comfyPage.workflow.loadWorkflow('groups/single_group')
|
||||
const bypass = comfyPage.page.getByTestId(TestIds.selectionToolbox.bypass)
|
||||
|
||||
// Select group + node should show bypass button
|
||||
await comfyPage.canvas.focus()
|
||||
await comfyPage.page.keyboard.press('Control+A')
|
||||
await expect(
|
||||
comfyPage.page.locator(
|
||||
'.selection-toolbox *[data-testid="bypass-button"]'
|
||||
)
|
||||
).toBeVisible()
|
||||
|
||||
// Deselect node (Only group is selected) should hide bypass button
|
||||
await comfyPage.nodeOps.selectNodes(['KSampler'])
|
||||
await expect(
|
||||
comfyPage.page.locator(
|
||||
'.selection-toolbox *[data-testid="bypass-button"]'
|
||||
)
|
||||
).toBeHidden()
|
||||
await expect(bypass).toBeVisible()
|
||||
await comfyPage.keyboard.delete()
|
||||
|
||||
// (Only empty group is selected) should hide bypass button
|
||||
await comfyPage.keyboard.selectAll()
|
||||
await expect(comfyPage.selectionToolbox).toBeVisible()
|
||||
await expect(bypass).toBeHidden()
|
||||
})
|
||||
|
||||
test.describe('Color Picker', () => {
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
comfyPageFixture as test
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
import { getGroupTitlePosition } from '@e2e/fixtures/utils/groupHelpers'
|
||||
|
||||
const CREATE_GROUP_HOTKEY = 'Control+g'
|
||||
|
||||
@@ -217,4 +219,40 @@ test.describe('Vue Node Groups', { tag: ['@screenshot', '@vue-nodes'] }, () => {
|
||||
)
|
||||
}).toPass({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('Bypassing a group bypasses contents', async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
await comfyPage.keyboard.selectAll()
|
||||
await comfyPage.page.keyboard.press('.')
|
||||
await comfyPage.page.keyboard.press(CREATE_GROUP_HOTKEY)
|
||||
|
||||
const toggleBypass = () =>
|
||||
comfyPage.page.getByTestId(TestIds.selectionToolbox.bypass).click()
|
||||
const bypassCount = () =>
|
||||
comfyPage.page.evaluate(
|
||||
() => graph!.nodes.filter((node) => node.mode === 4).length
|
||||
)
|
||||
expect(await bypassCount()).toBe(0)
|
||||
const groupCount = () => comfyPage.page.evaluate(() => graph!.groups.length)
|
||||
await expect.poll(groupCount, 'create group').toBe(1)
|
||||
|
||||
const ksampler = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
await ksampler.select()
|
||||
await toggleBypass()
|
||||
await expect.poll(bypassCount, 'setup bypass of single node').toBe(1)
|
||||
|
||||
const groupPos = await getGroupTitlePosition(comfyPage, 'Group')
|
||||
await comfyPage.page.mouse.click(groupPos.x, groupPos.y)
|
||||
await toggleBypass()
|
||||
await expect.poll(bypassCount, 'all nodes are set to bypassed').toBe(7)
|
||||
await toggleBypass()
|
||||
await expect.poll(bypassCount, 'all nodes are unbypassed').toBe(0)
|
||||
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await ksampler.select()
|
||||
await comfyPage.page.keyboard.up('Shift')
|
||||
|
||||
await toggleBypass()
|
||||
await expect.poll(bypassCount, "won't toggle double selected node").toBe(7)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
@@ -34,7 +34,7 @@ function formatAndEslint(fileNames: string[]) {
|
||||
const joinedPaths = toJoinedRelativePaths(fileNames)
|
||||
return [
|
||||
`pnpm exec oxfmt --write ${joinedPaths}`,
|
||||
`pnpm exec oxlint --type-aware --fix ${joinedPaths}`,
|
||||
`pnpm exec oxlint --type-aware --no-error-on-unmatched-pattern --fix ${joinedPaths}`,
|
||||
`pnpm exec eslint --cache --fix --no-warn-ignored ${joinedPaths}`
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.46.12",
|
||||
"version": "1.47.0",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
443
pnpm-lock.yaml
generated
@@ -211,8 +211,8 @@ catalogs:
|
||||
specifier: ^4.16.2
|
||||
version: 4.16.2
|
||||
eslint-plugin-oxlint:
|
||||
specifier: 1.59.0
|
||||
version: 1.59.0
|
||||
specifier: 1.69.0
|
||||
version: 1.69.0
|
||||
eslint-plugin-playwright:
|
||||
specifier: ^2.10.1
|
||||
version: 2.10.1
|
||||
@@ -277,14 +277,14 @@ catalogs:
|
||||
specifier: ^2.12.9
|
||||
version: 2.12.9
|
||||
oxfmt:
|
||||
specifier: ^0.44.0
|
||||
version: 0.44.0
|
||||
specifier: ^0.54.0
|
||||
version: 0.54.0
|
||||
oxlint:
|
||||
specifier: ^1.59.0
|
||||
version: 1.59.0
|
||||
specifier: ^1.69.0
|
||||
version: 1.69.0
|
||||
oxlint-tsgolint:
|
||||
specifier: ^0.20.0
|
||||
version: 0.20.0
|
||||
specifier: ^0.23.0
|
||||
version: 0.23.0
|
||||
picocolors:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
@@ -717,13 +717,13 @@ importers:
|
||||
version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1)))(eslint@10.4.0(jiti@2.6.1))
|
||||
eslint-plugin-better-tailwindcss:
|
||||
specifier: 'catalog:'
|
||||
version: 4.3.1(eslint@10.4.0(jiti@2.6.1))(oxlint@1.59.0(oxlint-tsgolint@0.20.0))(tailwindcss@4.3.0)(typescript@5.9.3)
|
||||
version: 4.3.1(eslint@10.4.0(jiti@2.6.1))(oxlint@1.69.0(oxlint-tsgolint@0.23.0))(tailwindcss@4.3.0)(typescript@5.9.3)
|
||||
eslint-plugin-import-x:
|
||||
specifier: 'catalog:'
|
||||
version: 4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.0(jiti@2.6.1))
|
||||
eslint-plugin-oxlint:
|
||||
specifier: 'catalog:'
|
||||
version: 1.59.0(oxlint@1.59.0(oxlint-tsgolint@0.20.0))
|
||||
version: 1.69.0(oxlint@1.69.0(oxlint-tsgolint@0.23.0))
|
||||
eslint-plugin-playwright:
|
||||
specifier: 'catalog:'
|
||||
version: 2.10.1(eslint@10.4.0(jiti@2.6.1))
|
||||
@@ -777,13 +777,13 @@ importers:
|
||||
version: 2.12.9
|
||||
oxfmt:
|
||||
specifier: 'catalog:'
|
||||
version: 0.44.0
|
||||
version: 0.54.0
|
||||
oxlint:
|
||||
specifier: 'catalog:'
|
||||
version: 1.59.0(oxlint-tsgolint@0.20.0)
|
||||
version: 1.69.0(oxlint-tsgolint@0.23.0)
|
||||
oxlint-tsgolint:
|
||||
specifier: 'catalog:'
|
||||
version: 0.20.0
|
||||
version: 0.23.0
|
||||
picocolors:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.1
|
||||
@@ -2751,276 +2751,276 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxfmt/binding-android-arm-eabi@0.44.0':
|
||||
resolution: {integrity: sha512-5UvghMd9SA/yvKTWCAxMAPXS1d2i054UeOf4iFjZjfayTwCINcC3oaSXjtbZfCaEpxgJod7XiOjTtby5yEv/BQ==}
|
||||
'@oxfmt/binding-android-arm-eabi@0.54.0':
|
||||
resolution: {integrity: sha512-NAtpl/SiaeU103e7/OmZw0MvUnsUUopW7hEm/ecegJg7YM0skQaA0IXEZoyTV6NUdiNPupdIUreRqUZTShbn/g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@oxfmt/binding-android-arm64@0.44.0':
|
||||
resolution: {integrity: sha512-IVudM1BWfvrYO++Khtzr8q9n5Rxu7msUvoFMqzGJVdX7HfUXUDHwaH2zHZNB58svx2J56pmCUzophyaPFkcG/A==}
|
||||
'@oxfmt/binding-android-arm64@0.54.0':
|
||||
resolution: {integrity: sha512-B4VZfBUlKK1rmMChsssNZbkZjE8+FzG3avMjGgMDwbGxXRoXkoeXiAZ+78Oa+eyDPHvDCiUb4zH/vmCOUSafLQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@oxfmt/binding-darwin-arm64@0.44.0':
|
||||
resolution: {integrity: sha512-eWCLAIKAHfx88EqEP1Ga2yz7qVcqDU5lemn4xck+07bH182hDdprOHjbogyk0In1Djys3T0/pO2JepFnRJ41Mg==}
|
||||
'@oxfmt/binding-darwin-arm64@0.54.0':
|
||||
resolution: {integrity: sha512-i02vF75b+ePsQP3tHqSxVYI5S6b8X/xqdPu7/mDHXtpgXLTYXi3jJmfHU0j+dnZZDKaYTx/ioCK7QYJmtiJR2g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxfmt/binding-darwin-x64@0.44.0':
|
||||
resolution: {integrity: sha512-eHTBznHLM49++dwz07MblQ2cOXyIgeedmE3Wgy4ptUESj38/qYZyRi1MPwC9olQJWssMeY6WI3UZ7YmU5ggvyQ==}
|
||||
'@oxfmt/binding-darwin-x64@0.54.0':
|
||||
resolution: {integrity: sha512-8VMFvGvooXj7mswkbrhdVZ2/sgiDaBzWpkkbtO+qGDLV4EfJd67nQadHkQC0ZNbaWA9ajXfqI6i7PZLIeDzxEQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxfmt/binding-freebsd-x64@0.44.0':
|
||||
resolution: {integrity: sha512-jLMmbj0u0Ft43QpkUVr/0v1ZfQCGWAvU+WznEHcN3wZC/q6ox7XeSJtk9P36CCpiDSUf3sGnzbIuG1KdEMEDJQ==}
|
||||
'@oxfmt/binding-freebsd-x64@0.54.0':
|
||||
resolution: {integrity: sha512-0cRHnp43WN1Jrc5s0BdbdKgR1XirdvHy7TAFi3JEsoEVQVJxTXMbpVd76sxXlgRswNMDhVFSJw+y7Eb8mEavFQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@oxfmt/binding-linux-arm-gnueabihf@0.44.0':
|
||||
resolution: {integrity: sha512-n+A/u/ByK1qV8FVGOwyaSpw5NPNl0qlZfgTBqHeGIqr8Qzq1tyWZ4lAaxPoe5mZqE3w88vn3+jZtMxriHPE7tg==}
|
||||
'@oxfmt/binding-linux-arm-gnueabihf@0.54.0':
|
||||
resolution: {integrity: sha512-JyQAk3hK/OEtup7Rw6kZwfdzbKqTVD5jXXb8Xpfay29suwZyfBDMVW/bj4RqEPySYWc6zCp198pOluf8n5uYzg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxfmt/binding-linux-arm-musleabihf@0.44.0':
|
||||
resolution: {integrity: sha512-5eax+FkxyCqAi3Rw0mrZFr7+KTt/XweFsbALR+B5ljWBLBl8nHe4ADrUnb1gLEfQCJLl+Ca5FIVD4xEt95AwIw==}
|
||||
'@oxfmt/binding-linux-arm-musleabihf@0.54.0':
|
||||
resolution: {integrity: sha512-qnvLatTpM8vtvjOfcckBOzJjk+n6ce/wwpP8OFeUrD5aNLYcKyWAitwj+Rk3PK9jGanbZvKsJnv14JGQ6XqFdw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-58l8JaHxSGOmOMOG2CIrNsnkRJAj0YcHQCmvNACniOa/vd1iRHhlPajczegzS5jwMENlqgreyiTR9iNlke8qCw==}
|
||||
'@oxfmt/binding-linux-arm64-gnu@0.54.0':
|
||||
resolution: {integrity: sha512-SMkhnCzIYZYDk9vw3W/80eeYKmrMpGF0Giuxt4HruFlCH7jEtnPeb3SdQKMfgYi/dgtaf+hZAb5XWPYnxqCQ3w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.44.0':
|
||||
resolution: {integrity: sha512-AlObQIXyVRZ96LbtVljtFq0JqH5B92NU+BQeDFrXWBUWlCKAM0wF5GLfIhCLT5kQ3Sl+U0YjRJ7Alqj5hGQaCg==}
|
||||
'@oxfmt/binding-linux-arm64-musl@0.54.0':
|
||||
resolution: {integrity: sha512-QrwJlBFFKnxOd95TAaszpMbZBLzMoYMpGaQTZF8oibacnF5rv8l12IhILhQRPmksWiBqg0YSe2Mnl7ayeJAHSA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-YcFE8/q/BbrCiIiM5piwbkA6GwJc5QqhMQp2yDrqQ2fuVkZ7CInb1aIijZ/k8EXc72qXMSwKpVlBv1w/MsGO/A==}
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.54.0':
|
||||
resolution: {integrity: sha512-WILatiol/TUHTlhod7R09+7Az/XlhKwmY1MHfLZNmewltPWNN/EwxP2rQSHahibZ/cB8gmckEBjBOByD+5bYsQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-eOdzs6RqkRzuqNHUX5C8ISN5xfGh4xDww8OEd9YAmc3OWN8oAe5bmlIqQ+rrHLpv58/0BuU48bxkhnIGjA/ATQ==}
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.54.0':
|
||||
resolution: {integrity: sha512-f05YMG4BH4G8S4ME6UM6fi1MnJ9094mrnvO5Pa4SJlMfWlUM+1/ZWMEF4NnjM7shZAvbHsHRuVYpUo0PHC4P9Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.44.0':
|
||||
resolution: {integrity: sha512-YBgNTxntD/QvlFUfgvh8bEdwOhXiquX8gaofZJAwYa/Xp1S1DQrFVZEeck7GFktr24DztsSp8N8WtWCBwxs0Hw==}
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.54.0':
|
||||
resolution: {integrity: sha512-UfL+2hj1ClNqcCRT9s8vBU4axDpjxgVxX96G+9DYAYjoc5b0u15CJtn2jgsi9iM+EbGNc5CW1HVRgwVu76UsSA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-GLIh1R6WHWshl/i4QQDNgj0WtT25aRO4HNUWEoitxiywyRdhTFmFEYT2rXlcl9U6/26vhmOqG5cRlMLG3ocaIA==}
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.54.0':
|
||||
resolution: {integrity: sha512-3/XZe931Hka+J6NjnaqJzYpsWWxDTuRdUdwSQHnOuJEgbC+SehIMFJS8hsEjV7LBhVSL2OCnRLvbVW8O97XIyw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-gZOpgTlOsLcLfAF9qgpTr7FIIFSKnQN3hDf/0JvQ4CIwMY7h+eilNjxq/CorqvYcEOu+LRt1W4ZS7KccEHLOdA==}
|
||||
'@oxfmt/binding-linux-x64-gnu@0.54.0':
|
||||
resolution: {integrity: sha512-Ik93RlObtu43GbxApafayFjwYE06L6Xr08cSwpBPYbDrLp2ReZx0Jm1DqwRyYRnukUJy+rK2WaEvUQOxdytU9Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.44.0':
|
||||
resolution: {integrity: sha512-1CyS9JTB+pCUFYFI6pkQGGZaT/AY5gnhHVrQQLhFba6idP9AzVYm1xbdWfywoldTYvjxQJV6x4SuduCIfP3W+A==}
|
||||
'@oxfmt/binding-linux-x64-musl@0.54.0':
|
||||
resolution: {integrity: sha512-yZcakmPlD86CNymknd7KfW+FH+qfbqJH+i0h69CYfV1+KMoVeM9UED+8+TDVoU4haxI0NxY7RPCvRLy3Sqd2Qg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.44.0':
|
||||
resolution: {integrity: sha512-bmEv70Ak6jLr1xotCbF5TxIKjsmQaiX+jFRtnGtfA03tJPf6VG3cKh96S21boAt3JZc+Vjx8PYcDuLj39vM2Pw==}
|
||||
'@oxfmt/binding-openharmony-arm64@0.54.0':
|
||||
resolution: {integrity: sha512-GiVBZNnEZnKu00f1jTg49nomv187d0GQX+O+ocykoLeiaALuEO+swoTehHn9TehTfi7V8H0i0e/yvUjCqnwk1w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@oxfmt/binding-win32-arm64-msvc@0.44.0':
|
||||
resolution: {integrity: sha512-yWzB+oCpSnP/dmw85eFLAT5o35Ve5pkGS2uF/UCISpIwDqf1xa7OpmtomiqY/Vzg8VyvMbuf6vroF2khF/+1Vg==}
|
||||
'@oxfmt/binding-win32-arm64-msvc@0.54.0':
|
||||
resolution: {integrity: sha512-J0SSB8Z1Fre2sxRolYcW6Rl1RQmKdQ2hnHyq4YJrfBRiXTObLw4DXnIVraM/UyqGqwOi7yTrQA4VT7DPxlHVKA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxfmt/binding-win32-ia32-msvc@0.44.0':
|
||||
resolution: {integrity: sha512-TcWpo18xEIE3AmIG2kpr3kz5IEhQgnx0lazl2+8L+3eTopOAUevQcmlr4nhguImNWz0OMeOZrYZOhJNCf16nlQ==}
|
||||
'@oxfmt/binding-win32-ia32-msvc@0.54.0':
|
||||
resolution: {integrity: sha512-O61UDVj8zz6yXJjkHPf05VaMLOXmEF8P5kf/N0W7AQMmd6bcQogl+KJc7rMutKTL524oE9iH32JXZClBFmEQIg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@oxfmt/binding-win32-x64-msvc@0.44.0':
|
||||
resolution: {integrity: sha512-oj8aLkPJZppIM4CMQNsyir9ybM1Xw/CfGPTSsTnzpVGyljgfbdP0EVUlURiGM0BDrmw5psQ6ArmGCcUY/yABaQ==}
|
||||
'@oxfmt/binding-win32-x64-msvc@0.54.0':
|
||||
resolution: {integrity: sha512-1MDpqJPiFqxWtIHas8vkb1VZ7f7eKyTffAwmO8isxQYMaG1OFKsH666BWLeXQLO+IWNfiMssLD55hbR1lIPTqg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.20.0':
|
||||
resolution: {integrity: sha512-KKQcIHZHMxqpHUA1VXIbOG6chNCFkUWbQy6M+AFVtPKkA/3xAeJkJ3njoV66bfzwPHRcWQO+kcj5XqtbkjakoA==}
|
||||
'@oxlint-tsgolint/darwin-arm64@0.23.0':
|
||||
resolution: {integrity: sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint-tsgolint/darwin-x64@0.20.0':
|
||||
resolution: {integrity: sha512-7HeVMuclGfG+NLZi2ybY0T4fMI7/XxO/208rJk+zEIloKkVnlh11Wd241JMGwgNFXn+MLJbOqOfojDb2Dt4L1g==}
|
||||
'@oxlint-tsgolint/darwin-x64@0.23.0':
|
||||
resolution: {integrity: sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint-tsgolint/linux-arm64@0.20.0':
|
||||
resolution: {integrity: sha512-zxhUwz+WSxE6oWlZLK2z2ps9yC6ebmgoYmjAl0Oa48+GqkZ56NVgo+wb8DURNv6xrggzHStQxqQxe3mK51HZag==}
|
||||
'@oxlint-tsgolint/linux-arm64@0.23.0':
|
||||
resolution: {integrity: sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint-tsgolint/linux-x64@0.20.0':
|
||||
resolution: {integrity: sha512-/1l6FnahC9im8PK+Ekkx/V3yetO/PzZnJegE2FXcv/iXEhbeVxP/ouiTYcUQu9shT1FWJCSNti1VJHH+21Y1dg==}
|
||||
'@oxlint-tsgolint/linux-x64@0.23.0':
|
||||
resolution: {integrity: sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint-tsgolint/win32-arm64@0.20.0':
|
||||
resolution: {integrity: sha512-oPZ5Yz8sVdo7P/5q+i3IKeix31eFZ55JAPa1+RGPoe9PoaYVsdMvR6Jvib6YtrqoJnFPlg3fjEjlEPL8VBKYJA==}
|
||||
'@oxlint-tsgolint/win32-arm64@0.23.0':
|
||||
resolution: {integrity: sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint-tsgolint/win32-x64@0.20.0':
|
||||
resolution: {integrity: sha512-4stx8RHj3SP9vQyRF/yZbz5igtPvYMEUR8CUoha4BVNZihi39DpCR8qkU7lpjB5Ga1DRMo2pHaA4bdTOMaY4mw==}
|
||||
'@oxlint-tsgolint/win32-x64@0.23.0':
|
||||
resolution: {integrity: sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint/binding-android-arm-eabi@1.59.0':
|
||||
resolution: {integrity: sha512-etYDw/UaEv936AQUd/CRMBVd+e+XuuU6wC+VzOv1STvsTyZenLChepLWqLtnyTTp4YMlM22ypzogDDwqYxv5cg==}
|
||||
'@oxlint/binding-android-arm-eabi@1.69.0':
|
||||
resolution: {integrity: sha512-DKQQbD5cZ/MYfDgDI7YGyGD9FSxABlsBsYFo5p26lloob543tP9+4N3guwdXIYJN+7HSZxLe8YJuwcOWw5qnHg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@oxlint/binding-android-arm64@1.59.0':
|
||||
resolution: {integrity: sha512-TgLc7XVLKH2a4h8j3vn1MDjfK33i9MY60f/bKhRGWyVzbk5LCZ4X01VZG7iHrMmi5vYbAp8//Ponigx03CLsdw==}
|
||||
'@oxlint/binding-android-arm64@1.69.0':
|
||||
resolution: {integrity: sha512-lEhb+I5pr4inux+JFwfCa1HRq3Os7NirEFQ0H1I35SVEHPm6byX0Ah47xmRha3qi6LAkxUcxViL8o/9PivjzBg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@oxlint/binding-darwin-arm64@1.59.0':
|
||||
resolution: {integrity: sha512-DXyFPf5ZKldMLloRHx/B9fsxsiTQomaw7cmEW3YIJko2HgCh+GUhp9gGYwHrqlLJPsEe3dYj9JebjX92D3j3AA==}
|
||||
'@oxlint/binding-darwin-arm64@1.69.0':
|
||||
resolution: {integrity: sha512-GY2YE8lOZW59BW1Ia1y+1gR0XyjrZRvVWHAr8LGeGhYHE0OQJ/7cRKXTkx1P+E9/6awEc3SX8a68SFTjh/E//A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint/binding-darwin-x64@1.59.0':
|
||||
resolution: {integrity: sha512-LgvrsdgVLX1qWqIEmNsSmMXJhpAWdtUQ0M+oR0CySwi+9IHWyOGuIL8w8+u/kbZNMyZr4WUyYB5i0+D+AKgkLg==}
|
||||
'@oxlint/binding-darwin-x64@1.69.0':
|
||||
resolution: {integrity: sha512-ax1oZnOjHX3LB7myQyHEaQkDwfLb6str3/nSP6O7EVUviQGNkEGzGV0EqcBJWK+Ufwx0l4xPgyYayurvhAdl2Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint/binding-freebsd-x64@1.59.0':
|
||||
resolution: {integrity: sha512-bOJhqX/ny4hrFuTPlyk8foSRx/vLRpxJh0jOOKN2NWW6FScXHPAA5rQbrwdQPcgGB5V8Ua51RS03fke8ssBcug==}
|
||||
'@oxlint/binding-freebsd-x64@1.69.0':
|
||||
resolution: {integrity: sha512-kHWeHv4g2h8NY+mpCxzCtY4uerMJWTN/TSnNj1CPbakFpHEJ6cTya2wWV0pDSYWOJ2+0UiEbhn3AtXxHtsnKjg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@oxlint/binding-linux-arm-gnueabihf@1.59.0':
|
||||
resolution: {integrity: sha512-vVUXxYMF9trXCsz4m9H6U0IjehosVHxBzVgJUxly1uz4W1PdDyicaBnpC0KRXsHYretLVe+uS9pJy8iM57Kujw==}
|
||||
'@oxlint/binding-linux-arm-gnueabihf@1.69.0':
|
||||
resolution: {integrity: sha512-gq84vM1a1oEehXo27YCDzGVcxPsZDI1yswZwz2Da1/cbnWtrL16XZZnz0G/+gIU8edtHpfjxq5c+vWEHqJfWoQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/binding-linux-arm-musleabihf@1.59.0':
|
||||
resolution: {integrity: sha512-TULQW8YBPGRWg5yZpFPL54HLOnJ3/HiX6VenDPi6YfxB/jlItwSMFh3/hCeSNbh+DAMaE1Py0j5MOaivHkI/9Q==}
|
||||
'@oxlint/binding-linux-arm-musleabihf@1.69.0':
|
||||
resolution: {integrity: sha512-kIqEa98JQ0VRyrcncxA417m2AzasqTlD+FyVT1AksjvjkqQcvm7pBWYvoW3/mpyOP2XYvi5nSCCTIe6De1yu5g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint/binding-linux-arm64-gnu@1.59.0':
|
||||
resolution: {integrity: sha512-Gt54Y4eqSgYJ90xipm24xeyaPV854706o/kiT8oZvUt3VDY7qqxdqyGqchMaujd87ib+/MXvnl9WkK8Cc1BExg==}
|
||||
'@oxlint/binding-linux-arm64-gnu@1.69.0':
|
||||
resolution: {integrity: sha512-j+xYiXozxGWx2cpjCrwwGR4awTxPFsRv3JZrv23RCogEPMc4R7UqjHW47p/RG0aRlbWiROCJ8coUfCwy0dvzHA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.59.0':
|
||||
resolution: {integrity: sha512-3CtsKp7NFB3OfqQzbuAecrY7GIZeiv7AD+xutU4tefVQzlfmTI7/ygWLrvkzsDEjTlMq41rYHxgsn6Yh8tybmA==}
|
||||
'@oxlint/binding-linux-arm64-musl@1.69.0':
|
||||
resolution: {integrity: sha512-xEPpNppTfN1l/nM7gYSf9iocscu/as+p/7vxkLeLEKnYU+09Dm+5V6IhDYDh+Uz6FajEupWwCLt5SOG0y1PCKg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.59.0':
|
||||
resolution: {integrity: sha512-K0diOpT3ncDmOfl9I1HuvpEsAuTxkts0VYwIv/w6Xiy9CdwyPBVX88Ga9l8VlGgMrwBMnSY4xIvVlVY/fkQk7Q==}
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.69.0':
|
||||
resolution: {integrity: sha512-Ug0+eU7HJBlek+SjklYH62IlOMirEJsdxpihH0kSqX0XdrDD4NdHpQc10fK1JC35yn6KrrcN+uYzlHD38XAf8Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.59.0':
|
||||
resolution: {integrity: sha512-xAU7+QDU6kTJJ7mJLOGgo7oOjtAtkKyFZ0Yjdb5cEo3DiCCPFLvyr08rWiQh6evZ7RiUTf+o65NY/bqttzJiQQ==}
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.69.0':
|
||||
resolution: {integrity: sha512-iEyI3GIg0l/s3G4qy2TlaaWKdzj4PJJStwtlocpDTC00PY9hZueotf6OKUj9+yfQh0lrpBW/pLMgTztbAHKJEg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.59.0':
|
||||
resolution: {integrity: sha512-KUmZmKlTTyauOnvUNVxK7G40sSSx0+w5l1UhaGsC6KPpOYHenx2oqJTnabmpLJicok7IC+3Y6fXAUOMyexaeJQ==}
|
||||
'@oxlint/binding-linux-riscv64-musl@1.69.0':
|
||||
resolution: {integrity: sha512-NjHjpiI4WIKSMwuoJSZi5VToPeoYOS1FR52HLIDG6lidMdqquusgtODb4iLk0+lb1q3Z0nv2/aPRcC/olmpQGg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.59.0':
|
||||
resolution: {integrity: sha512-4usRxC8gS0PGdkHnRmwJt/4zrQNZyk6vL0trCxwZSsAKM+OxhB8nKiR+mhjdBbl8lbMh2gc3bZpNN/ik8c4c2A==}
|
||||
'@oxlint/binding-linux-s390x-gnu@1.69.0':
|
||||
resolution: {integrity: sha512-Ai/prDewoItkDXbp38gwGZi41DycZbUTZJ3UidwoHgQC0/DaqC2TGdtBTQLJ6hSD+SAxASzh8+/eSBPmxfOacA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.59.0':
|
||||
resolution: {integrity: sha512-s/rNE2gDmbwAOOP493xk2X7M8LZfI1LJFSSW1+yanz3vuQCFPiHkx4GY+O1HuLUDtkzGlhtMrIcxxzyYLv308w==}
|
||||
'@oxlint/binding-linux-x64-gnu@1.69.0':
|
||||
resolution: {integrity: sha512-Gt3KHgp46mRKz4sJeaASmKvD8ayXookRw07RMf+NowhEztGGDZ7VrXpoW96XuKJLjFukWizOFVNjmYb/u7caNQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.59.0':
|
||||
resolution: {integrity: sha512-+yYj1udJa2UvvIUmEm0IcKgc0UlPMgz0nsSTvkPL2y6n0uU5LgIHSwVu4AHhrve6j9BpVSoRksnz8c9QcvITJA==}
|
||||
'@oxlint/binding-linux-x64-musl@1.69.0':
|
||||
resolution: {integrity: sha512-7tQhJ2+p/oHv1zcfnjYI7YVzC/7iBaVOfIvFYtxdJ5F45mWgEdrCyXZXZGfiLey5t/5JhOhsaMnnv1kAzckd7g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.59.0':
|
||||
resolution: {integrity: sha512-bUplUb48LYsB3hHlQXP2ZMOenpieWoOyppLAnnAhuPag3MGPnt+7caxE3w/Vl9wpQsTA3gzLntQi9rxWrs7Xqg==}
|
||||
'@oxlint/binding-openharmony-arm64@1.69.0':
|
||||
resolution: {integrity: sha512-vmWz6TKp/3hfA4lksR0zHBv/6xuX1jhym6eqOjdH2DXsDDHZWcp2f0KG0VCAnlVbIrjk29G4wAWMXb/Hn1YobA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@oxlint/binding-win32-arm64-msvc@1.59.0':
|
||||
resolution: {integrity: sha512-/HLsLuz42rWl7h7ePdmMTpHm2HIDmPtcEMYgm5BBEHiEiuNOrzMaUpd2z7UnNni5LGN9obJy2YoAYBLXQwazrA==}
|
||||
'@oxlint/binding-win32-arm64-msvc@1.69.0':
|
||||
resolution: {integrity: sha512-9RExaLgmaw6IoIkU9cTpT71mLfI0xZ86iZH8x518LVsOkjquJMYqb9P7KpC8lgd1t0Dxs41p2pxynq4XR3Ttzw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint/binding-win32-ia32-msvc@1.59.0':
|
||||
resolution: {integrity: sha512-rUPy+JnanpPwV/aJCPnxAD1fW50+XPI0VkWr7f0vEbqcdsS8NpB24Rw6RsS7SdpFv8Dw+8ugCwao5nCFbqOUSg==}
|
||||
'@oxlint/binding-win32-ia32-msvc@1.69.0':
|
||||
resolution: {integrity: sha512-1907kRPF8/PrcIw1E7LMs9JbVrpgnt/MvFdss3an8oDkYNAACXzTntV3t3869ZZhMZxb2AzRGbz1pA/jdFatXA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint/binding-win32-x64-msvc@1.59.0':
|
||||
resolution: {integrity: sha512-xkE7puteDS/vUyRngLXW0t8WgdWoS/tfxXjhP/P7SMqPDx+hs44SpssO3h3qmTqECYEuXBUPzcAw5257Ka+ofA==}
|
||||
'@oxlint/binding-win32-x64-msvc@1.69.0':
|
||||
resolution: {integrity: sha512-w8SOXv3mT9Fi6jY8OXdXCfnvX/3KNLXGNr4HEz2TA7S4Mv/PYAOmpB8y/ge40mxvBMgGNaSaaDwZpAsQn7HtWA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -5395,10 +5395,10 @@ packages:
|
||||
eslint-import-resolver-node:
|
||||
optional: true
|
||||
|
||||
eslint-plugin-oxlint@1.59.0:
|
||||
resolution: {integrity: sha512-g0DR+xSsnUdyaMc2KAXvBVGWz5V4GwlAE1PM+ocKxl2Eg7YgOjkRLLbxgJ3bhYOhRLhD8F0X4DjJu2FSDvrvAg==}
|
||||
eslint-plugin-oxlint@1.69.0:
|
||||
resolution: {integrity: sha512-ryJT8Pqb3jgWhmQcKA/D98K6UckthAR70wPTBI4rOjcaKJ9nmQkysTLbTVVEcdzfT9mznV/2MKspBsCCpXm36w==}
|
||||
peerDependencies:
|
||||
oxlint: ~1.59.0
|
||||
oxlint: ~1.69.0
|
||||
|
||||
eslint-plugin-playwright@2.10.1:
|
||||
resolution: {integrity: sha512-qea3UxBOb8fTwJ77FMApZKvRye5DOluDHcev0LDJwID3RELeun0JlqzrNIXAB/SXCyB/AesCW/6sZfcT9q3Edg==}
|
||||
@@ -7017,24 +7017,35 @@ packages:
|
||||
oxc-resolver@11.20.0:
|
||||
resolution: {integrity: sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g==}
|
||||
|
||||
oxfmt@0.44.0:
|
||||
resolution: {integrity: sha512-lnncqvHewyRvaqdrnntVIrZV2tEddz8lbvPsQzG/zlkfvgZkwy0HP1p/2u1aCDToeg1jb9zBpbJdfkV73Itw+w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
oxlint-tsgolint@0.20.0:
|
||||
resolution: {integrity: sha512-/Uc9TQyN1l8w9QNvXtVHYtz+SzDJHKpb5X0UnHodl0BVzijUPk0LPlDOHAvogd1UI+iy9ZSF6gQxEqfzUxCULQ==}
|
||||
hasBin: true
|
||||
|
||||
oxlint@1.59.0:
|
||||
resolution: {integrity: sha512-0xBLeGGjP4vD9pygRo8iuOkOzEU1MqOnfiOl7KYezL/QvWL8NUg6n03zXc7ZVqltiOpUxBk2zgHI3PnRIEdAvw==}
|
||||
oxfmt@0.54.0:
|
||||
resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
oxlint-tsgolint: '>=0.18.0'
|
||||
svelte: ^5.0.0
|
||||
vite-plus: '*'
|
||||
peerDependenciesMeta:
|
||||
svelte:
|
||||
optional: true
|
||||
vite-plus:
|
||||
optional: true
|
||||
|
||||
oxlint-tsgolint@0.23.0:
|
||||
resolution: {integrity: sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA==}
|
||||
hasBin: true
|
||||
|
||||
oxlint@1.69.0:
|
||||
resolution: {integrity: sha512-ypZkK/aDc5NQV8zIR6s2H2Tl3aNW8FmJ1m9+2qsaYuRenl8vgnHNCGwTHviWJdUQzglOlHFchgopdtGhSy17Rw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
oxlint-tsgolint: '>=0.22.1'
|
||||
vite-plus: '*'
|
||||
peerDependenciesMeta:
|
||||
oxlint-tsgolint:
|
||||
optional: true
|
||||
vite-plus:
|
||||
optional: true
|
||||
|
||||
p-limit@3.1.0:
|
||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||
@@ -8629,8 +8640,8 @@ packages:
|
||||
vue-component-type-helpers@3.3.2:
|
||||
resolution: {integrity: sha512-l4Z2Y34m7nFMlx8vrslJaVtXxUpzgDMSESC7TakG/c5kwjYT/do+E0NcT2/vWDzaoIhsShg/2OKwX7Q4nbzC0g==}
|
||||
|
||||
vue-component-type-helpers@3.3.3:
|
||||
resolution: {integrity: sha512-x4nsFpy5Pe8fqPzp/5vkTPeTTDBpAx4WVtV47Ejt0+2FQrq4pRRsJs7JmYRqMFzTu/LW+pCWEjQ3YVCkPV7f9g==}
|
||||
vue-component-type-helpers@3.3.4:
|
||||
resolution: {integrity: sha512-joip1uZTaQR0nD23N400gIdJ7xY+WiiiMA/BCKz842gvGBknqDQAzklUvDEhqFvvrhQY8S2ZANBMu4X70VMFGw==}
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
@@ -10739,136 +10750,136 @@ snapshots:
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.20.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-android-arm-eabi@0.44.0':
|
||||
'@oxfmt/binding-android-arm-eabi@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-android-arm64@0.44.0':
|
||||
'@oxfmt/binding-android-arm64@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-darwin-arm64@0.44.0':
|
||||
'@oxfmt/binding-darwin-arm64@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-darwin-x64@0.44.0':
|
||||
'@oxfmt/binding-darwin-x64@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-freebsd-x64@0.44.0':
|
||||
'@oxfmt/binding-freebsd-x64@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-arm-gnueabihf@0.44.0':
|
||||
'@oxfmt/binding-linux-arm-gnueabihf@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-arm-musleabihf@0.44.0':
|
||||
'@oxfmt/binding-linux-arm-musleabihf@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-arm64-gnu@0.44.0':
|
||||
'@oxfmt/binding-linux-arm64-gnu@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.44.0':
|
||||
'@oxfmt/binding-linux-arm64-musl@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.44.0':
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.44.0':
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.44.0':
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.44.0':
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.44.0':
|
||||
'@oxfmt/binding-linux-x64-gnu@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.44.0':
|
||||
'@oxfmt/binding-linux-x64-musl@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.44.0':
|
||||
'@oxfmt/binding-openharmony-arm64@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-win32-arm64-msvc@0.44.0':
|
||||
'@oxfmt/binding-win32-arm64-msvc@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-win32-ia32-msvc@0.44.0':
|
||||
'@oxfmt/binding-win32-ia32-msvc@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/binding-win32-x64-msvc@0.44.0':
|
||||
'@oxfmt/binding-win32-x64-msvc@0.54.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.20.0':
|
||||
'@oxlint-tsgolint/darwin-arm64@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/darwin-x64@0.20.0':
|
||||
'@oxlint-tsgolint/darwin-x64@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/linux-arm64@0.20.0':
|
||||
'@oxlint-tsgolint/linux-arm64@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/linux-x64@0.20.0':
|
||||
'@oxlint-tsgolint/linux-x64@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/win32-arm64@0.20.0':
|
||||
'@oxlint-tsgolint/win32-arm64@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/win32-x64@0.20.0':
|
||||
'@oxlint-tsgolint/win32-x64@0.23.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-android-arm-eabi@1.59.0':
|
||||
'@oxlint/binding-android-arm-eabi@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-android-arm64@1.59.0':
|
||||
'@oxlint/binding-android-arm64@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-darwin-arm64@1.59.0':
|
||||
'@oxlint/binding-darwin-arm64@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-darwin-x64@1.59.0':
|
||||
'@oxlint/binding-darwin-x64@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-freebsd-x64@1.59.0':
|
||||
'@oxlint/binding-freebsd-x64@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-arm-gnueabihf@1.59.0':
|
||||
'@oxlint/binding-linux-arm-gnueabihf@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-arm-musleabihf@1.59.0':
|
||||
'@oxlint/binding-linux-arm-musleabihf@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-arm64-gnu@1.59.0':
|
||||
'@oxlint/binding-linux-arm64-gnu@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.59.0':
|
||||
'@oxlint/binding-linux-arm64-musl@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.59.0':
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.59.0':
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.59.0':
|
||||
'@oxlint/binding-linux-riscv64-musl@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.59.0':
|
||||
'@oxlint/binding-linux-s390x-gnu@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.59.0':
|
||||
'@oxlint/binding-linux-x64-gnu@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.59.0':
|
||||
'@oxlint/binding-linux-x64-musl@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.59.0':
|
||||
'@oxlint/binding-openharmony-arm64@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-win32-arm64-msvc@1.59.0':
|
||||
'@oxlint/binding-win32-arm64-msvc@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-win32-ia32-msvc@1.59.0':
|
||||
'@oxlint/binding-win32-ia32-msvc@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-win32-x64-msvc@1.59.0':
|
||||
'@oxlint/binding-win32-x64-msvc@1.69.0':
|
||||
optional: true
|
||||
|
||||
'@package-json/types@0.0.12': {}
|
||||
@@ -11312,7 +11323,7 @@ snapshots:
|
||||
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
vue-component-type-helpers: 3.3.3
|
||||
vue-component-type-helpers: 3.3.4
|
||||
|
||||
'@swc/helpers@0.5.21':
|
||||
dependencies:
|
||||
@@ -13448,7 +13459,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-better-tailwindcss@4.3.1(eslint@10.4.0(jiti@2.6.1))(oxlint@1.59.0(oxlint-tsgolint@0.20.0))(tailwindcss@4.3.0)(typescript@5.9.3):
|
||||
eslint-plugin-better-tailwindcss@4.3.1(eslint@10.4.0(jiti@2.6.1))(oxlint@1.69.0(oxlint-tsgolint@0.23.0))(tailwindcss@4.3.0)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@eslint/css-tree': 3.6.9
|
||||
'@valibot/to-json-schema': 1.5.0(valibot@1.2.0(typescript@5.9.3))
|
||||
@@ -13461,7 +13472,7 @@ snapshots:
|
||||
valibot: 1.2.0(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
eslint: 10.4.0(jiti@2.6.1)
|
||||
oxlint: 1.59.0(oxlint-tsgolint@0.20.0)
|
||||
oxlint: 1.69.0(oxlint-tsgolint@0.23.0)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
@@ -13483,10 +13494,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-oxlint@1.59.0(oxlint@1.59.0(oxlint-tsgolint@0.20.0)):
|
||||
eslint-plugin-oxlint@1.69.0(oxlint@1.69.0(oxlint-tsgolint@0.23.0)):
|
||||
dependencies:
|
||||
jsonc-parser: 3.3.1
|
||||
oxlint: 1.59.0(oxlint-tsgolint@0.20.0)
|
||||
oxlint: 1.69.0(oxlint-tsgolint@0.23.0)
|
||||
|
||||
eslint-plugin-playwright@2.10.1(eslint@10.4.0(jiti@2.6.1)):
|
||||
dependencies:
|
||||
@@ -15453,61 +15464,61 @@ snapshots:
|
||||
'@oxc-resolver/binding-win32-arm64-msvc': 11.20.0
|
||||
'@oxc-resolver/binding-win32-x64-msvc': 11.20.0
|
||||
|
||||
oxfmt@0.44.0:
|
||||
oxfmt@0.54.0:
|
||||
dependencies:
|
||||
tinypool: 2.1.0
|
||||
optionalDependencies:
|
||||
'@oxfmt/binding-android-arm-eabi': 0.44.0
|
||||
'@oxfmt/binding-android-arm64': 0.44.0
|
||||
'@oxfmt/binding-darwin-arm64': 0.44.0
|
||||
'@oxfmt/binding-darwin-x64': 0.44.0
|
||||
'@oxfmt/binding-freebsd-x64': 0.44.0
|
||||
'@oxfmt/binding-linux-arm-gnueabihf': 0.44.0
|
||||
'@oxfmt/binding-linux-arm-musleabihf': 0.44.0
|
||||
'@oxfmt/binding-linux-arm64-gnu': 0.44.0
|
||||
'@oxfmt/binding-linux-arm64-musl': 0.44.0
|
||||
'@oxfmt/binding-linux-ppc64-gnu': 0.44.0
|
||||
'@oxfmt/binding-linux-riscv64-gnu': 0.44.0
|
||||
'@oxfmt/binding-linux-riscv64-musl': 0.44.0
|
||||
'@oxfmt/binding-linux-s390x-gnu': 0.44.0
|
||||
'@oxfmt/binding-linux-x64-gnu': 0.44.0
|
||||
'@oxfmt/binding-linux-x64-musl': 0.44.0
|
||||
'@oxfmt/binding-openharmony-arm64': 0.44.0
|
||||
'@oxfmt/binding-win32-arm64-msvc': 0.44.0
|
||||
'@oxfmt/binding-win32-ia32-msvc': 0.44.0
|
||||
'@oxfmt/binding-win32-x64-msvc': 0.44.0
|
||||
'@oxfmt/binding-android-arm-eabi': 0.54.0
|
||||
'@oxfmt/binding-android-arm64': 0.54.0
|
||||
'@oxfmt/binding-darwin-arm64': 0.54.0
|
||||
'@oxfmt/binding-darwin-x64': 0.54.0
|
||||
'@oxfmt/binding-freebsd-x64': 0.54.0
|
||||
'@oxfmt/binding-linux-arm-gnueabihf': 0.54.0
|
||||
'@oxfmt/binding-linux-arm-musleabihf': 0.54.0
|
||||
'@oxfmt/binding-linux-arm64-gnu': 0.54.0
|
||||
'@oxfmt/binding-linux-arm64-musl': 0.54.0
|
||||
'@oxfmt/binding-linux-ppc64-gnu': 0.54.0
|
||||
'@oxfmt/binding-linux-riscv64-gnu': 0.54.0
|
||||
'@oxfmt/binding-linux-riscv64-musl': 0.54.0
|
||||
'@oxfmt/binding-linux-s390x-gnu': 0.54.0
|
||||
'@oxfmt/binding-linux-x64-gnu': 0.54.0
|
||||
'@oxfmt/binding-linux-x64-musl': 0.54.0
|
||||
'@oxfmt/binding-openharmony-arm64': 0.54.0
|
||||
'@oxfmt/binding-win32-arm64-msvc': 0.54.0
|
||||
'@oxfmt/binding-win32-ia32-msvc': 0.54.0
|
||||
'@oxfmt/binding-win32-x64-msvc': 0.54.0
|
||||
|
||||
oxlint-tsgolint@0.20.0:
|
||||
oxlint-tsgolint@0.23.0:
|
||||
optionalDependencies:
|
||||
'@oxlint-tsgolint/darwin-arm64': 0.20.0
|
||||
'@oxlint-tsgolint/darwin-x64': 0.20.0
|
||||
'@oxlint-tsgolint/linux-arm64': 0.20.0
|
||||
'@oxlint-tsgolint/linux-x64': 0.20.0
|
||||
'@oxlint-tsgolint/win32-arm64': 0.20.0
|
||||
'@oxlint-tsgolint/win32-x64': 0.20.0
|
||||
'@oxlint-tsgolint/darwin-arm64': 0.23.0
|
||||
'@oxlint-tsgolint/darwin-x64': 0.23.0
|
||||
'@oxlint-tsgolint/linux-arm64': 0.23.0
|
||||
'@oxlint-tsgolint/linux-x64': 0.23.0
|
||||
'@oxlint-tsgolint/win32-arm64': 0.23.0
|
||||
'@oxlint-tsgolint/win32-x64': 0.23.0
|
||||
|
||||
oxlint@1.59.0(oxlint-tsgolint@0.20.0):
|
||||
oxlint@1.69.0(oxlint-tsgolint@0.23.0):
|
||||
optionalDependencies:
|
||||
'@oxlint/binding-android-arm-eabi': 1.59.0
|
||||
'@oxlint/binding-android-arm64': 1.59.0
|
||||
'@oxlint/binding-darwin-arm64': 1.59.0
|
||||
'@oxlint/binding-darwin-x64': 1.59.0
|
||||
'@oxlint/binding-freebsd-x64': 1.59.0
|
||||
'@oxlint/binding-linux-arm-gnueabihf': 1.59.0
|
||||
'@oxlint/binding-linux-arm-musleabihf': 1.59.0
|
||||
'@oxlint/binding-linux-arm64-gnu': 1.59.0
|
||||
'@oxlint/binding-linux-arm64-musl': 1.59.0
|
||||
'@oxlint/binding-linux-ppc64-gnu': 1.59.0
|
||||
'@oxlint/binding-linux-riscv64-gnu': 1.59.0
|
||||
'@oxlint/binding-linux-riscv64-musl': 1.59.0
|
||||
'@oxlint/binding-linux-s390x-gnu': 1.59.0
|
||||
'@oxlint/binding-linux-x64-gnu': 1.59.0
|
||||
'@oxlint/binding-linux-x64-musl': 1.59.0
|
||||
'@oxlint/binding-openharmony-arm64': 1.59.0
|
||||
'@oxlint/binding-win32-arm64-msvc': 1.59.0
|
||||
'@oxlint/binding-win32-ia32-msvc': 1.59.0
|
||||
'@oxlint/binding-win32-x64-msvc': 1.59.0
|
||||
oxlint-tsgolint: 0.20.0
|
||||
'@oxlint/binding-android-arm-eabi': 1.69.0
|
||||
'@oxlint/binding-android-arm64': 1.69.0
|
||||
'@oxlint/binding-darwin-arm64': 1.69.0
|
||||
'@oxlint/binding-darwin-x64': 1.69.0
|
||||
'@oxlint/binding-freebsd-x64': 1.69.0
|
||||
'@oxlint/binding-linux-arm-gnueabihf': 1.69.0
|
||||
'@oxlint/binding-linux-arm-musleabihf': 1.69.0
|
||||
'@oxlint/binding-linux-arm64-gnu': 1.69.0
|
||||
'@oxlint/binding-linux-arm64-musl': 1.69.0
|
||||
'@oxlint/binding-linux-ppc64-gnu': 1.69.0
|
||||
'@oxlint/binding-linux-riscv64-gnu': 1.69.0
|
||||
'@oxlint/binding-linux-riscv64-musl': 1.69.0
|
||||
'@oxlint/binding-linux-s390x-gnu': 1.69.0
|
||||
'@oxlint/binding-linux-x64-gnu': 1.69.0
|
||||
'@oxlint/binding-linux-x64-musl': 1.69.0
|
||||
'@oxlint/binding-openharmony-arm64': 1.69.0
|
||||
'@oxlint/binding-win32-arm64-msvc': 1.69.0
|
||||
'@oxlint/binding-win32-ia32-msvc': 1.69.0
|
||||
'@oxlint/binding-win32-x64-msvc': 1.69.0
|
||||
oxlint-tsgolint: 0.23.0
|
||||
|
||||
p-limit@3.1.0:
|
||||
dependencies:
|
||||
@@ -17458,7 +17469,7 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@3.3.2: {}
|
||||
|
||||
vue-component-type-helpers@3.3.3: {}
|
||||
vue-component-type-helpers@3.3.4: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.34(typescript@5.9.3)):
|
||||
dependencies:
|
||||
|
||||
@@ -79,7 +79,7 @@ catalog:
|
||||
eslint-import-resolver-typescript: ^4.4.4
|
||||
eslint-plugin-better-tailwindcss: ^4.3.1
|
||||
eslint-plugin-import-x: ^4.16.2
|
||||
eslint-plugin-oxlint: 1.59.0
|
||||
eslint-plugin-oxlint: 1.69.0
|
||||
eslint-plugin-playwright: ^2.10.1
|
||||
eslint-plugin-storybook: ^10.2.10
|
||||
eslint-plugin-testing-library: ^7.16.1
|
||||
@@ -101,9 +101,9 @@ catalog:
|
||||
markdown-table: ^3.0.4
|
||||
mixpanel-browser: ^2.71.0
|
||||
monocart-coverage-reports: ^2.12.9
|
||||
oxfmt: ^0.44.0
|
||||
oxlint: ^1.59.0
|
||||
oxlint-tsgolint: ^0.20.0
|
||||
oxfmt: ^0.54.0
|
||||
oxlint: ^1.69.0
|
||||
oxlint-tsgolint: ^0.23.0
|
||||
picocolors: ^1.1.1
|
||||
pinia: ^3.0.4
|
||||
postcss-html: ^1.8.0
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -87,6 +87,14 @@ vi.mock('@/scripts/app', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
const mockTrackUiButtonClicked = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: () => ({
|
||||
trackUiButtonClicked: mockTrackUiButtonClicked
|
||||
})
|
||||
}))
|
||||
|
||||
type WrapperOptions = {
|
||||
pinia?: ReturnType<typeof createTestingPinia>
|
||||
stubs?: Record<string, boolean | Component>
|
||||
@@ -110,6 +118,9 @@ function createWrapper({
|
||||
activeJobsShort: '{count} active | {count} active',
|
||||
clearQueueTooltip: 'Clear queue'
|
||||
}
|
||||
},
|
||||
rightSidePanel: {
|
||||
togglePanel: 'Toggle properties panel'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,6 +277,19 @@ describe('TopMenuSection', () => {
|
||||
expect(screen.queryByTestId('active-jobs-indicator')).toBeNull()
|
||||
})
|
||||
|
||||
it('tracks right side panel opens', async () => {
|
||||
const { user } = createWrapper()
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Toggle properties panel' })
|
||||
)
|
||||
|
||||
expect(mockTrackUiButtonClicked).toHaveBeenCalledWith({
|
||||
button_id: 'right_side_panel_opened',
|
||||
element_group: 'top_menu'
|
||||
})
|
||||
})
|
||||
|
||||
it('hides queue progress overlay when QPO V2 is enabled', async () => {
|
||||
const pinia = createTestingPinia({ createSpy: vi.fn })
|
||||
const settingStore = useSettingStore(pinia)
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('rightSidePanel.togglePanel')"
|
||||
@click="rightSidePanelStore.togglePanel"
|
||||
@click="openRightSidePanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
</Button>
|
||||
@@ -148,6 +148,7 @@ import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useActionBarButtonStore } from '@/stores/actionBarButtonStore'
|
||||
@@ -282,6 +283,14 @@ const rightSidePanelTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('rightSidePanel.togglePanel'))
|
||||
)
|
||||
|
||||
function openRightSidePanel() {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'right_side_panel_opened',
|
||||
element_group: 'top_menu'
|
||||
})
|
||||
rightSidePanelStore.togglePanel()
|
||||
}
|
||||
|
||||
// Maintain support for legacy topbar elements attached by custom scripts
|
||||
const legacyCommandsContainerRef = ref<HTMLElement>()
|
||||
const hasLegacyContent = ref(false)
|
||||
|
||||
@@ -222,7 +222,8 @@ watch(visible, async (newVisible) => {
|
||||
*/
|
||||
useEventListener(dragHandleRef, 'mousedown', () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'actionbar_run_handle_drag_start'
|
||||
button_id: 'actionbar_run_handle_drag_start',
|
||||
element_group: 'actionbar'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -131,7 +131,8 @@ const queueModeMenuItemLookup = computed<Record<string, QueueModeMenuItem>>(
|
||||
tooltip: t('menu.onChangeTooltip'),
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_mode_option_run_on_change_selected'
|
||||
button_id: 'queue_mode_option_run_on_change_selected',
|
||||
element_group: 'queue'
|
||||
})
|
||||
queueMode.value = 'change'
|
||||
}
|
||||
@@ -145,7 +146,8 @@ const queueModeMenuItemLookup = computed<Record<string, QueueModeMenuItem>>(
|
||||
tooltip: t('menu.instantTooltip'),
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_mode_option_run_instant_selected'
|
||||
button_id: 'queue_mode_option_run_instant_selected',
|
||||
element_group: 'queue'
|
||||
})
|
||||
queueMode.value = 'instant-idle'
|
||||
}
|
||||
@@ -237,7 +239,8 @@ const queuePrompt = async (e: Event) => {
|
||||
|
||||
if (batchCount.value > 1) {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_run_multiple_batches_submitted'
|
||||
button_id: 'queue_run_multiple_batches_submitted',
|
||||
element_group: 'queue'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,8 @@ const home = computed(() => ({
|
||||
isBlueprint: isBlueprint.value,
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'breadcrumb_subgraph_root_selected'
|
||||
button_id: 'breadcrumb_subgraph_root_selected',
|
||||
element_group: 'breadcrumb'
|
||||
})
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
||||
@@ -103,7 +104,8 @@ const items = computed(() => {
|
||||
key: `subgraph-${subgraph.id}`,
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'breadcrumb_subgraph_item_selected'
|
||||
button_id: 'breadcrumb_subgraph_item_selected',
|
||||
element_group: 'breadcrumb'
|
||||
})
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
||||
|
||||
@@ -33,7 +33,6 @@ const {
|
||||
items,
|
||||
gridStyle,
|
||||
bufferRows = 1,
|
||||
scrollThrottle = 64,
|
||||
resizeDebounce = 64,
|
||||
defaultItemHeight = 200,
|
||||
defaultItemWidth = 200,
|
||||
@@ -42,7 +41,6 @@ const {
|
||||
items: (T & { key: string })[]
|
||||
gridStyle: CSSProperties
|
||||
bufferRows?: number
|
||||
scrollThrottle?: number
|
||||
resizeDebounce?: number
|
||||
defaultItemHeight?: number
|
||||
defaultItemWidth?: number
|
||||
@@ -61,7 +59,6 @@ const itemWidth = ref(defaultItemWidth)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const { width, height } = useElementSize(container)
|
||||
const { y: scrollY } = useScroll(container, {
|
||||
throttle: scrollThrottle,
|
||||
eventListenerOptions: { passive: true }
|
||||
})
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ function handleOpen(open: boolean) {
|
||||
if (open) {
|
||||
markAsSeen()
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: source
|
||||
button_id: source,
|
||||
element_group: 'workflow_actions'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,8 @@ const reportOpen = ref(false)
|
||||
*/
|
||||
const showReport = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'error_dialog_show_report_clicked'
|
||||
button_id: 'error_dialog_show_report_clicked',
|
||||
element_group: 'error_dialog'
|
||||
})
|
||||
reportOpen.value = true
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ const queryString = computed(() => props.errorMessage + ' is:issue')
|
||||
|
||||
function openGitHubIssues() {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'error_dialog_find_existing_issues_clicked'
|
||||
button_id: 'error_dialog_find_existing_issues_clicked',
|
||||
element_group: 'error_dialog'
|
||||
})
|
||||
const query = encodeURIComponent(queryString.value)
|
||||
const url = `https://github.com/${props.repoOwner}/${props.repoName}/issues?q=${query}`
|
||||
|
||||
@@ -218,7 +218,8 @@ onMounted(() => {
|
||||
*/
|
||||
const onMinimapToggleClick = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'graph_menu_minimap_toggle_clicked'
|
||||
button_id: 'graph_menu_minimap_toggle_clicked',
|
||||
element_group: 'graph_menu'
|
||||
})
|
||||
void commandStore.execute('Comfy.Canvas.ToggleMinimap')
|
||||
}
|
||||
@@ -228,7 +229,8 @@ const onMinimapToggleClick = () => {
|
||||
*/
|
||||
const onLinkVisibilityToggleClick = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'graph_menu_hide_links_toggle_clicked'
|
||||
button_id: 'graph_menu_hide_links_toggle_clicked',
|
||||
element_group: 'graph_menu'
|
||||
})
|
||||
void commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
||||
|
||||
const {
|
||||
hasAnySelection,
|
||||
hasGroupedNodesSelection,
|
||||
hasMultipleSelection,
|
||||
isSingleNode,
|
||||
isSingleSubgraph,
|
||||
@@ -118,7 +119,10 @@ const showSubgraphButtons = computed(() => isSingleSubgraph.value)
|
||||
|
||||
const showBypass = computed(
|
||||
() =>
|
||||
isSingleNode.value || isSingleSubgraph.value || hasMultipleSelection.value
|
||||
isSingleNode.value ||
|
||||
isSingleSubgraph.value ||
|
||||
hasMultipleSelection.value ||
|
||||
hasGroupedNodesSelection.value
|
||||
)
|
||||
const showLoad3DViewer = computed(() => hasAny3DNodeSelected.value)
|
||||
const showMaskEditor = computed(() => isSingleImageNode.value)
|
||||
|
||||
@@ -65,7 +65,8 @@ describe('InfoButton', () => {
|
||||
|
||||
expect(openNodeInfoMock).toHaveBeenCalled()
|
||||
expect(trackUiButtonClickedMock).toHaveBeenCalledWith({
|
||||
button_id: 'selection_toolbox_node_info_opened'
|
||||
button_id: 'selection_toolbox_node_info_opened',
|
||||
element_group: 'selection_toolbox'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ const onInfoClick = () => {
|
||||
if (!openNodeInfo()) return
|
||||
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'selection_toolbox_node_info_opened'
|
||||
button_id: 'selection_toolbox_node_info_opened',
|
||||
element_group: 'selection_toolbox'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
:can-use-background-image="canUseBackgroundImage"
|
||||
:material-modes="materialModes"
|
||||
:has-skeleton="hasSkeleton"
|
||||
:source-format="sourceFormat"
|
||||
@update-background-image="handleBackgroundImageUpdate"
|
||||
@export-model="handleExportModel"
|
||||
@update-hdri-file="handleHDRIFileUpdate"
|
||||
@@ -166,6 +167,7 @@ const {
|
||||
canExport,
|
||||
materialModes,
|
||||
hasSkeleton,
|
||||
sourceFormat,
|
||||
hasRecording,
|
||||
recordingDuration,
|
||||
animations,
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
|
||||
<ExportControls
|
||||
v-if="showExportControls"
|
||||
:source-format="sourceFormat"
|
||||
@export-model="handleExportModel"
|
||||
/>
|
||||
|
||||
@@ -134,7 +135,8 @@ const {
|
||||
canUseHdri = true,
|
||||
canUseBackgroundImage = true,
|
||||
materialModes = ['original', 'normal', 'wireframe'],
|
||||
hasSkeleton = false
|
||||
hasSkeleton = false,
|
||||
sourceFormat = null
|
||||
} = defineProps<{
|
||||
canUseGizmo?: boolean
|
||||
canUseLighting?: boolean
|
||||
@@ -143,6 +145,7 @@ const {
|
||||
canUseBackgroundImage?: boolean
|
||||
materialModes?: readonly MaterialMode[]
|
||||
hasSkeleton?: boolean
|
||||
sourceFormat?: string | null
|
||||
}>()
|
||||
|
||||
const sceneConfig = defineModel<SceneConfig>('sceneConfig')
|
||||
|
||||
@@ -59,6 +59,7 @@ function buildViewerStub() {
|
||||
canUseGizmo: ref(true),
|
||||
canUseLighting: ref(true),
|
||||
canExport: ref(true),
|
||||
sourceFormat: ref<string | null>(null),
|
||||
materialModes: ref(['original', 'normal', 'wireframe']),
|
||||
animations: ref<Array<{ name: string; index: number }>>([]),
|
||||
playing: ref(false),
|
||||
|
||||
@@ -82,7 +82,10 @@
|
||||
</div>
|
||||
|
||||
<div v-if="viewer.canExport.value" class="space-y-4 p-2">
|
||||
<ExportControls @export-model="viewer.exportModel" />
|
||||
<ExportControls
|
||||
:source-format="viewer.sourceFormat.value"
|
||||
@export-model="viewer.exportModel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,9 +13,12 @@ const i18n = createI18n({
|
||||
}
|
||||
})
|
||||
|
||||
function renderComponent(onExportModel?: (format: string) => void) {
|
||||
function renderComponent(
|
||||
onExportModel?: (format: string) => void,
|
||||
sourceFormat: string | null = null
|
||||
) {
|
||||
const utils = render(ExportControls, {
|
||||
props: { onExportModel },
|
||||
props: { onExportModel, sourceFormat },
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: { tooltip: () => {} }
|
||||
@@ -63,6 +66,23 @@ describe('ExportControls', () => {
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('offers only the source format for direct-export files (e.g. ply)', async () => {
|
||||
const onExportModel = vi.fn()
|
||||
const { user } = renderComponent(onExportModel, 'ply')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Export model' }))
|
||||
|
||||
expect(screen.getByRole('button', { name: 'PLY' })).toBeVisible()
|
||||
for (const label of ['GLB', 'OBJ', 'STL', 'FBX']) {
|
||||
expect(
|
||||
screen.queryByRole('button', { name: label })
|
||||
).not.toBeInTheDocument()
|
||||
}
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'PLY' }))
|
||||
expect(onExportModel).toHaveBeenCalledWith('ply')
|
||||
})
|
||||
|
||||
it('hides the popup when a click happens outside the trigger', async () => {
|
||||
const { user } = renderComponent()
|
||||
|
||||
|
||||
@@ -35,9 +35,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { getExportFormatOptions } from '@/extensions/core/load3d/constants'
|
||||
|
||||
const { sourceFormat = null } = defineProps<{
|
||||
sourceFormat?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'exportModel', format: string): void
|
||||
@@ -45,12 +50,7 @@ const emit = defineEmits<{
|
||||
|
||||
const showExportFormats = ref(false)
|
||||
|
||||
const exportFormats = [
|
||||
{ label: 'GLB', value: 'glb' },
|
||||
{ label: 'OBJ', value: 'obj' },
|
||||
{ label: 'STL', value: 'stl' },
|
||||
{ label: 'FBX', value: 'fbx' }
|
||||
]
|
||||
const exportFormats = computed(() => getExportFormatOptions(sourceFormat))
|
||||
|
||||
function toggleExportFormats() {
|
||||
showExportFormats.value = !showExportFormats.value
|
||||
|
||||
@@ -72,9 +72,12 @@ const i18n = createI18n({
|
||||
messages: { en: { load3d: { export: 'Export' } } }
|
||||
})
|
||||
|
||||
function renderComponent(onExportModel?: (format: string) => void) {
|
||||
function renderComponent(
|
||||
onExportModel?: (format: string) => void,
|
||||
sourceFormat: string | null = null
|
||||
) {
|
||||
const utils = render(ViewerExportControls, {
|
||||
props: { onExportModel },
|
||||
props: { onExportModel, sourceFormat },
|
||||
global: { plugins: [i18n] }
|
||||
})
|
||||
return { ...utils, user: userEvent.setup() }
|
||||
@@ -114,4 +117,32 @@ describe('ViewerExportControls', () => {
|
||||
|
||||
expect(onExportModel).toHaveBeenCalledWith('glb')
|
||||
})
|
||||
|
||||
it('offers only the source format for direct-export files (e.g. spz)', async () => {
|
||||
const onExportModel = vi.fn()
|
||||
const { user } = renderComponent(onExportModel, 'spz')
|
||||
const select = screen.getByRole('combobox') as HTMLSelectElement
|
||||
|
||||
expect(Array.from(select.options).map((o) => o.value)).toEqual(['spz'])
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Export' }))
|
||||
|
||||
expect(onExportModel).toHaveBeenCalledWith('spz')
|
||||
})
|
||||
|
||||
it('repairs the selected format when sourceFormat switches to a direct-export type', async () => {
|
||||
const onExportModel = vi.fn()
|
||||
const { user, rerender } = renderComponent(onExportModel, null)
|
||||
const select = screen.getByRole('combobox') as HTMLSelectElement
|
||||
|
||||
expect(select.value).toBe('obj')
|
||||
|
||||
await rerender({ onExportModel, sourceFormat: 'ply' })
|
||||
|
||||
expect(Array.from(select.options).map((o) => o.value)).toEqual(['ply'])
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Export' }))
|
||||
|
||||
expect(onExportModel).toHaveBeenCalledWith('ply')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Select from '@/components/ui/select/Select.vue'
|
||||
@@ -34,20 +34,30 @@ import SelectContent from '@/components/ui/select/SelectContent.vue'
|
||||
import SelectItem from '@/components/ui/select/SelectItem.vue'
|
||||
import SelectTrigger from '@/components/ui/select/SelectTrigger.vue'
|
||||
import SelectValue from '@/components/ui/select/SelectValue.vue'
|
||||
import { getExportFormatOptions } from '@/extensions/core/load3d/constants'
|
||||
|
||||
const { sourceFormat = null } = defineProps<{
|
||||
sourceFormat?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'exportModel', format: string): void
|
||||
}>()
|
||||
|
||||
const exportFormats = [
|
||||
{ label: 'GLB', value: 'glb' },
|
||||
{ label: 'OBJ', value: 'obj' },
|
||||
{ label: 'STL', value: 'stl' },
|
||||
{ label: 'FBX', value: 'fbx' }
|
||||
]
|
||||
const exportFormats = computed(() => getExportFormatOptions(sourceFormat))
|
||||
|
||||
const exportFormat = ref('obj')
|
||||
|
||||
watch(
|
||||
exportFormats,
|
||||
(formats) => {
|
||||
if (!formats.some((fmt) => fmt.value === exportFormat.value)) {
|
||||
exportFormat.value = formats[0]?.value ?? ''
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const exportModel = (format: string) => {
|
||||
emit('exportModel', format)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getActiveGraphNodeIds } from '@/utils/graphTraversalUtil'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import { useMissingMediaStore } from '@/platform/missingMedia/missingMediaStore'
|
||||
@@ -106,6 +107,10 @@ const isSingleSubgraphNode = computed(() => {
|
||||
})
|
||||
|
||||
function closePanel() {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'right_side_panel_closed',
|
||||
element_group: 'right_side_panel'
|
||||
})
|
||||
rightSidePanelStore.closePanel()
|
||||
}
|
||||
|
||||
|
||||
@@ -530,7 +530,9 @@ describe('TabErrors.vue', () => {
|
||||
expect(
|
||||
screen.getByText('Some nodes can be replaced with alternatives')
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText('OldSampler (1)')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'OldSampler' })
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText('KSampler')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', { name: /Replace Node/ })
|
||||
|
||||
@@ -157,7 +157,6 @@
|
||||
<SwapNodesCard
|
||||
v-if="group.type === 'swap_nodes'"
|
||||
:swap-node-groups="swapNodeGroups"
|
||||
:show-node-id-badge="showNodeIdBadge"
|
||||
@locate-node="handleLocateMissingNode"
|
||||
@replace="handleReplaceGroup"
|
||||
/>
|
||||
|
||||
@@ -58,7 +58,8 @@ describe('useErrorActions', () => {
|
||||
openGitHubIssues()
|
||||
|
||||
expect(mocks.trackUiButtonClicked).toHaveBeenCalledWith({
|
||||
button_id: 'error_tab_github_issues_clicked'
|
||||
button_id: 'error_tab_github_issues_clicked',
|
||||
element_group: 'errors_panel'
|
||||
})
|
||||
expect(windowOpenSpy).toHaveBeenCalledWith(
|
||||
mocks.staticUrls.githubIssues,
|
||||
@@ -123,7 +124,8 @@ describe('useErrorActions', () => {
|
||||
findOnGitHub('CUDA out of memory')
|
||||
|
||||
expect(mocks.trackUiButtonClicked).toHaveBeenCalledWith({
|
||||
button_id: 'error_tab_find_existing_issues_clicked'
|
||||
button_id: 'error_tab_find_existing_issues_clicked',
|
||||
element_group: 'errors_panel'
|
||||
})
|
||||
const expectedQuery = encodeURIComponent('CUDA out of memory is:issue')
|
||||
expect(windowOpenSpy).toHaveBeenCalledWith(
|
||||
|
||||
@@ -9,7 +9,8 @@ export function useErrorActions() {
|
||||
|
||||
function openGitHubIssues() {
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'error_tab_github_issues_clicked'
|
||||
button_id: 'error_tab_github_issues_clicked',
|
||||
element_group: 'errors_panel'
|
||||
})
|
||||
window.open(staticUrls.githubIssues, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
@@ -25,7 +26,8 @@ export function useErrorActions() {
|
||||
|
||||
function findOnGitHub(errorMessage: string) {
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'error_tab_find_existing_issues_clicked'
|
||||
button_id: 'error_tab_find_existing_issues_clicked',
|
||||
element_group: 'errors_panel'
|
||||
})
|
||||
const query = encodeURIComponent(errorMessage + ' is:issue')
|
||||
window.open(
|
||||
|
||||
@@ -5,12 +5,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed, defineComponent, nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { RootCategory } from '@/components/searchbox/v2/rootCategories'
|
||||
import { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import type { Settings } from '@/schemas/apiSchema'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import type { FuseFilter, FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
|
||||
import NodeSearchBoxPopover from './NodeSearchBoxPopover.vue'
|
||||
@@ -74,8 +71,7 @@ describe('NodeSearchBoxPopover', () => {
|
||||
const NodeSearchContentStub = defineComponent({
|
||||
name: 'NodeSearchContent',
|
||||
props: {
|
||||
filters: { type: Array, default: () => [] },
|
||||
defaultRootFilter: { type: String, default: null }
|
||||
filters: { type: Array, default: () => [] }
|
||||
},
|
||||
emits: ['addFilter', 'removeFilter', 'addNode', 'hoverNode'],
|
||||
setup(_, { emit }) {
|
||||
@@ -83,8 +79,7 @@ describe('NodeSearchBoxPopover', () => {
|
||||
emit('addNode', nodeDef, dragEvent)
|
||||
return {}
|
||||
},
|
||||
template:
|
||||
'<div data-testid="search-content-v2" :data-default-root-filter="defaultRootFilter"></div>'
|
||||
template: '<div data-testid="search-content-v2"></div>'
|
||||
})
|
||||
|
||||
const pinia = createTestingPinia({
|
||||
@@ -281,75 +276,4 @@ describe('NodeSearchBoxPopover', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('defaultRootFilter on dialog open', () => {
|
||||
function setGraphNodes(nodes: unknown[]) {
|
||||
const canvasStore = useCanvasStore()
|
||||
canvasStore.canvas = {
|
||||
graph: { nodes },
|
||||
allow_searchbox: false,
|
||||
setDirty: vi.fn(),
|
||||
linkConnector: {
|
||||
events: new EventTarget(),
|
||||
reset: vi.fn(),
|
||||
disconnectLinks: vi.fn()
|
||||
}
|
||||
} as unknown as ReturnType<typeof useCanvasStore>['canvas']
|
||||
}
|
||||
|
||||
async function openSearch() {
|
||||
useSearchBoxStore().visible = true
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
it('defaults to Essentials when the graph is empty', async () => {
|
||||
renderComponent({ 'Comfy.NodeSearchBoxImpl': 'default' })
|
||||
setGraphNodes([])
|
||||
await openSearch()
|
||||
|
||||
expect(screen.getByTestId('search-content-v2')).toHaveAttribute(
|
||||
'data-default-root-filter',
|
||||
RootCategory.Essentials
|
||||
)
|
||||
})
|
||||
|
||||
it('defaults to Essentials when the canvas is not yet available', async () => {
|
||||
renderComponent({ 'Comfy.NodeSearchBoxImpl': 'default' })
|
||||
await openSearch()
|
||||
|
||||
expect(screen.getByTestId('search-content-v2')).toHaveAttribute(
|
||||
'data-default-root-filter',
|
||||
RootCategory.Essentials
|
||||
)
|
||||
})
|
||||
|
||||
it('defaults to null when the graph has nodes', async () => {
|
||||
renderComponent({ 'Comfy.NodeSearchBoxImpl': 'default' })
|
||||
setGraphNodes([{ id: 1 }])
|
||||
await openSearch()
|
||||
|
||||
expect(screen.getByTestId('search-content-v2')).not.toHaveAttribute(
|
||||
'data-default-root-filter'
|
||||
)
|
||||
})
|
||||
|
||||
it('re-evaluates each time the dialog opens', async () => {
|
||||
renderComponent({ 'Comfy.NodeSearchBoxImpl': 'default' })
|
||||
|
||||
setGraphNodes([])
|
||||
await openSearch()
|
||||
expect(screen.getByTestId('search-content-v2')).toHaveAttribute(
|
||||
'data-default-root-filter',
|
||||
RootCategory.Essentials
|
||||
)
|
||||
|
||||
useSearchBoxStore().visible = false
|
||||
await nextTick()
|
||||
setGraphNodes([{ id: 1 }])
|
||||
await openSearch()
|
||||
expect(screen.getByTestId('search-content-v2')).not.toHaveAttribute(
|
||||
'data-default-root-filter'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
<div v-if="useSearchBoxV2" role="search" class="relative">
|
||||
<NodeSearchContent
|
||||
:filters="nodeFilters"
|
||||
:default-root-filter="defaultRootFilter"
|
||||
@add-filter="addFilter"
|
||||
@remove-filter="removeFilter"
|
||||
@add-node="addNode"
|
||||
@@ -78,8 +77,6 @@ import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
|
||||
import type { FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
|
||||
import NodePreviewCard from '@/components/node/NodePreviewCard.vue'
|
||||
import { RootCategory } from '@/components/searchbox/v2/rootCategories'
|
||||
import type { RootCategoryId } from '@/components/searchbox/v2/rootCategories'
|
||||
|
||||
import NodeSearchContent from './v2/NodeSearchContent.vue'
|
||||
import NodeSearchBox from './NodeSearchBox.vue'
|
||||
@@ -91,7 +88,6 @@ let disconnectOnReset = false
|
||||
const settingStore = useSettingStore()
|
||||
const searchBoxStore = useSearchBoxStore()
|
||||
const litegraphService = useLitegraphService()
|
||||
const canvasStore = useCanvasStore()
|
||||
const { trackFeatureUsed } = useSurveyFeatureTracking('node-search')
|
||||
|
||||
const { visible, newSearchBoxEnabled, useSearchBoxV2 } =
|
||||
@@ -107,13 +103,6 @@ const enableNodePreview = computed(
|
||||
settingStore.get('Comfy.NodeSearchBoxImpl.NodePreview') &&
|
||||
windowWidth.value >= MIN_WIDTH_FOR_PREVIEW
|
||||
)
|
||||
const defaultRootFilter = ref<RootCategoryId | null>(null)
|
||||
watch(visible, (isVisible) => {
|
||||
if (!isVisible) return
|
||||
defaultRootFilter.value = !canvasStore.canvas?.graph?.nodes?.length
|
||||
? RootCategory.Essentials
|
||||
: null
|
||||
})
|
||||
function getNewNodeLocation(): Point {
|
||||
return triggerEvent
|
||||
? [triggerEvent.canvasX, triggerEvent.canvasY]
|
||||
@@ -138,6 +127,7 @@ function clearFilters() {
|
||||
function closeDialog() {
|
||||
visible.value = false
|
||||
}
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
function addNode(nodeDef: ComfyNodeDefImpl, dragEvent?: MouseEvent) {
|
||||
const followCursor = settingStore.get('Comfy.NodeSearchBoxImpl.FollowCursor')
|
||||
|
||||
@@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import NodeSearchContent from '@/components/searchbox/v2/NodeSearchContent.vue'
|
||||
import { RootCategory } from '@/components/searchbox/v2/rootCategories'
|
||||
import {
|
||||
createMockNodeDef,
|
||||
setViewport,
|
||||
@@ -231,48 +230,6 @@ describe('NodeSearchContent', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply defaultRootFilter when provided and category is available', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({
|
||||
name: 'EssentialNode',
|
||||
display_name: 'Essential Node',
|
||||
essentials_category: 'basic'
|
||||
}),
|
||||
createMockNodeDef({
|
||||
name: 'RegularNode',
|
||||
display_name: 'Regular Node'
|
||||
})
|
||||
])
|
||||
|
||||
renderComponent({ defaultRootFilter: RootCategory.Essentials })
|
||||
|
||||
await waitFor(() => {
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('Essential Node')
|
||||
})
|
||||
})
|
||||
|
||||
it('should ignore defaultRootFilter of Essentials when no essentials exist', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({
|
||||
name: 'FrequentNode',
|
||||
display_name: 'Frequent Node'
|
||||
})
|
||||
])
|
||||
vi.spyOn(useNodeFrequencyStore(), 'topNodeDefs', 'get').mockReturnValue([
|
||||
useNodeDefStore().nodeDefsByName['FrequentNode']
|
||||
])
|
||||
|
||||
renderComponent({ defaultRootFilter: RootCategory.Essentials })
|
||||
|
||||
await waitFor(() => {
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('Frequent Node')
|
||||
})
|
||||
})
|
||||
|
||||
it('should show only API nodes when Partner Nodes filter is active', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({
|
||||
|
||||
@@ -142,9 +142,8 @@ const sourceCategoryFilters: Record<string, (n: ComfyNodeDefImpl) => boolean> =
|
||||
[RootCategory.Custom]: isCustomNode
|
||||
}
|
||||
|
||||
const { filters, defaultRootFilter = null } = defineProps<{
|
||||
const { filters } = defineProps<{
|
||||
filters: FuseFilterWithValue<ComfyNodeDefImpl, string>[]
|
||||
defaultRootFilter?: RootCategoryId | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -195,12 +194,8 @@ function onSearchFocus() {
|
||||
if (isMobile.value) isSidebarOpen.value = false
|
||||
}
|
||||
|
||||
const rootFilter = ref<RootCategoryId | null>(
|
||||
defaultRootFilter === RootCategory.Essentials &&
|
||||
!nodeAvailability.value.essential
|
||||
? null
|
||||
: defaultRootFilter
|
||||
)
|
||||
// Root filter from filter bar category buttons (radio toggle)
|
||||
const rootFilter = ref<RootCategoryId | null>(null)
|
||||
|
||||
const rootFilterLabel = computed(() => {
|
||||
switch (rootFilter.value) {
|
||||
|
||||
@@ -150,7 +150,8 @@ const telemetry = useTelemetry()
|
||||
|
||||
function onLogoMenuClick(event: MouseEvent) {
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_comfy_menu_opened'
|
||||
button_id: 'sidebar_comfy_menu_opened',
|
||||
element_group: 'sidebar'
|
||||
})
|
||||
menuRef.value?.toggle(event)
|
||||
}
|
||||
@@ -217,7 +218,8 @@ const extraMenuItems = computed(() => [
|
||||
icon: 'icon-[lucide--settings]',
|
||||
command: () => {
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_settings_menu_opened'
|
||||
button_id: 'sidebar_settings_menu_opened',
|
||||
element_group: 'sidebar'
|
||||
})
|
||||
showSettings()
|
||||
}
|
||||
@@ -329,7 +331,8 @@ const handleNodes2ToggleClick = () => {
|
||||
const onNodes2ToggleChange = async (value: boolean) => {
|
||||
await settingStore.set('Comfy.VueNodes.Enabled', value)
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: `menu_nodes_2.0_toggle_${value ? 'enabled' : 'disabled'}`
|
||||
button_id: `menu_nodes_2.0_toggle_${value ? 'enabled' : 'disabled'}`,
|
||||
element_group: 'sidebar'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -138,19 +138,23 @@ const onTabClick = async (item: SidebarTabExtension) => {
|
||||
|
||||
if (isNodeLibraryTab)
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_tab_node_library_selected'
|
||||
button_id: 'sidebar_tab_node_library_selected',
|
||||
element_group: 'sidebar'
|
||||
})
|
||||
else if (isModelLibraryTab)
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_tab_model_library_selected'
|
||||
button_id: 'sidebar_tab_model_library_selected',
|
||||
element_group: 'sidebar'
|
||||
})
|
||||
else if (isWorkflowsTab)
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_tab_workflows_selected'
|
||||
button_id: 'sidebar_tab_workflows_selected',
|
||||
element_group: 'sidebar'
|
||||
})
|
||||
else if (isAssetsTab)
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_tab_assets_media_selected'
|
||||
button_id: 'sidebar_tab_assets_media_selected',
|
||||
element_group: 'sidebar'
|
||||
})
|
||||
|
||||
await commandStore.commands
|
||||
|
||||
@@ -21,7 +21,8 @@ const bottomPanelStore = useBottomPanelStore()
|
||||
*/
|
||||
const toggleConsole = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_bottom_panel_console_toggled'
|
||||
button_id: 'sidebar_bottom_panel_console_toggled',
|
||||
element_group: 'sidebar'
|
||||
})
|
||||
bottomPanelStore.toggleBottomPanel()
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ const tooltipText = computed(
|
||||
const showSettingsDialog = () => {
|
||||
command.function()
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_settings_button_clicked'
|
||||
button_id: 'sidebar_settings_button_clicked',
|
||||
element_group: 'sidebar'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -37,7 +37,8 @@ const tooltipText = computed(
|
||||
*/
|
||||
const toggleShortcutsPanel = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_shortcuts_panel_toggled'
|
||||
button_id: 'sidebar_shortcuts_panel_toggled',
|
||||
element_group: 'sidebar'
|
||||
})
|
||||
bottomPanelStore.togglePanel('shortcuts')
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ const isSmall = computed(
|
||||
*/
|
||||
const openTemplates = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_templates_dialog_opened'
|
||||
button_id: 'sidebar_templates_dialog_opened',
|
||||
element_group: 'sidebar'
|
||||
})
|
||||
useWorkflowTemplateSelectorDialog().show('sidebar')
|
||||
}
|
||||
|
||||
@@ -118,7 +118,8 @@ const toggleBookmark = async () => {
|
||||
|
||||
const onHelpClick = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'node_library_help_button'
|
||||
button_id: 'node_library_help_button',
|
||||
element_group: 'node_library'
|
||||
})
|
||||
props.openNodeHelp(nodeDef.value)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import type { ComputedRef, Ref } from 'vue'
|
||||
|
||||
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import type {
|
||||
BillingStatus,
|
||||
BillingSubscriptionStatus,
|
||||
CreateTopupResponse,
|
||||
Plan,
|
||||
PreviewSubscribeResponse,
|
||||
SubscribeResponse,
|
||||
@@ -16,7 +19,9 @@ export interface SubscriptionInfo {
|
||||
tier: SubscriptionTier | null
|
||||
duration: SubscriptionDuration | null
|
||||
planSlug: string | null
|
||||
/** ISO 8601 */
|
||||
renewalDate: string | null
|
||||
/** ISO 8601 */
|
||||
endDate: string | null
|
||||
isCancelled: boolean
|
||||
hasFunds: boolean
|
||||
@@ -44,6 +49,9 @@ export interface BillingActions {
|
||||
) => Promise<PreviewSubscribeResponse | null>
|
||||
manageSubscription: () => Promise<void>
|
||||
cancelSubscription: () => Promise<void>
|
||||
resubscribe: () => Promise<void>
|
||||
/** `amountCents` must be a whole-dollar multiple of 100. */
|
||||
topup: (amountCents: number) => Promise<CreateTopupResponse | void>
|
||||
fetchPlans: () => Promise<void>
|
||||
/**
|
||||
* Ensures billing is initialized and subscription is active.
|
||||
@@ -65,16 +73,12 @@ export interface BillingState {
|
||||
currentPlanSlug: ComputedRef<string | null>
|
||||
isLoading: Ref<boolean>
|
||||
error: Ref<string | null>
|
||||
/**
|
||||
* Convenience computed for checking if subscription is active.
|
||||
* Equivalent to `subscription.value?.isActive ?? false`
|
||||
*/
|
||||
isActiveSubscription: ComputedRef<boolean>
|
||||
/**
|
||||
* Whether the current billing context has a FREE tier subscription.
|
||||
* Workspace-aware: reflects the active workspace's tier, not the user's personal tier.
|
||||
*/
|
||||
isFreeTier: ComputedRef<boolean>
|
||||
billingStatus: ComputedRef<BillingStatus | null>
|
||||
subscriptionStatus: ComputedRef<BillingSubscriptionStatus | null>
|
||||
tier: ComputedRef<SubscriptionTier | null>
|
||||
renewalDate: ComputedRef<string | null>
|
||||
}
|
||||
|
||||
export interface BillingContext extends BillingState, BillingActions {
|
||||
|
||||
@@ -5,13 +5,17 @@ import type { Plan } from '@/platform/workspace/api/workspaceApi'
|
||||
|
||||
import { useBillingContext } from './useBillingContext'
|
||||
|
||||
const { mockTeamWorkspacesEnabled, mockIsPersonal, mockPlans } = vi.hoisted(
|
||||
() => ({
|
||||
mockTeamWorkspacesEnabled: { value: false },
|
||||
mockIsPersonal: { value: true },
|
||||
mockPlans: { value: [] as Plan[] }
|
||||
})
|
||||
)
|
||||
const {
|
||||
mockTeamWorkspacesEnabled,
|
||||
mockIsPersonal,
|
||||
mockPlans,
|
||||
mockPurchaseCredits
|
||||
} = vi.hoisted(() => ({
|
||||
mockTeamWorkspacesEnabled: { value: false },
|
||||
mockIsPersonal: { value: true },
|
||||
mockPlans: { value: [] as Plan[] },
|
||||
mockPurchaseCredits: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', async (importOriginal) => {
|
||||
const original = await importOriginal()
|
||||
@@ -50,8 +54,9 @@ vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
isActiveSubscription: { value: true },
|
||||
subscriptionTier: { value: 'PRO' },
|
||||
subscriptionDuration: { value: 'MONTHLY' },
|
||||
formattedRenewalDate: { value: 'Jan 1, 2025' },
|
||||
formattedEndDate: { value: '' },
|
||||
subscriptionStatus: {
|
||||
value: { renewal_date: '2025-01-01T00:00:00Z', end_date: null }
|
||||
},
|
||||
isCancelled: { value: false },
|
||||
fetchStatus: vi.fn().mockResolvedValue(undefined),
|
||||
manageSubscription: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -70,6 +75,12 @@ vi.mock(
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: () => ({
|
||||
purchaseCredits: mockPurchaseCredits
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () => ({
|
||||
balance: { amount_micros: 5000000 },
|
||||
@@ -129,7 +140,7 @@ describe('useBillingContext', () => {
|
||||
tier: 'PRO',
|
||||
duration: 'MONTHLY',
|
||||
planSlug: null,
|
||||
renewalDate: 'Jan 1, 2025',
|
||||
renewalDate: '2025-01-01T00:00:00Z',
|
||||
endDate: null,
|
||||
isCancelled: false,
|
||||
hasFunds: true
|
||||
@@ -173,6 +184,13 @@ describe('useBillingContext', () => {
|
||||
await expect(manageSubscription()).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('converts topup cents to whole dollars for the legacy credit endpoint', async () => {
|
||||
const { topup } = useBillingContext()
|
||||
await topup(500)
|
||||
|
||||
expect(mockPurchaseCredits).toHaveBeenCalledWith(5)
|
||||
})
|
||||
|
||||
it('provides isActiveSubscription convenience computed', () => {
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
expect(isActiveSubscription.value).toBe(true)
|
||||
|
||||
@@ -122,6 +122,15 @@ function useBillingContextInternal(): BillingContext {
|
||||
|
||||
const isFreeTier = computed(() => subscription.value?.tier === 'FREE')
|
||||
|
||||
const billingStatus = computed(() =>
|
||||
toValue(activeContext.value.billingStatus)
|
||||
)
|
||||
const subscriptionStatus = computed(() =>
|
||||
toValue(activeContext.value.subscriptionStatus)
|
||||
)
|
||||
const tier = computed(() => toValue(activeContext.value.tier))
|
||||
const renewalDate = computed(() => toValue(activeContext.value.renewalDate))
|
||||
|
||||
function getMaxSeats(tierKey: TierKey): number {
|
||||
if (type.value === 'legacy') return 1
|
||||
|
||||
@@ -218,6 +227,14 @@ function useBillingContextInternal(): BillingContext {
|
||||
return activeContext.value.cancelSubscription()
|
||||
}
|
||||
|
||||
async function resubscribe() {
|
||||
return activeContext.value.resubscribe()
|
||||
}
|
||||
|
||||
async function topup(amountCents: number) {
|
||||
return activeContext.value.topup(amountCents)
|
||||
}
|
||||
|
||||
async function fetchPlans() {
|
||||
return activeContext.value.fetchPlans()
|
||||
}
|
||||
@@ -241,6 +258,10 @@ function useBillingContextInternal(): BillingContext {
|
||||
error,
|
||||
isActiveSubscription,
|
||||
isFreeTier,
|
||||
billingStatus,
|
||||
subscriptionStatus,
|
||||
tier,
|
||||
renewalDate,
|
||||
getMaxSeats,
|
||||
|
||||
initialize,
|
||||
@@ -250,6 +271,8 @@ function useBillingContextInternal(): BillingContext {
|
||||
previewSubscribe,
|
||||
manageSubscription,
|
||||
cancelSubscription,
|
||||
resubscribe,
|
||||
topup,
|
||||
fetchPlans,
|
||||
requireActiveSubscription,
|
||||
showSubscriptionDialog
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import type {
|
||||
BillingStatus,
|
||||
BillingSubscriptionStatus,
|
||||
PreviewSubscribeResponse,
|
||||
SubscribeResponse
|
||||
} from '@/platform/workspace/api/workspaceApi'
|
||||
@@ -24,8 +27,7 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
isActiveSubscription: legacyIsActiveSubscription,
|
||||
subscriptionTier,
|
||||
subscriptionDuration,
|
||||
formattedRenewalDate,
|
||||
formattedEndDate,
|
||||
subscriptionStatus: legacySubscriptionStatus,
|
||||
isCancelled,
|
||||
fetchStatus: legacyFetchStatus,
|
||||
manageSubscription: legacyManageSubscription,
|
||||
@@ -34,6 +36,7 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
} = useSubscription()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const authActions = useAuthActions()
|
||||
|
||||
const isInitialized = ref(false)
|
||||
const isLoading = ref(false)
|
||||
@@ -52,8 +55,8 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
tier: subscriptionTier.value,
|
||||
duration: subscriptionDuration.value,
|
||||
planSlug: null, // Legacy doesn't use plan slugs
|
||||
renewalDate: formattedRenewalDate.value || null,
|
||||
endDate: formattedEndDate.value || null,
|
||||
renewalDate: legacySubscriptionStatus.value?.renewal_date ?? null,
|
||||
endDate: legacySubscriptionStatus.value?.end_date ?? null,
|
||||
isCancelled: isCancelled.value,
|
||||
hasFunds: (authStore.balance?.amount_micros ?? 0) > 0
|
||||
}
|
||||
@@ -75,6 +78,18 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
}
|
||||
})
|
||||
|
||||
// Legacy has no coarse billing_status concept (workspace-only).
|
||||
const billingStatus = computed<BillingStatus | null>(() => null)
|
||||
const subscriptionStatus = computed<BillingSubscriptionStatus | null>(() => {
|
||||
if (isCancelled.value) return 'canceled'
|
||||
if (legacyIsActiveSubscription.value) return 'active'
|
||||
return null
|
||||
})
|
||||
const tier = computed(() => subscriptionTier.value)
|
||||
const renewalDate = computed(
|
||||
() => legacySubscriptionStatus.value?.renewal_date ?? null
|
||||
)
|
||||
|
||||
// Legacy billing doesn't have workspace-style plans
|
||||
const plans = computed(() => [])
|
||||
const currentPlanSlug = computed(() => null)
|
||||
@@ -152,6 +167,16 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
await legacyManageSubscription()
|
||||
}
|
||||
|
||||
async function resubscribe(): Promise<void> {
|
||||
// Legacy has no resubscribe endpoint; resubscribing is a fresh checkout.
|
||||
await legacySubscribe()
|
||||
}
|
||||
|
||||
async function topup(amountCents: number): Promise<void> {
|
||||
// Facade standardizes on cents; legacy /customers/credit takes dollars.
|
||||
await authActions.purchaseCredits(amountCents / 100)
|
||||
}
|
||||
|
||||
async function fetchPlans(): Promise<void> {
|
||||
// Legacy billing doesn't have workspace-style plans
|
||||
// Plans are hardcoded in the UI for legacy subscriptions
|
||||
@@ -179,6 +204,10 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
error,
|
||||
isActiveSubscription,
|
||||
isFreeTier,
|
||||
billingStatus,
|
||||
subscriptionStatus,
|
||||
tier,
|
||||
renewalDate,
|
||||
|
||||
// Actions
|
||||
initialize,
|
||||
@@ -188,6 +217,8 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
previewSubscribe,
|
||||
manageSubscription,
|
||||
cancelSubscription,
|
||||
resubscribe,
|
||||
topup,
|
||||
fetchPlans,
|
||||
requireActiveSubscription,
|
||||
showSubscriptionDialog
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { uniq } from 'es-toolkit'
|
||||
|
||||
import type { LGraphNode, Positionable } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphEventMode, Reroute } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { collectFromNodes } from '@/utils/graphTraversalUtil'
|
||||
import { isLGraphNode } from '@/utils/litegraphUtil'
|
||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
/**
|
||||
* Composable for handling selected LiteGraph items filtering and operations.
|
||||
@@ -71,7 +73,13 @@ export function useSelectedLiteGraphItems() {
|
||||
* the prior null-tolerance for callers wired to early-firing commands.
|
||||
*/
|
||||
const getSelectedNodesShallow = (): LGraphNode[] =>
|
||||
Array.from(canvasStore.canvas?.selectedItems ?? []).filter(isLGraphNode)
|
||||
uniq(
|
||||
[...(canvasStore.canvas?.selectedItems ?? [])].flatMap((item) => {
|
||||
if (isLGraphNode(item)) return [item]
|
||||
if (isLGraphGroup(item)) return [...item.children].filter(isLGraphNode)
|
||||
return []
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Get only the selected nodes (LGraphNode instances) from the canvas.
|
||||
|
||||
@@ -7,7 +7,12 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { isImageNode, isLGraphNode, isLoad3dNode } from '@/utils/litegraphUtil'
|
||||
import {
|
||||
isImageNode,
|
||||
isLGraphGroup,
|
||||
isLGraphNode,
|
||||
isLoad3dNode
|
||||
} from '@/utils/litegraphUtil'
|
||||
import { filterOutputNodes } from '@/utils/nodeFilterUtil'
|
||||
|
||||
export interface NodeSelectionState {
|
||||
@@ -41,6 +46,11 @@ export function useSelectionState() {
|
||||
const hasAnySelection = computed(() => selectedItems.value.length > 0)
|
||||
const hasSingleSelection = computed(() => selectedItems.value.length === 1)
|
||||
const hasMultipleSelection = computed(() => selectedItems.value.length > 1)
|
||||
const hasGroupedNodesSelection = computed(() =>
|
||||
selectedItems.value.some(
|
||||
(item) => isLGraphGroup(item) && [...item.children].some(isLGraphNode)
|
||||
)
|
||||
)
|
||||
|
||||
const isSingleNode = computed(
|
||||
() => hasSingleSelection.value && isLGraphNode(selectedItems.value[0])
|
||||
@@ -112,6 +122,7 @@ export function useSelectionState() {
|
||||
openNodeInfo,
|
||||
hasAny3DNodeSelected,
|
||||
hasAnySelection,
|
||||
hasGroupedNodesSelection,
|
||||
hasSingleSelection,
|
||||
hasMultipleSelection,
|
||||
isSingleNode,
|
||||
|
||||
@@ -9,16 +9,26 @@ export type AppMode =
|
||||
| 'builder:outputs'
|
||||
| 'builder:arrange'
|
||||
|
||||
type WorkflowModeSource = {
|
||||
activeMode: AppMode | null
|
||||
initialMode: AppMode | null | undefined
|
||||
}
|
||||
|
||||
export function getWorkflowMode(
|
||||
workflow: WorkflowModeSource | null | undefined
|
||||
): AppMode {
|
||||
return workflow?.activeMode ?? workflow?.initialMode ?? 'graph'
|
||||
}
|
||||
|
||||
export function isAppModeValue(mode: AppMode): boolean {
|
||||
return mode === 'app' || mode === 'builder:arrange'
|
||||
}
|
||||
|
||||
const enableAppBuilder = ref(true)
|
||||
|
||||
export function useAppMode() {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const mode = computed(
|
||||
() =>
|
||||
workflowStore.activeWorkflow?.activeMode ??
|
||||
workflowStore.activeWorkflow?.initialMode ??
|
||||
'graph'
|
||||
)
|
||||
const mode = computed(() => getWorkflowMode(workflowStore.activeWorkflow))
|
||||
|
||||
const isBuilderMode = computed(
|
||||
() => isSelectMode.value || isArrangeMode.value
|
||||
@@ -29,9 +39,7 @@ export function useAppMode() {
|
||||
() => isSelectInputsMode.value || isSelectOutputsMode.value
|
||||
)
|
||||
const isArrangeMode = computed(() => mode.value === 'builder:arrange')
|
||||
const isAppMode = computed(
|
||||
() => mode.value === 'app' || mode.value === 'builder:arrange'
|
||||
)
|
||||
const isAppMode = computed(() => isAppModeValue(mode.value))
|
||||
const isGraphMode = computed(
|
||||
() => mode.value === 'graph' || isSelectMode.value
|
||||
)
|
||||
|
||||
@@ -38,7 +38,8 @@ export function useHelpCenter() {
|
||||
*/
|
||||
const toggleHelpCenter = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_help_center_toggled'
|
||||
button_id: 'sidebar_help_center_toggled',
|
||||
element_group: 'sidebar'
|
||||
})
|
||||
helpCenterStore.toggle()
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@ describe('useLoad3d', () => {
|
||||
exportModel: vi.fn().mockResolvedValue(undefined),
|
||||
isSplatModel: vi.fn().mockReturnValue(false),
|
||||
isPlyModel: vi.fn().mockReturnValue(false),
|
||||
getSourceFormat: vi.fn().mockReturnValue(null),
|
||||
getCurrentModelCapabilities: vi.fn().mockReturnValue({
|
||||
fitToViewer: true,
|
||||
requiresMaterialRebuild: false,
|
||||
|
||||
@@ -169,6 +169,7 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
const isPreview = ref(false)
|
||||
const isSplatModel = ref(false)
|
||||
const isPlyModel = ref(false)
|
||||
const sourceFormat = ref<string | null>(null)
|
||||
const canFitToViewer = ref(true)
|
||||
const canCenterCameraOnModel = ref(false)
|
||||
const canUseGizmo = ref(true)
|
||||
@@ -905,6 +906,7 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
loading.value = false
|
||||
isSplatModel.value = load3d?.isSplatModel() ?? false
|
||||
isPlyModel.value = load3d?.isPlyModel() ?? false
|
||||
sourceFormat.value = load3d?.getSourceFormat() ?? null
|
||||
canCenterCameraOnModel.value = isSplatModel.value || isPlyModel.value
|
||||
const caps = load3d?.getCurrentModelCapabilities()
|
||||
canFitToViewer.value = caps?.fitToViewer ?? true
|
||||
@@ -1070,6 +1072,7 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
isPreview,
|
||||
isSplatModel,
|
||||
isPlyModel,
|
||||
sourceFormat,
|
||||
canFitToViewer,
|
||||
canCenterCameraOnModel,
|
||||
canUseGizmo,
|
||||
|
||||
@@ -130,6 +130,7 @@ describe('useLoad3dViewer', () => {
|
||||
hasAnimations: vi.fn().mockReturnValue(false),
|
||||
isSplatModel: vi.fn().mockReturnValue(false),
|
||||
isPlyModel: vi.fn().mockReturnValue(false),
|
||||
getSourceFormat: vi.fn().mockReturnValue(null),
|
||||
getCurrentModelCapabilities: vi.fn().mockReturnValue({
|
||||
fitToViewer: true,
|
||||
requiresMaterialRebuild: false,
|
||||
@@ -618,7 +619,7 @@ describe('useLoad3dViewer', () => {
|
||||
requiresMaterialRebuild: false,
|
||||
gizmoTransform: true,
|
||||
lighting: false,
|
||||
exportable: false,
|
||||
exportable: true,
|
||||
materialModes: [],
|
||||
fitTargetSize: 20
|
||||
})
|
||||
@@ -630,7 +631,7 @@ describe('useLoad3dViewer', () => {
|
||||
expect.stringContaining('dropped.splat')
|
||||
)
|
||||
expect(viewer.canUseLighting.value).toBe(false)
|
||||
expect(viewer.canExport.value).toBe(false)
|
||||
expect(viewer.canExport.value).toBe(true)
|
||||
expect(viewer.isSplatModel.value).toBe(true)
|
||||
expect([...viewer.materialModes.value]).toEqual([])
|
||||
})
|
||||
|
||||
@@ -83,6 +83,7 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
const isStandaloneMode = ref(false)
|
||||
const isSplatModel = ref(false)
|
||||
const isPlyModel = ref(false)
|
||||
const sourceFormat = ref<string | null>(null)
|
||||
const canFitToViewer = ref(true)
|
||||
const canUseGizmo = ref(true)
|
||||
const canUseLighting = ref(true)
|
||||
@@ -96,6 +97,7 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
const captureAdapterFlags = (source: Load3d) => {
|
||||
isSplatModel.value = source.isSplatModel()
|
||||
isPlyModel.value = source.isPlyModel()
|
||||
sourceFormat.value = source.getSourceFormat()
|
||||
const caps = source.getCurrentModelCapabilities()
|
||||
canFitToViewer.value = caps.fitToViewer
|
||||
canUseGizmo.value = caps.gizmoTransform
|
||||
@@ -839,6 +841,7 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
isStandaloneMode,
|
||||
isSplatModel,
|
||||
isPlyModel,
|
||||
sourceFormat,
|
||||
canFitToViewer,
|
||||
canUseGizmo,
|
||||
canUseLighting,
|
||||
|
||||
@@ -12,13 +12,17 @@ const {
|
||||
exportGLBMock,
|
||||
exportOBJMock,
|
||||
exportSTLMock,
|
||||
exportFBXMock
|
||||
exportFBXMock,
|
||||
exportDirectMock,
|
||||
detectFormatFromURLMock
|
||||
} = vi.hoisted(() => ({
|
||||
cloneSkinnedMock: vi.fn(),
|
||||
exportGLBMock: vi.fn(),
|
||||
exportOBJMock: vi.fn(),
|
||||
exportSTLMock: vi.fn(),
|
||||
exportFBXMock: vi.fn()
|
||||
exportFBXMock: vi.fn(),
|
||||
exportDirectMock: vi.fn(),
|
||||
detectFormatFromURLMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('three/examples/jsm/utils/SkeletonUtils.js', () => ({
|
||||
@@ -30,7 +34,9 @@ vi.mock('@/extensions/core/load3d/ModelExporter', () => ({
|
||||
exportGLB: exportGLBMock,
|
||||
exportOBJ: exportOBJMock,
|
||||
exportSTL: exportSTLMock,
|
||||
exportFBX: exportFBXMock
|
||||
exportFBX: exportFBXMock,
|
||||
exportDirect: exportDirectMock,
|
||||
detectFormatFromURL: detectFormatFromURLMock
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -1172,5 +1178,56 @@ describe('Load3d', () => {
|
||||
'Unsupported export format: xyz'
|
||||
)
|
||||
})
|
||||
|
||||
it('downloads the source file directly for direct-export formats', async () => {
|
||||
exportDirectMock.mockReset()
|
||||
detectFormatFromURLMock.mockReturnValue('ply')
|
||||
const model = new THREE.Object3D()
|
||||
setupForExport({
|
||||
currentModel: model,
|
||||
originalFileName: 'cloud',
|
||||
originalURL: 'http://example.com/api/view?filename=cloud.ply'
|
||||
})
|
||||
|
||||
await ctx.load3d.exportModel('ply')
|
||||
|
||||
expect(exportDirectMock).toHaveBeenCalledWith(
|
||||
'http://example.com/api/view?filename=cloud.ply',
|
||||
'cloud.ply',
|
||||
'ply'
|
||||
)
|
||||
expect(exportGLBMock).not.toHaveBeenCalled()
|
||||
expect(exportOBJMock).not.toHaveBeenCalled()
|
||||
expect(cloneSkinnedMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses a direct export when the requested format differs from the source', async () => {
|
||||
exportDirectMock.mockReset()
|
||||
detectFormatFromURLMock.mockReturnValue('spz')
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
setupForExport({
|
||||
currentModel: new THREE.Object3D(),
|
||||
originalFileName: 'scene',
|
||||
originalURL: 'http://example.com/api/view?filename=scene.spz'
|
||||
})
|
||||
|
||||
await expect(ctx.load3d.exportModel('ply')).rejects.toThrow(
|
||||
'Cannot export ply without converting from the loaded spz source'
|
||||
)
|
||||
expect(exportDirectMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('getSourceFormat derives the extension from the original URL', () => {
|
||||
detectFormatFromURLMock.mockReturnValue('spz')
|
||||
setupForExport({
|
||||
currentModel: new THREE.Object3D(),
|
||||
originalURL: 'http://example.com/api/view?filename=scene.spz'
|
||||
})
|
||||
|
||||
expect(ctx.load3d.getSourceFormat()).toBe('spz')
|
||||
expect(detectFormatFromURLMock).toHaveBeenCalledWith(
|
||||
'http://example.com/api/view?filename=scene.spz'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { GizmoManager } from './GizmoManager'
|
||||
import type { HDRIManager } from './HDRIManager'
|
||||
import type { LightingManager } from './LightingManager'
|
||||
import type { LoaderManager } from './LoaderManager'
|
||||
import { DIRECT_EXPORT_FORMATS } from './constants'
|
||||
import { ModelExporter } from './ModelExporter'
|
||||
import { DEFAULT_MODEL_CAPABILITIES } from './ModelAdapter'
|
||||
import type { AdapterRef, ModelAdapterCapabilities } from './ModelAdapter'
|
||||
@@ -359,6 +360,27 @@ class Load3d {
|
||||
const exportMessage = `Exporting as ${format.toUpperCase()}...`
|
||||
this.eventManager.emitEvent('exportLoadingStart', exportMessage)
|
||||
|
||||
const originalFileName = this.modelManager.originalFileName || 'model'
|
||||
const filename = `${originalFileName}.${format}`
|
||||
const originalURL = this.modelManager.originalURL
|
||||
|
||||
if (DIRECT_EXPORT_FORMATS.has(format)) {
|
||||
try {
|
||||
if (this.getSourceFormat() !== format) {
|
||||
throw new Error(
|
||||
`Cannot export ${format} without converting from the loaded ${this.getSourceFormat() ?? 'unknown'} source`
|
||||
)
|
||||
}
|
||||
await ModelExporter.exportDirect(originalURL, filename, format)
|
||||
} catch (error) {
|
||||
console.error(`Error exporting model as ${format}:`, error)
|
||||
throw error
|
||||
} finally {
|
||||
this.eventManager.emitEvent('exportLoadingEnd', null)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const source = this.modelManager.currentModel
|
||||
const savedPos = source.position.clone()
|
||||
const savedRot = source.rotation.clone()
|
||||
@@ -384,11 +406,6 @@ class Load3d {
|
||||
? Object.assign(cloneSkinned(source), { animations: clips })
|
||||
: source.clone()
|
||||
|
||||
const originalFileName = this.modelManager.originalFileName || 'model'
|
||||
const filename = `${originalFileName}.${format}`
|
||||
|
||||
const originalURL = this.modelManager.originalURL
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
switch (format) {
|
||||
@@ -399,7 +416,7 @@ class Load3d {
|
||||
await ModelExporter.exportOBJ(model, filename, originalURL)
|
||||
break
|
||||
case 'stl':
|
||||
;(await ModelExporter.exportSTL(model, filename), originalURL)
|
||||
await ModelExporter.exportSTL(model, filename, originalURL)
|
||||
break
|
||||
case 'fbx':
|
||||
await ModelExporter.exportFBX(model, filename, originalURL)
|
||||
@@ -421,6 +438,12 @@ class Load3d {
|
||||
}
|
||||
}
|
||||
|
||||
getSourceFormat(): string | null {
|
||||
const url = this.modelManager.originalURL
|
||||
if (!url) return null
|
||||
return ModelExporter.detectFormatFromURL(url)
|
||||
}
|
||||
|
||||
setBackgroundColor(color: string): void {
|
||||
this.sceneManager.setBackgroundColor(color)
|
||||
|
||||
|
||||
@@ -338,6 +338,35 @@ describe('ModelExporter', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('exportDirect', () => {
|
||||
it('downloads the original source file unchanged', async () => {
|
||||
const blob = new Blob(['x'])
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi
|
||||
.fn()
|
||||
.mockResolvedValue({ ok: true, blob: () => Promise.resolve(blob) })
|
||||
)
|
||||
|
||||
await ModelExporter.exportDirect(
|
||||
'http://example.com/api/view?filename=src.ply',
|
||||
'out.ply',
|
||||
'ply'
|
||||
)
|
||||
|
||||
expect(downloadBlobMock).toHaveBeenCalledWith('out.ply', blob)
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('throws without toasting when there is no source URL, leaving the alert to the caller', async () => {
|
||||
await expect(
|
||||
ModelExporter.exportDirect(null, 'out.spz', 'spz')
|
||||
).rejects.toThrow('No source file available to export as spz')
|
||||
expect(downloadBlobMock).not.toHaveBeenCalled()
|
||||
expect(addAlertMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('exportFBX', () => {
|
||||
it('uses the direct-URL fast path for matching .fbx URLs', async () => {
|
||||
const blob = new Blob(['x'])
|
||||
|
||||
@@ -184,6 +184,18 @@ export class ModelExporter {
|
||||
}
|
||||
}
|
||||
|
||||
static async exportDirect(
|
||||
originalURL: string | null | undefined,
|
||||
filename: string,
|
||||
format: string
|
||||
): Promise<void> {
|
||||
if (!originalURL) {
|
||||
throw new Error(`No source file available to export as ${format}`)
|
||||
}
|
||||
|
||||
return ModelExporter.downloadFromURL(originalURL, filename)
|
||||
}
|
||||
|
||||
private static saveArrayBuffer(buffer: ArrayBuffer, filename: string): void {
|
||||
const blob = new Blob([buffer], { type: 'application/octet-stream' })
|
||||
downloadBlob(filename, blob)
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('SplatModelAdapter', () => {
|
||||
const adapter = new SplatModelAdapter()
|
||||
expect(adapter.kind).toBe('splat')
|
||||
expect(adapter.capabilities.lighting).toBe(false)
|
||||
expect(adapter.capabilities.exportable).toBe(false)
|
||||
expect(adapter.capabilities.exportable).toBe(true)
|
||||
expect([...adapter.capabilities.materialModes]).toEqual([])
|
||||
})
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export class SplatModelAdapter implements ModelAdapter {
|
||||
requiresMaterialRebuild: false,
|
||||
gizmoTransform: true,
|
||||
lighting: false,
|
||||
exportable: false,
|
||||
exportable: true,
|
||||
materialModes: [],
|
||||
fitTargetSize: 20
|
||||
}
|
||||
|
||||
38
src/extensions/core/load3d/constants.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getExportFormatOptions } from './constants'
|
||||
|
||||
describe('getExportFormatOptions', () => {
|
||||
it('returns the convertible mesh formats for mesh sources', () => {
|
||||
expect(getExportFormatOptions('glb').map((o) => o.value)).toEqual([
|
||||
'glb',
|
||||
'obj',
|
||||
'stl',
|
||||
'fbx'
|
||||
])
|
||||
})
|
||||
|
||||
it('returns the convertible mesh formats when the source is unknown', () => {
|
||||
expect(getExportFormatOptions(null).map((o) => o.value)).toEqual([
|
||||
'glb',
|
||||
'obj',
|
||||
'stl',
|
||||
'fbx'
|
||||
])
|
||||
})
|
||||
|
||||
it.each(['ply', 'spz', 'splat', 'ksplat'])(
|
||||
'offers only the source format for direct-export type %s',
|
||||
(format) => {
|
||||
expect(getExportFormatOptions(format)).toEqual([
|
||||
{ label: format.toUpperCase(), value: format }
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
it('matches direct-export formats case-insensitively', () => {
|
||||
expect(getExportFormatOptions('PLY')).toEqual([
|
||||
{ label: 'PLY', value: 'ply' }
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -24,3 +24,27 @@ export const SUPPORTED_HDRI_EXTENSIONS_ACCEPT = [
|
||||
].join(',')
|
||||
|
||||
export const LOAD3D_NONE_MODEL = 'none'
|
||||
|
||||
export const DIRECT_EXPORT_FORMATS = new Set(['ply', 'spz', 'splat', 'ksplat'])
|
||||
|
||||
export interface ExportFormatOption {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const CONVERTIBLE_EXPORT_FORMAT_OPTIONS: ExportFormatOption[] = [
|
||||
{ label: 'GLB', value: 'glb' },
|
||||
{ label: 'OBJ', value: 'obj' },
|
||||
{ label: 'STL', value: 'stl' },
|
||||
{ label: 'FBX', value: 'fbx' }
|
||||
]
|
||||
|
||||
export function getExportFormatOptions(
|
||||
sourceFormat: string | null | undefined
|
||||
): ExportFormatOption[] {
|
||||
const format = sourceFormat?.toLowerCase()
|
||||
if (format && DIRECT_EXPORT_FORMATS.has(format)) {
|
||||
return [{ label: format.toUpperCase(), value: format }]
|
||||
}
|
||||
return CONVERTIBLE_EXPORT_FORMAT_OPTIONS
|
||||
}
|
||||
|
||||
@@ -1401,7 +1401,6 @@ export class LGraphNode
|
||||
|
||||
default:
|
||||
return false
|
||||
break
|
||||
}
|
||||
this.mode = modeTo
|
||||
return true
|
||||
|
||||
@@ -822,6 +822,8 @@
|
||||
"CONDITIONING": "تكييف",
|
||||
"CONTROL_NET": "ControlNet",
|
||||
"CURVE": "منحنى",
|
||||
"DA3_GEOMETRY": "DA3_GEOMETRY",
|
||||
"DA3_MODEL": "DA3_MODEL",
|
||||
"ELEVENLABS_VOICE": "ELEVENLABS_VOICE",
|
||||
"FACE_DETECTION_MODEL": "نموذج كشف الوجه",
|
||||
"FACE_LANDMARKS": "معالم الوجه",
|
||||
@@ -883,6 +885,8 @@
|
||||
"RECRAFT_V3_STYLE": "نمط Recraft V3",
|
||||
"RETARGET_TASK_ID": "معرّف مهمة إعادة الاستهداف",
|
||||
"RIG_TASK_ID": "معرّف مهمة الهيكل",
|
||||
"RUNWAY_ALEPH2_KEYFRAME": "RUNWAY_ALEPH2_KEYFRAME",
|
||||
"RUNWAY_ALEPH2_PROMPT_IMAGE": "RUNWAY_ALEPH2_PROMPT_IMAGE",
|
||||
"SAM3_TRACK_DATA": "SAM3_TRACK_DATA",
|
||||
"SAMPLER": "جهاز تجميع",
|
||||
"SIGMAS": "سيجمات",
|
||||
@@ -3091,11 +3095,9 @@
|
||||
"collapse": "طي",
|
||||
"expand": "توسيع",
|
||||
"installAll": "تثبيت الكل",
|
||||
"installNodePack": "تثبيت حزمة العقد",
|
||||
"installed": "تم التثبيت",
|
||||
"installing": "جارٍ التثبيت...",
|
||||
"ossManagerDisabledHint": "لتثبيت العقد المفقودة، قم أولاً بتشغيل {pipCmd} في بيئة بايثون الخاصة بك لتثبيت مدير العقد، ثم أعد تشغيل ComfyUI مع العلم {flag}.",
|
||||
"searchInManager": "البحث في مدير العقد",
|
||||
"title": "حزم العقد المفقودة",
|
||||
"unknownPack": "حزمة غير معروفة",
|
||||
"unsupportedTitle": "حزم العقد غير المدعومة",
|
||||
@@ -3673,6 +3675,7 @@
|
||||
"comfyCloudLogo": "شعار Comfy Cloud",
|
||||
"contactOwnerToSubscribe": "يرجى التواصل مع مالك مساحة العمل للاشتراك",
|
||||
"contactUs": "تواصل معنا",
|
||||
"creditSliderSave": "وفر {percent}% ({amount})",
|
||||
"creditsRemainingThisMonth": "الرصيد المتبقي لهذا الشهر",
|
||||
"creditsRemainingThisYear": "الرصيد المتبقي لهذا العام",
|
||||
"creditsYouveAdded": "الرصيد الذي أضفته",
|
||||
|
||||
@@ -700,6 +700,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"BriaVideoReplaceBackground": {
|
||||
"description": "استبدل خلفية الفيديو بصورة أو فيديو محدد باستخدام Bria. يحتفظ الناتج بدقة ومعدل إطارات المقدمة؛ إذا كانت الخلفية بنسبة عرض إلى ارتفاع مختلفة، سيتم تمديدها لتناسب، لذا يُفضل مطابقة النسبة للحصول على نتائج غير مشوهة.",
|
||||
"display_name": "استبدال خلفية الفيديو بواسطة Bria",
|
||||
"inputs": {
|
||||
"background_image": {
|
||||
"name": "صورة الخلفية",
|
||||
"tooltip": "صورة الخلفية التي سيتم تركيبها خلف المقدمة. يرجى تقديم صورة خلفية أو فيديو خلفية، وليس كلاهما."
|
||||
},
|
||||
"background_video": {
|
||||
"name": "فيديو الخلفية",
|
||||
"tooltip": "فيديو الخلفية الذي سيتم تركيبه خلف المقدمة. يرجى تقديم صورة خلفية أو فيديو خلفية، وليس كلاهما."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة",
|
||||
"tooltip": "تتحكم البذرة فيما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
|
||||
},
|
||||
"video": {
|
||||
"name": "فيديو",
|
||||
"tooltip": "فيديو المقدمة الذي سيتم استبدال خلفيته."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ByteDance2FirstLastFrameNode": {
|
||||
"description": "إنشاء فيديو باستخدام Seedance 2.0 من صورة الإطار الأول وصورة الإطار الأخير (اختياري).",
|
||||
"display_name": "ByteDance Seedance 2.0 من الإطار الأول/الأخير إلى فيديو",
|
||||
@@ -2874,6 +2904,10 @@
|
||||
"name": "الصوت",
|
||||
"tooltip": "الصوت الذي سيتم إضافته للفيديو."
|
||||
},
|
||||
"bit_depth": {
|
||||
"name": "bit_depth",
|
||||
"tooltip": "عمق البت للفيديو المُنشأ. عمق ١٠ بت يحافظ على تدرجات أكثر سلاسة مع تقليل التدرج اللوني، لكن بعض المشغلات والعُقد اللاحقة قد لا تدعمه."
|
||||
},
|
||||
"fps": {
|
||||
"name": "الإطارات في الثانية"
|
||||
},
|
||||
@@ -2982,6 +3016,98 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DA3GeometryToMesh": {
|
||||
"description": "تحويل خريطة العمق إلى شبكة ثلاثية الأبعاد مثلثة.",
|
||||
"display_name": "تحويل هندسة DA3 إلى شبكة",
|
||||
"inputs": {
|
||||
"batch_index": {
|
||||
"name": "batch_index",
|
||||
"tooltip": "أي صورة من الدفعة سيتم تحويلها. عدد الرؤوس يختلف لكل صورة، لذلك لا يمكن تكديس الدُفعات."
|
||||
},
|
||||
"confidence_threshold": {
|
||||
"name": "confidence_threshold",
|
||||
"tooltip": "استبعاد البكسلات التي يكون مستوى الثقة المُطَبَّع لكل صورة أقل من هذه القيمة (٠ = الاحتفاظ بالجميع، ١ = الاحتفاظ بالبكسل الأكثر ثقة فقط). يُستخدم عندما تحتوي الهندسة على خريطة ثقة (نماذج Small/Base)."
|
||||
},
|
||||
"da3_geometry": {
|
||||
"name": "da3_geometry"
|
||||
},
|
||||
"decimation": {
|
||||
"name": "decimation",
|
||||
"tooltip": "تخطي الرؤوس. ١ = الدقة الكاملة، ٢ = النصف، وهكذا."
|
||||
},
|
||||
"discontinuity_threshold": {
|
||||
"name": "discontinuity_threshold",
|
||||
"tooltip": "إسقاط المثلثات التي يتجاوز نطاق العمق ٣×٣ الخاص بها هذا الكسر. ٠ = إيقاف."
|
||||
},
|
||||
"texture": {
|
||||
"name": "texture",
|
||||
"tooltip": "استخدام الصورة الأصلية كخامة لون أساسية."
|
||||
},
|
||||
"use_sky_mask": {
|
||||
"name": "use_sky_mask",
|
||||
"tooltip": "استبعاد بكسلات السماء (السماء ≥ ٠٫٥) من الشبكة. يُستخدم عندما تحتوي الهندسة على خريطة سماء (نماذج Mono/Metric)."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"DA3Inference": {
|
||||
"description": "تشغيل Depth Anything 3 على صورة. في وضع الرؤية المتعددة، تُعتبر كل صورة منظورًا منفصلًا لنفس المشهد.",
|
||||
"display_name": "تشغيل Depth Anything 3",
|
||||
"inputs": {
|
||||
"da3_model": {
|
||||
"name": "da3_model"
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
},
|
||||
"mode": {
|
||||
"name": "mode",
|
||||
"tooltip": "mono: صورة بمنظور واحد (يعمل مع أي نوع من النماذج).\nmultiview: تتم معالجة جميع الصور معًا لتحقيق التناسق الهندسي + وضعية الكاميرا (لنماذج Small/Base فقط)."
|
||||
},
|
||||
"resize_method": {
|
||||
"name": "resize_method",
|
||||
"tooltip": "upper_bound_resize: التحجيم بحيث يكون أطول ضلع = الدقة (يحد من الذاكرة، الافتراضي).\nlower_bound_resize: التحجيم بحيث يكون أقصر ضلع = الدقة (يحافظ على مزيد من التفاصيل في الصور الطويلة/العريضة، يستهلك ذاكرة أكثر)."
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution",
|
||||
"tooltip": "الدقة التي يعمل بها النموذج (أطول ضلع، مضاعف للعدد ١٤).\nأقل = أسرع / ذاكرة أقل.\nأعلى = تفاصيل أكثر.\nيتم تكبير الناتج إلى الحجم الأصلي."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "da3_geometry",
|
||||
"tooltip": "قاموس من التنسورات غير المُطبَّعة.\nدائمًا يحتوي على المفاتيح: depth، image، mode.\nمفاتيح اختيارية: sky (لـ Mono/Metric)، confidence (لـ Small/Base)، extrinsics + intrinsics (للوضع متعدد الرؤية)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"DA3Render": {
|
||||
"description": "عرض خريطة العمق أو خريطة الثقة أو قناع السماء من بيانات هندسة Depth Anything 3.",
|
||||
"display_name": "عرض Depth Anything 3",
|
||||
"inputs": {
|
||||
"da3_geometry": {
|
||||
"name": "da3_geometry"
|
||||
},
|
||||
"output": {
|
||||
"name": "output",
|
||||
"tooltip": "- depth: صورة عمق رمادية مُطبَّعة.\n- depth_colored: العمق معروض عبر خريطة ألوان Turbo.\n- sky_mask: احتمالية السماء في النطاق [٠، ١] (لنماذج Mono/Metric فقط).\n- confidence: ثقة العمق المُطبَّعة (لنماذج Small/Base فقط)."
|
||||
},
|
||||
"output_apply_sky_clip": {
|
||||
"name": "apply_sky_clip"
|
||||
},
|
||||
"output_normalization": {
|
||||
"name": "normalization"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"DiffControlNetLoader": {
|
||||
"display_name": "تحميل نموذج ControlNet (فرق)",
|
||||
"inputs": {
|
||||
@@ -5098,6 +5224,10 @@
|
||||
"2": {
|
||||
"name": "معدل_الإطارات",
|
||||
"tooltip": null
|
||||
},
|
||||
"3": {
|
||||
"name": "bit_depth",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8987,6 +9117,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoadDA3Model": {
|
||||
"display_name": "تحميل Depth Anything 3",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
"weight_dtype": {
|
||||
"name": "weight_dtype"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoadImage": {
|
||||
"display_name": "تحميل صورة",
|
||||
"inputs": {
|
||||
@@ -15717,6 +15863,98 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"RunwayAleph2KeyframeNode": {
|
||||
"description": "ثبت صورة إرشادية في لحظة معينة من فيديو الإدخال (المصدر)، بحيث يوجه Aleph2 التعديل في تلك النقطة من الفيديو. قم بتوصيل هذه العقدة بمدخل 'keyframes' في عقدة Runway Aleph2 Video to Video؛ يمكنك ربط عدة عقد معًا (حتى ٥) عبر مدخل 'keyframes' الاختياري أدناه.",
|
||||
"display_name": "Runway Aleph2 Keyframe",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "الصورة",
|
||||
"tooltip": "الصورة الإرشادية التي سيتم تطبيقها في اللحظة المختارة من فيديو الإدخال."
|
||||
},
|
||||
"keyframes": {
|
||||
"name": "keyframes",
|
||||
"tooltip": "إطارات رئيسية سابقة اختيارية لربطها مع هذه."
|
||||
},
|
||||
"timing": {
|
||||
"name": "التوقيت",
|
||||
"tooltip": "كيفية وضع هذه الصورة على الجدول الزمني لفيديو الإدخال."
|
||||
},
|
||||
"timing_seconds": {
|
||||
"name": "ثواني"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "keyframes",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"RunwayAleph2PromptImageNode": {
|
||||
"description": "ثبت صورة إرشادية في لحظة معينة من فيديو الإخراج (النتيجة)، لتوجيه شكل الفيديو المعدل في تلك النقطة. قم بتوصيل هذه العقدة بمدخل 'prompt_images' في عقدة Runway Aleph2 Video to Video؛ يمكنك ربط عدة صور معًا (حتى ٥) عبر مدخل 'prompt_images' الاختياري أدناه.",
|
||||
"display_name": "Runway Aleph2 Prompt Image",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "الصورة",
|
||||
"tooltip": "الصورة الإرشادية التي سيتم وضعها في اللحظة المختارة من فيديو الإخراج."
|
||||
},
|
||||
"position": {
|
||||
"name": "الموضع",
|
||||
"tooltip": "كيفية وضع هذه الصورة على الجدول الزمني لفيديو الإخراج."
|
||||
},
|
||||
"position_seconds": {
|
||||
"name": "ثواني"
|
||||
},
|
||||
"prompt_images": {
|
||||
"name": "prompt_images",
|
||||
"tooltip": "صور إرشادية سابقة اختيارية لربطها مع هذه."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "prompt_images",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"RunwayAleph2VideoToVideoNode": {
|
||||
"description": "حرر فيديو باستخدام نص توجيهي عبر نموذج Aleph2 من Runway. يقوم Aleph2 بتحويل لقطاتك (تغيير الأسلوب، إعادة الإضاءة، إضافة أو إزالة عناصر، تغيير زاوية الرؤية) مع الحفاظ على الحركة والتوقيت الأصليين؛ دقة الإخراج تطابق الفيديو الأصلي، ويجب أن يكون الفيديو المدخل بين ٢ و٣٠ ثانية وبمعدل ٣٠ إطارًا في الثانية أو أقل. يمكنك توجيه التحرير باستخدام إما إطارات رئيسية (مرتبطة بالفيديو الأصلي) أو صور توجيهية (مرتبطة بالفيديو الناتج) - استخدم أحد الخيارين فقط، وليس كليهما.",
|
||||
"display_name": "Runway Aleph2 تحويل الفيديو إلى فيديو",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"keyframes": {
|
||||
"name": "الإطارات الرئيسية",
|
||||
"tooltip": "صور إرشادية مرتبطة بفيديو الإدخال، من عقد Aleph2 Keyframe (حتى ٥). استخدم الإطارات الرئيسية أو الصور التوجيهية، وليس كليهما."
|
||||
},
|
||||
"prompt": {
|
||||
"name": "النص التوجيهي",
|
||||
"tooltip": "يصف ما يجب أن يظهر في النتيجة (١-١٠٠٠ حرف)."
|
||||
},
|
||||
"prompt_images": {
|
||||
"name": "الصور التوجيهية",
|
||||
"tooltip": "صور إرشادية مرتبطة بفيديو الإخراج، من عقد Aleph2 Prompt Image (حتى ٥). استخدم الإطارات الرئيسية أو الصور التوجيهية، وليس كليهما."
|
||||
},
|
||||
"public_figure_threshold": {
|
||||
"name": "عتبة الشخصيات العامة",
|
||||
"tooltip": "مراقبة المحتوى للأشخاص المعروفين من الشخصيات العامة."
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة",
|
||||
"tooltip": "بذرة عشوائية للتوليد"
|
||||
},
|
||||
"video": {
|
||||
"name": "الفيديو",
|
||||
"tooltip": "فيديو الإدخال للتحرير. يجب أن يكون بين ٢ و٣٠ ثانية وبمعدل ٣٠ إطارًا في الثانية أو أقل."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"RunwayFirstLastFrameNode": {
|
||||
"description": "قم برفع الإطارات الرئيسية الأولى والأخيرة، واكتب موجهًا، وقم بتوليد فيديو. قد تستفيد التحولات الأكثر تعقيدًا، مثل الحالات التي يختلف فيها الإطار الأخير تمامًا عن الإطار الأول، من المدة الأطول البالغة 10 ثوانٍ. سيمنح هذا التوليد مزيدًا من الوقت للانتقال بسلاسة بين المدخلين. قبل البدء، راجع أفضل الممارسات هذه لضمان أن اختياراتك للمدخلات ستؤدي إلى نجاح التوليد: https://help.runwayml.com/hc/en-us/articles/34170748696595-Creating-with-Keyframes-on-Gen-3.",
|
||||
"display_name": "Runway تحويل الإطار الأول-الأخير إلى فيديو",
|
||||
@@ -19356,6 +19594,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TripoImportModelNode": {
|
||||
"description": "استيراد نموذج ثلاثي الأبعاد خارجي (مثلاً من Rodin، Hunyuan3D أو ملف محلي) إلى Tripo لاستخدامه مع عُقد المعالجة اللاحقة في Tripo: الإكساء، التحريك، التحويل. يُوصى باستخدام GLB: تبقى الخامات محفوظة فقط إذا كانت مضمنة داخل الملف. يرجى ملاحظة أن إكساء نموذج مستورد يتطلب مطالبة إكساء.",
|
||||
"display_name": "Tripo: استيراد نموذج",
|
||||
"inputs": {
|
||||
"model_3d": {
|
||||
"name": "model_3d",
|
||||
"tooltip": "نموذج ثلاثي الأبعاد للاستيراد (GLB / FBX / OBJ / STL، حتى ١٥٠ ميجابايت). ملفات OBJ و STL لا تحتوي على خامات مضمنة."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "model task_id",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"TripoMultiviewToModelNode": {
|
||||
"display_name": "Tripo: متعدد المناظر إلى نموذج",
|
||||
"inputs": {
|
||||
@@ -19824,6 +20078,10 @@
|
||||
"texture_alignment": {
|
||||
"name": "محاذاة_الملمس"
|
||||
},
|
||||
"texture_prompt": {
|
||||
"name": "texture_prompt",
|
||||
"tooltip": "إرشادات نصية اختيارية للإكساء. مطلوبة عملياً للنماذج المستوردة (Tripo: استيراد نموذج)، والتي لا تحتوي على صورة مصدر لاستنتاج الألوان منها."
|
||||
},
|
||||
"texture_quality": {
|
||||
"name": "جودة_الملمس"
|
||||
},
|
||||
|
||||
@@ -1707,6 +1707,7 @@
|
||||
"3d": "3d",
|
||||
"scheduling": "scheduling",
|
||||
"create": "create",
|
||||
"geometry estimation": "geometry estimation",
|
||||
"deprecated": "deprecated",
|
||||
"detection": "detection",
|
||||
"debug": "debug",
|
||||
@@ -1743,7 +1744,6 @@
|
||||
"Meshy": "Meshy",
|
||||
"MiniMax": "MiniMax",
|
||||
"model_specific": "model_specific",
|
||||
"geometry estimation": "geometry estimation",
|
||||
"multigpu": "multigpu",
|
||||
"OpenAI": "OpenAI",
|
||||
"Sora": "Sora",
|
||||
@@ -1797,6 +1797,8 @@
|
||||
"CONDITIONING": "CONDITIONING",
|
||||
"CONTROL_NET": "CONTROL_NET",
|
||||
"CURVE": "CURVE",
|
||||
"DA3_GEOMETRY": "DA3_GEOMETRY",
|
||||
"DA3_MODEL": "DA3_MODEL",
|
||||
"ELEVENLABS_VOICE": "ELEVENLABS_VOICE",
|
||||
"FACE_DETECTION_MODEL": "FACE_DETECTION_MODEL",
|
||||
"FACE_LANDMARKS": "FACE_LANDMARKS",
|
||||
@@ -1858,6 +1860,8 @@
|
||||
"RECRAFT_V3_STYLE": "RECRAFT_V3_STYLE",
|
||||
"RETARGET_TASK_ID": "RETARGET_TASK_ID",
|
||||
"RIG_TASK_ID": "RIG_TASK_ID",
|
||||
"RUNWAY_ALEPH2_KEYFRAME": "RUNWAY_ALEPH2_KEYFRAME",
|
||||
"RUNWAY_ALEPH2_PROMPT_IMAGE": "RUNWAY_ALEPH2_PROMPT_IMAGE",
|
||||
"SAM3_TRACK_DATA": "SAM3_TRACK_DATA",
|
||||
"SAMPLER": "SAMPLER",
|
||||
"SIGMAS": "SIGMAS",
|
||||
|
||||
@@ -700,6 +700,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"BriaVideoReplaceBackground": {
|
||||
"display_name": "Bria Video Replace Background",
|
||||
"description": "Replace a video's background with a supplied image or video using Bria. The output keeps the foreground's resolution and frame rate; a background with a different aspect ratio is stretched to fit, so match it for undistorted results.",
|
||||
"inputs": {
|
||||
"video": {
|
||||
"name": "video",
|
||||
"tooltip": "Foreground video whose background is replaced."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
|
||||
},
|
||||
"background_image": {
|
||||
"name": "background_image",
|
||||
"tooltip": "Background image to composite behind the foreground. Provide either a background image or a background video, not both."
|
||||
},
|
||||
"background_video": {
|
||||
"name": "background_video",
|
||||
"tooltip": "Background video to composite behind the foreground. Provide either a background image or a background video, not both."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ByteDance2FirstLastFrameNode": {
|
||||
"display_name": "ByteDance Seedance 2.0 First-Last-Frame to Video",
|
||||
"description": "Generate video using Seedance 2.0 from a first frame image and optional last frame image.",
|
||||
@@ -2880,6 +2910,10 @@
|
||||
"audio": {
|
||||
"name": "audio",
|
||||
"tooltip": "The audio to add to the video."
|
||||
},
|
||||
"bit_depth": {
|
||||
"name": "bit_depth",
|
||||
"tooltip": "Bit depth of the created video. 10-bit keeps smoother gradients with less banding, but some players and downstream nodes may not support it."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -2982,6 +3016,98 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DA3GeometryToMesh": {
|
||||
"display_name": "Convert DA3 Geometry to Mesh",
|
||||
"description": "Convert a depth map into a triangulated 3D mesh.",
|
||||
"inputs": {
|
||||
"da3_geometry": {
|
||||
"name": "da3_geometry"
|
||||
},
|
||||
"batch_index": {
|
||||
"name": "batch_index",
|
||||
"tooltip": "Which image of a batch to convert. Per-image vertex counts differ so batches cannot be stacked."
|
||||
},
|
||||
"decimation": {
|
||||
"name": "decimation",
|
||||
"tooltip": "Vertex stride. 1 = full resolution, 2 = half, etc."
|
||||
},
|
||||
"discontinuity_threshold": {
|
||||
"name": "discontinuity_threshold",
|
||||
"tooltip": "Drop triangles whose 3x3 depth span exceeds this fraction. 0 = off."
|
||||
},
|
||||
"confidence_threshold": {
|
||||
"name": "confidence_threshold",
|
||||
"tooltip": "Exclude pixels whose per-image normalised confidence is below this value (0 = keep all, 1 = keep only the single most confident pixel). Used when the geometry has a confidence map (Small/Base models)."
|
||||
},
|
||||
"use_sky_mask": {
|
||||
"name": "use_sky_mask",
|
||||
"tooltip": "Exclude sky-probability pixels (sky >= 0.5) from the mesh. Used when the geometry has a sky map (Mono/Metric models)."
|
||||
},
|
||||
"texture": {
|
||||
"name": "texture",
|
||||
"tooltip": "Use the source image as a base color texture."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"DA3Inference": {
|
||||
"display_name": "Run Depth Anything 3",
|
||||
"description": "Run Depth Anything 3 on an image. In multi-view mode each image is treated as a separate view of the same scene.",
|
||||
"inputs": {
|
||||
"da3_model": {
|
||||
"name": "da3_model"
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution",
|
||||
"tooltip": "Resolution the model runs at (longest side, multiple of 14).\nLower = faster / less VRAM.\nHigher = more detail.\nOutput is upsampled back to the original size."
|
||||
},
|
||||
"resize_method": {
|
||||
"name": "resize_method",
|
||||
"tooltip": "upper_bound_resize: scale so the longest side = resolution (caps memory, default).\nlower_bound_resize: scale so the shortest side = resolution (preserves more detail on tall/wide images, uses more memory)."
|
||||
},
|
||||
"mode": {
|
||||
"name": "mode",
|
||||
"tooltip": "mono: single view image (works with any model variant).\nmultiview: all images processed together for geometric consistency + camera pose (for Small/Base models only)."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "da3_geometry",
|
||||
"tooltip": "Dictionary of non-normalized tensors.\nAlways has the keys: depth, image, mode.\nOptional keys: sky (for Mono/Metric), confidence (for Small/Base), extrinsics + intrinsics (for multi-view)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"DA3Render": {
|
||||
"display_name": "Render Depth Anything 3",
|
||||
"description": "Render a depth map, confidence map, or sky mask from Depth Anything 3 geometry data.",
|
||||
"inputs": {
|
||||
"da3_geometry": {
|
||||
"name": "da3_geometry"
|
||||
},
|
||||
"output": {
|
||||
"name": "output",
|
||||
"tooltip": "- depth: normalised greyscale depth image.\n- depth_colored: depth mapped through the Turbo colormap.\n- sky_mask: sky probability in [0, 1] (for Mono/Metric models only).\n- confidence: normalised depth confidence (for Small/Base models only)."
|
||||
},
|
||||
"output_apply_sky_clip": {
|
||||
"name": "apply_sky_clip"
|
||||
},
|
||||
"output_normalization": {
|
||||
"name": "normalization"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"DiffControlNetLoader": {
|
||||
"display_name": "Load ControlNet Model (diff)",
|
||||
"inputs": {
|
||||
@@ -4971,7 +5097,7 @@
|
||||
},
|
||||
"GetVideoComponents": {
|
||||
"display_name": "Get Video Components",
|
||||
"description": "Extracts all components from a video: frames, audio, and framerate.",
|
||||
"description": "Extracts all components from a video: frames, audio, framerate, and bit depth.",
|
||||
"inputs": {
|
||||
"video": {
|
||||
"name": "video",
|
||||
@@ -4990,6 +5116,10 @@
|
||||
"2": {
|
||||
"name": "fps",
|
||||
"tooltip": null
|
||||
},
|
||||
"3": {
|
||||
"name": "bit_depth",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8561,6 +8691,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoadDA3Model": {
|
||||
"display_name": "Load Depth Anything 3",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
"weight_dtype": {
|
||||
"name": "weight_dtype"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoadImage": {
|
||||
"display_name": "Load Image",
|
||||
"inputs": {
|
||||
@@ -15717,6 +15863,98 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"RunwayAleph2KeyframeNode": {
|
||||
"display_name": "Runway Aleph2 Keyframe",
|
||||
"description": "Anchor a guidance image to a moment of the input (source) video, so Aleph2 steers the edit at that point of your footage. Connect this to the 'keyframes' input of the Runway Aleph2 Video to Video node; chain several together (up to 5) via the optional 'keyframes' input below.",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "image",
|
||||
"tooltip": "The guidance image to apply at the chosen moment of the input video."
|
||||
},
|
||||
"timing": {
|
||||
"name": "timing",
|
||||
"tooltip": "How to place this image on the input video's timeline."
|
||||
},
|
||||
"keyframes": {
|
||||
"name": "keyframes",
|
||||
"tooltip": "Optional earlier keyframes to chain with this one."
|
||||
},
|
||||
"timing_seconds": {
|
||||
"name": "seconds"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "keyframes",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"RunwayAleph2PromptImageNode": {
|
||||
"display_name": "Runway Aleph2 Prompt Image",
|
||||
"description": "Anchor a guidance image to a moment of the output (result) video, to guide what the edited video looks like at that point. Connect this to the 'prompt_images' input of the Runway Aleph2 Video to Video node; chain several together (up to 5) via the optional 'prompt_images' input below.",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "image",
|
||||
"tooltip": "The guidance image to place at the chosen moment of the output video."
|
||||
},
|
||||
"position": {
|
||||
"name": "position",
|
||||
"tooltip": "How to place this image on the output video's timeline."
|
||||
},
|
||||
"prompt_images": {
|
||||
"name": "prompt_images",
|
||||
"tooltip": "Optional earlier prompt images to chain with this one."
|
||||
},
|
||||
"position_seconds": {
|
||||
"name": "seconds"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "prompt_images",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"RunwayAleph2VideoToVideoNode": {
|
||||
"display_name": "Runway Aleph2 Video to Video",
|
||||
"description": "Edit a video with a text prompt using Runway's Aleph2 model. Aleph2 transforms your footage (restyle, relight, add or remove elements, change the viewpoint) while keeping the original motion and timing; the output resolution matches the input video, which must be 2-30 seconds at 30 fps or lower. Optionally steer the edit with either keyframes (anchored to the input video) or prompt images (anchored to the output video) - use one or the other, not both.",
|
||||
"inputs": {
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Describes what should appear in the output (1-1000 characters)."
|
||||
},
|
||||
"video": {
|
||||
"name": "video",
|
||||
"tooltip": "Input video to edit. Must be 2-30 seconds at 30 fps or lower."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Random seed for generation"
|
||||
},
|
||||
"public_figure_threshold": {
|
||||
"name": "public_figure_threshold",
|
||||
"tooltip": "Content moderation for recognizable public figures."
|
||||
},
|
||||
"keyframes": {
|
||||
"name": "keyframes",
|
||||
"tooltip": "Guidance images anchored to the input video, from Aleph2 Keyframe nodes (up to 5). Use keyframes or prompt images, not both."
|
||||
},
|
||||
"prompt_images": {
|
||||
"name": "prompt_images",
|
||||
"tooltip": "Guidance images anchored to the output video, from Aleph2 Prompt Image nodes (up to 5). Use keyframes or prompt images, not both."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"RunwayFirstLastFrameNode": {
|
||||
"display_name": "Runway First-Last-Frame to Video",
|
||||
"description": "Upload first and last keyframes, draft a prompt, and generate a video. More complex transitions, such as cases where the Last frame is completely different from the First frame, may benefit from the longer 10s duration. This would give the generation more time to smoothly transition between the two inputs. Before diving in, review these best practices to ensure that your input selections will set your generation up for success: https://help.runwayml.com/hc/en-us/articles/34170748696595-Creating-with-Keyframes-on-Gen-3.",
|
||||
@@ -16717,7 +16955,7 @@
|
||||
},
|
||||
"replacement_mode": {
|
||||
"name": "replacement_mode",
|
||||
"tooltip": "False = mask_video has black bg (Animation Mode). True = white bg (Replacement Mode). Set the matching replacement_mode on WanSCAILToVideo. reference_image_mask is always black-bg regardless."
|
||||
"tooltip": "False = Animation Mode (pose_video_mask has black background, reference_image_mask has white background). True = Replacement Mode (pose_video_mask has white background, reference_image_mask has black background)."
|
||||
},
|
||||
"ref_track_data": {
|
||||
"name": "ref_track_data",
|
||||
@@ -19356,6 +19594,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TripoImportModelNode": {
|
||||
"display_name": "Tripo: Import Model",
|
||||
"description": "Import an external 3D model (e.g. from Rodin, Hunyuan3D or a local file) into Tripo to use it with Tripo's post-processing nodes: Texture, Rig, Convert. GLB is recommended: textures survive import only when embedded in the file. Note that texturing an imported model requires a texture prompt.",
|
||||
"inputs": {
|
||||
"model_3d": {
|
||||
"name": "model_3d",
|
||||
"tooltip": "3D model to import (GLB / FBX / OBJ / STL, up to 150 MB). OBJ and STL files carry no embedded textures."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "model task_id",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"TripoMultiviewToModelNode": {
|
||||
"display_name": "Tripo: Multiview to Model",
|
||||
"inputs": {
|
||||
@@ -19829,6 +20083,10 @@
|
||||
},
|
||||
"texture_alignment": {
|
||||
"name": "texture_alignment"
|
||||
},
|
||||
"texture_prompt": {
|
||||
"name": "texture_prompt",
|
||||
"tooltip": "Optional text guidance for texturing. Required in practice for imported models (Tripo: Import Model), which carry no source image to infer colors from."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
|
||||
@@ -822,6 +822,8 @@
|
||||
"CONDITIONING": "ACONDICIONAMIENTO",
|
||||
"CONTROL_NET": "RED_DE_CONTROL",
|
||||
"CURVE": "CURVA",
|
||||
"DA3_GEOMETRY": "DA3_GEOMETRY",
|
||||
"DA3_MODEL": "DA3_MODEL",
|
||||
"ELEVENLABS_VOICE": "ELEVENLABS_VOICE",
|
||||
"FACE_DETECTION_MODEL": "MODELO_DE_DETECCIÓN_DE_CARAS",
|
||||
"FACE_LANDMARKS": "FACE_LANDMARKS",
|
||||
@@ -883,6 +885,8 @@
|
||||
"RECRAFT_V3_STYLE": "ESTILO RECRAFT V3",
|
||||
"RETARGET_TASK_ID": "ID_TAREA_REDESTINACIÓN",
|
||||
"RIG_TASK_ID": "ID_TAREA_ARMADURA",
|
||||
"RUNWAY_ALEPH2_KEYFRAME": "RUNWAY_ALEPH2_KEYFRAME",
|
||||
"RUNWAY_ALEPH2_PROMPT_IMAGE": "RUNWAY_ALEPH2_PROMPT_IMAGE",
|
||||
"SAM3_TRACK_DATA": "SAM3_TRACK_DATA",
|
||||
"SAMPLER": "MUESTREADOR",
|
||||
"SIGMAS": "SIGMAS",
|
||||
@@ -3091,11 +3095,9 @@
|
||||
"collapse": "Colapsar",
|
||||
"expand": "Expandir",
|
||||
"installAll": "Instalar todo",
|
||||
"installNodePack": "Instalar paquete de nodos",
|
||||
"installed": "Instalado",
|
||||
"installing": "Instalando...",
|
||||
"ossManagerDisabledHint": "Para instalar los nodos que faltan, primero ejecuta {pipCmd} en tu entorno de Python para instalar Node Manager, luego reinicia ComfyUI con la bandera {flag}.",
|
||||
"searchInManager": "Buscar en el Gestor de Nodos",
|
||||
"title": "Paquetes de nodos faltantes",
|
||||
"unknownPack": "Paquete desconocido",
|
||||
"unsupportedTitle": "Paquetes de nodos no compatibles",
|
||||
@@ -3673,6 +3675,7 @@
|
||||
"comfyCloudLogo": "Logo de Comfy Cloud",
|
||||
"contactOwnerToSubscribe": "Contacta al propietario del espacio de trabajo para suscribirte",
|
||||
"contactUs": "Contáctanos",
|
||||
"creditSliderSave": "Ahorra {percent}% ({amount})",
|
||||
"creditsRemainingThisMonth": "Créditos restantes este mes",
|
||||
"creditsRemainingThisYear": "Créditos restantes este año",
|
||||
"creditsYouveAdded": "Créditos que has agregado",
|
||||
|
||||