mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-25 23:07:46 +00:00
Compare commits
30 Commits
fix/load-a
...
drjkl/desl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d88d68c17 | ||
|
|
fbb31a13a2 | ||
|
|
e77f9d5cd9 | ||
|
|
425e06a07e | ||
|
|
62c88bf3d0 | ||
|
|
3c166ef654 | ||
|
|
64c3dac4e9 | ||
|
|
7f53451ab9 | ||
|
|
c72ab181b0 | ||
|
|
30f6260b97 | ||
|
|
3956fa68a8 | ||
|
|
376c32df05 | ||
|
|
4796154884 | ||
|
|
2205ead595 | ||
|
|
dad9b4e71e | ||
|
|
d11e82cfbe | ||
|
|
2f0e84100d | ||
|
|
2a46bbf420 | ||
|
|
335b4edc63 | ||
|
|
85572cf7aa | ||
|
|
9eb5e18a65 | ||
|
|
1cbfdbc0cb | ||
|
|
2efe71df89 | ||
|
|
cb49fdbda2 | ||
|
|
f25427c845 | ||
|
|
17093f9721 | ||
|
|
eb27404987 | ||
|
|
8799d4be07 | ||
|
|
82ba0028fb | ||
|
|
ca42569cb1 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -98,4 +98,5 @@ vitest.config.*.timestamp*
|
||||
# Weekly docs check output
|
||||
/output.txt
|
||||
|
||||
.amp
|
||||
.amp
|
||||
.venv
|
||||
@@ -1,19 +1,7 @@
|
||||
export interface UVMirror {
|
||||
/**
|
||||
* The setting id defined for the mirror.
|
||||
*/
|
||||
settingId: string
|
||||
/**
|
||||
* The default mirror to use.
|
||||
*/
|
||||
mirror: string
|
||||
/**
|
||||
* The fallback mirror to use.
|
||||
*/
|
||||
fallbackMirror: string
|
||||
/**
|
||||
* The path suffix to validate the mirror is reachable.
|
||||
*/
|
||||
validationPathSuffix?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,33 @@
|
||||
import type { PrimeVueSeverity } from '../primeVueTypes'
|
||||
|
||||
interface MaintenanceTaskButton {
|
||||
/** The text to display on the button. */
|
||||
text?: string
|
||||
/** CSS classes used for the button icon, e.g. 'pi pi-external-link' */
|
||||
/** CSS classes, e.g. 'pi pi-external-link' */
|
||||
icon?: string
|
||||
}
|
||||
|
||||
/** A maintenance task, used by the maintenance page. */
|
||||
export interface MaintenanceTask {
|
||||
/** ID string used as i18n key */
|
||||
/** Used as i18n key */
|
||||
id: string
|
||||
/** The display name of the task, e.g. Git */
|
||||
name: string
|
||||
/** Short description of the task. */
|
||||
shortDescription?: string
|
||||
/** Description of the task when it is in an error state. */
|
||||
errorDescription?: string
|
||||
/** Description of the task when it is in a warning state. */
|
||||
warningDescription?: string
|
||||
/** Full description of the task when it is in an OK state. */
|
||||
description?: string
|
||||
/** URL to the image to show in card mode. */
|
||||
headerImg?: string
|
||||
/** The button to display on the task card / list item. */
|
||||
button?: MaintenanceTaskButton
|
||||
/** Whether to show a confirmation dialog before running the task. */
|
||||
requireConfirm?: boolean
|
||||
/** The text to display in the confirmation dialog. */
|
||||
confirmText?: string
|
||||
/** Called by onClick to run the actual task. */
|
||||
execute: (args?: unknown[]) => boolean | Promise<boolean>
|
||||
/** Show the button with `severity="danger"` */
|
||||
severity?: PrimeVueSeverity
|
||||
/** Whether this task should display the terminal window when run. */
|
||||
usesTerminal?: boolean
|
||||
/** If `true`, successful completion of this task will refresh install validation and automatically continue if successful. */
|
||||
/** If true, successful completion refreshes install validation and auto-continues. */
|
||||
isInstallationFix?: boolean
|
||||
}
|
||||
|
||||
/** The filter options for the maintenance task list. */
|
||||
export interface MaintenanceFilter {
|
||||
/** CSS classes used for the filter button icon, e.g. 'pi pi-cross' */
|
||||
/** CSS classes, e.g. 'pi pi-cross' */
|
||||
icon: string
|
||||
/** The text to display on the filter button. */
|
||||
value: string
|
||||
/** The tasks to display when this filter is selected. */
|
||||
tasks: ReadonlyArray<MaintenanceTask>
|
||||
}
|
||||
|
||||
@@ -2,11 +2,6 @@ import { isValidUrl } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
import { electronAPI } from './envUtil'
|
||||
|
||||
/**
|
||||
* Check if a mirror is reachable from the electron App.
|
||||
* @param mirror - The mirror to check.
|
||||
* @returns True if the mirror is reachable, false otherwise.
|
||||
*/
|
||||
export const checkMirrorReachable = async (mirror: string) => {
|
||||
return (
|
||||
isValidUrl(mirror) && (await electronAPI().NetWork.canAccessUrl(mirror))
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
type ElectronWindow = typeof window & {
|
||||
electronAPI?: ElectronAPI
|
||||
}
|
||||
|
||||
export function isElectron() {
|
||||
return 'electronAPI' in window && window.electronAPI !== undefined
|
||||
}
|
||||
|
||||
export function electronAPI() {
|
||||
return (window as any).electronAPI as ElectronAPI
|
||||
export function electronAPI(): ElectronAPI {
|
||||
return (window as ElectronWindow).electronAPI as ElectronAPI
|
||||
}
|
||||
|
||||
export function isNativeWindow() {
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function getPromotedWidgetCount(
|
||||
return promotedWidgets.length
|
||||
}
|
||||
|
||||
export async function getPromotedWidgetCountByName(
|
||||
export function getPromotedWidgetCountByName(
|
||||
comfyPage: ComfyPage,
|
||||
nodeId: string,
|
||||
widgetName: string
|
||||
|
||||
@@ -89,13 +89,11 @@
|
||||
"chart.js": "^4.5.0",
|
||||
"cva": "catalog:",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "catalog:",
|
||||
"es-toolkit": "^1.39.9",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"firebase": "catalog:",
|
||||
"fuse.js": "^7.0.0",
|
||||
"glob": "catalog:",
|
||||
"jsonata": "catalog:",
|
||||
"jsondiffpatch": "catalog:",
|
||||
"loglevel": "^1.9.2",
|
||||
@@ -145,6 +143,7 @@
|
||||
"@vue/test-utils": "catalog:",
|
||||
"@webgpu/types": "catalog:",
|
||||
"cross-env": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"eslint-import-resolver-typescript": "catalog:",
|
||||
@@ -155,6 +154,7 @@
|
||||
"eslint-plugin-unused-imports": "catalog:",
|
||||
"eslint-plugin-vue": "catalog:",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "catalog:",
|
||||
"globals": "catalog:",
|
||||
"happy-dom": "catalog:",
|
||||
"husky": "catalog:",
|
||||
|
||||
71
pnpm-lock.yaml
generated
71
pnpm-lock.yaml
generated
@@ -482,9 +482,6 @@ importers:
|
||||
dompurify:
|
||||
specifier: ^3.2.5
|
||||
version: 3.3.1
|
||||
dotenv:
|
||||
specifier: 'catalog:'
|
||||
version: 16.6.1
|
||||
es-toolkit:
|
||||
specifier: ^1.39.9
|
||||
version: 1.39.10
|
||||
@@ -500,9 +497,6 @@ importers:
|
||||
fuse.js:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
glob:
|
||||
specifier: 'catalog:'
|
||||
version: 13.0.6
|
||||
jsonata:
|
||||
specifier: 'catalog:'
|
||||
version: 2.1.0
|
||||
@@ -645,6 +639,9 @@ importers:
|
||||
cross-env:
|
||||
specifier: 'catalog:'
|
||||
version: 10.1.0
|
||||
dotenv:
|
||||
specifier: 'catalog:'
|
||||
version: 16.6.1
|
||||
eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 9.39.1(jiti@2.6.1)
|
||||
@@ -675,6 +672,9 @@ importers:
|
||||
fs-extra:
|
||||
specifier: ^11.2.0
|
||||
version: 11.3.2
|
||||
glob:
|
||||
specifier: 'catalog:'
|
||||
version: 13.0.6
|
||||
globals:
|
||||
specifier: 'catalog:'
|
||||
version: 16.5.0
|
||||
@@ -2451,21 +2451,25 @@ packages:
|
||||
resolution: {integrity: sha512-D+tPXB0tkSuDPsuXvyQIsF3f3PBWfAwIe9FkBWtVoDVYqE+jbz+tVGsjQMNWGafLE4sC8ZQdjhsxyT8I53Anbw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@nx/nx-linux-arm64-musl@22.5.2':
|
||||
resolution: {integrity: sha512-UbO527qqa8KLBi13uXto5SmxcZv1Smer7sPexJonshDlmrJsyvx5m8nm6tcSv04W5yQEL90vPlTux8dNvEDWrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@nx/nx-linux-x64-gnu@22.5.2':
|
||||
resolution: {integrity: sha512-wR6596Vr/Z+blUAmjLHG2TCQMs4O1oi9JXK1J/PoPeO9UqdHwStCJBAd61zDFSUYJe0x+dkeRQu96fE5BW8Kcg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@nx/nx-linux-x64-musl@22.5.2':
|
||||
resolution: {integrity: sha512-MBXOw4AH4FWl4orwVykj/e75awTNDePogrl3pXNX9NcQLdj6JzS4e2jaALQeRBQLxQzeFvFQV/W4PBzoPV6/NA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@nx/nx-win32-arm64-msvc@22.5.2':
|
||||
resolution: {integrity: sha512-SaWSZkRH5uV8vP2lj6RRv+kw2IzaIDXkutReOXpooshIWZl9KjrQELNTCZTYyhLDsMlcyhSvLFlTiA4NkZ8udw==}
|
||||
@@ -2631,41 +2635,49 @@ packages:
|
||||
resolution: {integrity: sha512-SVjjjtMW66Mza76PBGJLqB0KKyFTBnxmtDXLJPbL6ZPGSctcXVmujz7/WAc0rb9m2oV0cHQTtVjnq6orQnI/jg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-musl@11.15.0':
|
||||
resolution: {integrity: sha512-JDv2/AycPF2qgzEiDeMJCcSzKNDm3KxNg0KKWipoKEMDFqfM7LxNwwSVyAOGmrYlE4l3dg290hOMsr9xG7jv9g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu@11.15.0':
|
||||
resolution: {integrity: sha512-zbu9FhvBLW4KJxo7ElFvZWbSt4vP685Qc/Gyk/Ns3g2gR9qh2qWXouH8PWySy+Ko/qJ42+HJCLg+ZNcxikERfg==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu@11.15.0':
|
||||
resolution: {integrity: sha512-Kfleehe6B09C2qCnyIU01xLFqFXCHI4ylzkicfX/89j+gNHh9xyNdpEvit88Kq6i5tTGdavVnM6DQfOE2qNtlg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-musl@11.15.0':
|
||||
resolution: {integrity: sha512-J7LPiEt27Tpm8P+qURDwNc8q45+n+mWgyys4/V6r5A8v5gDentHRGUx3iVk5NxdKhgoGulrzQocPTZVosq25Eg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-s390x-gnu@11.15.0':
|
||||
resolution: {integrity: sha512-+8/d2tAScPjVJNyqa7GPGnqleTB/XW9dZJQ2D/oIM3wpH3TG+DaFEXBbk4QFJ9K9AUGBhvQvWU2mQyhK/yYn3Q==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-gnu@11.15.0':
|
||||
resolution: {integrity: sha512-xtvSzH7Nr5MCZI2FKImmOdTl9kzuQ51RPyLh451tvD2qnkg3BaqI9Ox78bTk57YJhlXPuxWSOL5aZhKAc9J6qg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-musl@11.15.0':
|
||||
resolution: {integrity: sha512-14YL1zuXj06+/tqsuUZuzL0T425WA/I4nSVN1kBXeC5WHxem6lQ+2HGvG+crjeJEqHgZUT62YIgj88W+8E7eyg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-openharmony-arm64@11.15.0':
|
||||
resolution: {integrity: sha512-/7Qli+1Wk93coxnrQaU8ySlICYN8HsgyIrzqjgIkQEpI//9eUeaeIHZptNl2fMvBGeXa7k2QgLbRNaBRgpnvMw==}
|
||||
@@ -2739,48 +2751,56 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.34.0':
|
||||
resolution: {integrity: sha512-H+F8+71gHQoGTFPPJ6z4dD0Fzfzi0UP8Zx94h5kUmIFThLvMq5K1Y/bUUubiXwwHfwb5C3MPjUpYijiy0rj51Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.34.0':
|
||||
resolution: {integrity: sha512-dIGnzTNhCXqQD5pzBwduLg8pClm+t8R53qaE9i5h8iua1iaFAJyLffh4847CNZSlASb7gn1Ofuv7KoG/EpoGZg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.34.0':
|
||||
resolution: {integrity: sha512-FGQ2GTTooilDte/ogwWwkHuuL3lGtcE3uKM2EcC7kOXNWdUfMY6Jx3JCodNVVbFoybv4A+HuCj8WJji2uu1Ceg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.34.0':
|
||||
resolution: {integrity: sha512-2dGbGneJ7ptOIVKMwEIHdCkdZEomh74X3ggo4hCzEXL/rl9HwfsZDR15MkqfQqAs6nVXMvtGIOMxjDYa5lwKaA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.34.0':
|
||||
resolution: {integrity: sha512-cCtGgmrTrxq3OeSG0UAO+w6yLZTMeOF4XM9SAkNrRUxYhRQELSDQ/iNPCLyHhYNi38uHJQbS5RQweLUDpI4ajA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.34.0':
|
||||
resolution: {integrity: sha512-7AvMzmeX+k7GdgitXp99GQoIV/QZIpAS7rwxQvC/T541yWC45nwvk4mpnU8N+V6dE5SPEObnqfhCjO80s7qIsg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.34.0':
|
||||
resolution: {integrity: sha512-uNiglhcmivJo1oDMh3hoN/Z0WsbEXOpRXZdQ3W/IkOpyV8WF308jFjSC1ZxajdcNRXWej0zgge9QXba58Owt+g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.34.0':
|
||||
resolution: {integrity: sha512-5eFsTjCyji25j6zznzlMc+wQAZJoL9oWy576xhqd2efv+N4g1swIzuSDcb1dz4gpcVC6veWe9pAwD7HnrGjLwg==}
|
||||
@@ -2883,48 +2903,56 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.49.0':
|
||||
resolution: {integrity: sha512-xeqkMOARgGBlEg9BQuPDf6ZW711X6BT5qjDyeM5XNowCJeTSdmMhpePJjTEiVbbr3t21sIlK8RE6X5bc04nWyQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.49.0':
|
||||
resolution: {integrity: sha512-uvcqRO6PnlJGbL7TeePhTK5+7/JXbxGbN+C6FVmfICDeeRomgQqrfVjf0lUrVpUU8ii8TSkIbNdft3M+oNlOsQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.49.0':
|
||||
resolution: {integrity: sha512-Dw1HkdXAwHNH+ZDserHP2RzXQmhHtpsYYI0hf8fuGAVCIVwvS6w1+InLxpPMY25P8ASRNiFN3hADtoh6lI+4lg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.49.0':
|
||||
resolution: {integrity: sha512-EPlMYaA05tJ9km/0dI9K57iuMq3Tw+nHst7TNIegAJZrBPtsOtYaMFZEaWj02HA8FI5QvSnRHMt+CI+RIhXJBQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.49.0':
|
||||
resolution: {integrity: sha512-yZiQL9qEwse34aMbnMb5VqiAWfDY+fLFuoJbHOuzB1OaJZbN1MRF9Nk+W89PIpGr5DNPDipwjZb8+Q7wOywoUQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.49.0':
|
||||
resolution: {integrity: sha512-CcCDwMMXSchNkhdgvhVn3DLZ4EnBXAD8o8+gRzahg+IdSt/72y19xBgShJgadIRF0TsRcV/MhDUMwL5N/W54aQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.49.0':
|
||||
resolution: {integrity: sha512-u3HfKV8BV6t6UCCbN0RRiyqcymhrnpunVmLFI8sEa5S/EBu+p/0bJ3D7LZ2KT6PsBbrB71SWq4DeFrskOVgIZg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.49.0':
|
||||
resolution: {integrity: sha512-dRDpH9fw+oeUMpM4br0taYCFpW6jQtOuEIec89rOgDA1YhqwmeRcx0XYeCv7U48p57qJ1XZHeMGM9LdItIjfzA==}
|
||||
@@ -3093,24 +3121,28 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-Z03/wrqau9Bicfgb3Dbs6SYTHliELk2PM2LpG2nFd+cGupTMF5kanLEcj2vuuJLLhptNyS61rtk7SOZ+lPsTUA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-iSXXZsQp08CSilff/DCTFZHSVEpEwdicV3W8idHyrByrcsRDVh9sGC3sev6d8BygSGj3vt8GvUKBPCoyMA4tgQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-qaj+MFudtdCv9xZo9znFvkgoajLdc+vwf0Kz5N44g+LU5XMe+IsACgn3UG7uTRlCCvhMAGXm1XlpEA5bZBrOcw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-U662UnMETyjT65gFmG9ma+XziENrs7BBnENi/27swZPYagubfHRirXHG2oMl+pEax2WvO7Kb9gHZmMakpYqBHQ==}
|
||||
@@ -3188,56 +3220,67 @@ packages:
|
||||
resolution: {integrity: sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.53.5':
|
||||
resolution: {integrity: sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.53.5':
|
||||
resolution: {integrity: sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.53.5':
|
||||
resolution: {integrity: sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.53.5':
|
||||
resolution: {integrity: sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.53.5':
|
||||
resolution: {integrity: sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==}
|
||||
@@ -3513,24 +3556,28 @@ packages:
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.0':
|
||||
resolution: {integrity: sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.0':
|
||||
resolution: {integrity: sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.2.0':
|
||||
resolution: {integrity: sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.2.0':
|
||||
resolution: {integrity: sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==}
|
||||
@@ -4006,41 +4053,49 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
@@ -6416,24 +6471,28 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.31.1:
|
||||
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
|
||||
|
||||
@@ -133,7 +133,7 @@ function isKeyUsed(key: string, sourceFiles: string[]): boolean {
|
||||
}
|
||||
|
||||
// Main function
|
||||
async function checkNewUnusedKeys() {
|
||||
function checkNewUnusedKeys() {
|
||||
const stagedLocaleFiles = getStagedLocaleFiles()
|
||||
|
||||
if (stagedLocaleFiles.length === 0) {
|
||||
@@ -165,7 +165,7 @@ async function checkNewUnusedKeys() {
|
||||
if (unusedNewKeys.length > 0) {
|
||||
console.warn('\n⚠️ Warning: Found unused NEW i18n keys:\n')
|
||||
|
||||
for (const key of unusedNewKeys.sort()) {
|
||||
for (const key of unusedNewKeys.sort((a, b) => a.localeCompare(b))) {
|
||||
console.warn(` - ${key}`)
|
||||
}
|
||||
|
||||
@@ -183,7 +183,9 @@ async function checkNewUnusedKeys() {
|
||||
}
|
||||
|
||||
// Run the check
|
||||
checkNewUnusedKeys().catch((err) => {
|
||||
try {
|
||||
checkNewUnusedKeys()
|
||||
} catch (err) {
|
||||
console.error('Error checking unused keys:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ function resolveRelease(
|
||||
return null
|
||||
}
|
||||
|
||||
const [major, currentMinor, patch] = currentVersion.split('.').map(Number)
|
||||
const [, currentMinor] = currentVersion.split('.').map(Number)
|
||||
|
||||
// Fetch all branches
|
||||
exec('git fetch origin', frontendRepoPath)
|
||||
@@ -264,7 +264,7 @@ if (!releaseInfo) {
|
||||
}
|
||||
|
||||
// Output as JSON for GitHub Actions
|
||||
|
||||
// oxlint-disable-next-line no-console -- CI script output
|
||||
console.log(JSON.stringify(releaseInfo, null, 2))
|
||||
|
||||
export { resolveRelease }
|
||||
|
||||
@@ -51,10 +51,10 @@ export function downloadFile(url: string, filename?: string): void {
|
||||
|
||||
/**
|
||||
* Download a Blob by creating a temporary object URL and anchor element
|
||||
* @param filename - The filename to suggest to the browser
|
||||
* @param blob - The Blob to download
|
||||
* @param filename - The filename to suggest to the browser
|
||||
*/
|
||||
export function downloadBlob(filename: string, blob: Blob): void {
|
||||
export function downloadBlob(blob: Blob, filename: string): void {
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
triggerLinkDownload(url, filename)
|
||||
@@ -138,7 +138,7 @@ async function downloadViaBlobFetch(
|
||||
extractFilenameFromContentDisposition(contentDisposition)
|
||||
|
||||
const blob = await response.blob()
|
||||
downloadBlob(headerFilename ?? fallbackFilename, blob)
|
||||
downloadBlob(blob, headerFilename ?? fallbackFilename)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,19 +45,19 @@ export const usdToCredits = (usd: number): number =>
|
||||
export const creditsToUsd = (credits: number): number =>
|
||||
Math.round((credits / CREDITS_PER_USD) * 100) / 100
|
||||
|
||||
export type FormatOptions = {
|
||||
type FormatOptions = {
|
||||
value: number
|
||||
locale?: string
|
||||
numberOptions?: Intl.NumberFormatOptions
|
||||
}
|
||||
|
||||
export type FormatFromCentsOptions = {
|
||||
type FormatFromCentsOptions = {
|
||||
cents: number
|
||||
locale?: string
|
||||
numberOptions?: Intl.NumberFormatOptions
|
||||
}
|
||||
|
||||
export type FormatFromUsdOptions = {
|
||||
type FormatFromUsdOptions = {
|
||||
usd: number
|
||||
locale?: string
|
||||
numberOptions?: Intl.NumberFormatOptions
|
||||
@@ -113,13 +113,3 @@ export const formatUsdFromCents = ({
|
||||
locale,
|
||||
numberOptions
|
||||
})
|
||||
|
||||
/**
|
||||
* Clamps a USD value to the allowed range for credit purchases
|
||||
* @param value - The USD amount to clamp
|
||||
* @returns The clamped value between $1 and $1000, or 0 if NaN
|
||||
*/
|
||||
export const clampUsd = (value: number): number => {
|
||||
if (Number.isNaN(value)) return 0
|
||||
return Math.min(1000, Math.max(1, value))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
/**
|
||||
* Utilities for pointer event handling
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if a pointer or mouse event is a middle button input
|
||||
* @param event - The pointer or mouse event to check
|
||||
* @returns true if the event is from the middle button/wheel
|
||||
*/
|
||||
export function isMiddlePointerInput(
|
||||
event: PointerEvent | MouseEvent
|
||||
): boolean {
|
||||
|
||||
@@ -141,7 +141,7 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import { computed } from 'vue'
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
useDraggable,
|
||||
useEventListener,
|
||||
@@ -108,7 +108,7 @@ import StatusBadge from '@/components/common/StatusBadge.vue'
|
||||
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
@@ -41,7 +41,7 @@ const mockCommands: ComfyCommandImpl[] = [
|
||||
icon: 'pi pi-test',
|
||||
tooltip: 'Test tooltip',
|
||||
menubarLabel: 'Other Command',
|
||||
keybinding: null
|
||||
keybinding: undefined
|
||||
} as ComfyCommandImpl
|
||||
]
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('ShortcutsList', () => {
|
||||
id: 'No.Keybinding',
|
||||
label: 'No Keybinding',
|
||||
category: 'essentials',
|
||||
keybinding: null
|
||||
keybinding: undefined
|
||||
} as ComfyCommandImpl
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { fetchModelMetadata } from './missingModelsUtils'
|
||||
import {
|
||||
fetchModelMetadata,
|
||||
getBadgeLabel,
|
||||
hasValidDirectory,
|
||||
isModelDownloadable
|
||||
} from '@/components/dialog/content/missingModelsUtils'
|
||||
import type { ModelWithUrl } from '@/components/dialog/content/missingModelsUtils'
|
||||
|
||||
const fetchMock = vi.fn()
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
@@ -8,6 +14,84 @@ vi.stubGlobal('fetch', fetchMock)
|
||||
vi.mock('@/platform/distribution/types', () => ({ isDesktop: false }))
|
||||
vi.mock('@/stores/electronDownloadStore', () => ({}))
|
||||
|
||||
function makeModel(overrides: Partial<ModelWithUrl> = {}): ModelWithUrl {
|
||||
return {
|
||||
name: 'model.safetensors',
|
||||
url: 'https://civitai.com/api/download/12345',
|
||||
directory: 'checkpoints',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('isModelDownloadable', () => {
|
||||
it('allows civitai URLs with valid suffix', () => {
|
||||
expect(isModelDownloadable(makeModel())).toBe(true)
|
||||
})
|
||||
|
||||
it('allows huggingface URLs with valid suffix', () => {
|
||||
expect(
|
||||
isModelDownloadable(
|
||||
makeModel({
|
||||
url: 'https://huggingface.co/some/model',
|
||||
name: 'model.ckpt'
|
||||
})
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('allows localhost URLs with valid suffix', () => {
|
||||
expect(
|
||||
isModelDownloadable(makeModel({ url: 'http://localhost:8080/model' }))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects URLs from unknown sources', () => {
|
||||
expect(
|
||||
isModelDownloadable(
|
||||
makeModel({ url: 'https://evil.com/model.safetensors' })
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects files with invalid suffix', () => {
|
||||
expect(isModelDownloadable(makeModel({ name: 'model.exe' }))).toBe(false)
|
||||
})
|
||||
|
||||
it('allows whitelisted URLs regardless of suffix', () => {
|
||||
expect(
|
||||
isModelDownloadable(
|
||||
makeModel({
|
||||
url: 'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth',
|
||||
name: 'RealESRGAN_x4plus.pth'
|
||||
})
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasValidDirectory', () => {
|
||||
it('returns true when directory exists in paths', () => {
|
||||
const paths = { checkpoints: ['/models/checkpoints'] }
|
||||
expect(hasValidDirectory(makeModel(), paths)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when directory is missing', () => {
|
||||
expect(hasValidDirectory(makeModel(), {})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getBadgeLabel', () => {
|
||||
it('maps known directories to badge labels', () => {
|
||||
expect(getBadgeLabel('vae')).toBe('VAE')
|
||||
expect(getBadgeLabel('loras')).toBe('LORA')
|
||||
expect(getBadgeLabel('checkpoints')).toBe('CHECKPOINT')
|
||||
})
|
||||
|
||||
it('uppercases unknown directories', () => {
|
||||
expect(getBadgeLabel('custom_dir')).toBe('CUSTOM_DIR')
|
||||
})
|
||||
})
|
||||
|
||||
let testId = 0
|
||||
|
||||
describe('fetchModelMetadata', () => {
|
||||
|
||||
@@ -149,7 +149,7 @@ import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||
import { useNodeBadge } from '@/composables/node/useNodeBadge'
|
||||
import { useCanvasDrop } from '@/composables/useCanvasDrop'
|
||||
import { useContextMenuTranslation } from '@/composables/useContextMenuTranslation'
|
||||
import { useCopy } from '@/composables/useCopy'
|
||||
import { useGraphCopyHandler } from '@/composables/useGraphCopyHandler'
|
||||
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
||||
import { usePaste } from '@/composables/usePaste'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
@@ -446,7 +446,7 @@ useNodeBadge()
|
||||
|
||||
useGlobalLitegraph()
|
||||
useContextMenuTranslation()
|
||||
useCopy()
|
||||
useGraphCopyHandler()
|
||||
usePaste()
|
||||
useWorkflowAutoSave()
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<ZoomControlsModal :visible="isModalVisible" @close="hideModal" />
|
||||
<ZoomControlsModal :visible="isPopoverOpen" @close="hidePopover" />
|
||||
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
v-if="hasActivePopup"
|
||||
class="fixed inset-0 z-1200"
|
||||
@click="hideModal"
|
||||
@click="hidePopover"
|
||||
></div>
|
||||
|
||||
<ButtonGroup
|
||||
@@ -40,7 +40,7 @@
|
||||
:aria-label="t('zoomControls.label')"
|
||||
data-testid="zoom-controls-button"
|
||||
:style="stringifiedMinimapStyles.buttonStyles"
|
||||
@click="toggleModal"
|
||||
@click="togglePopover"
|
||||
>
|
||||
<span class="inline-flex items-center gap-1 px-2 text-xs">
|
||||
<span>{{ canvasStore.appScalePercentage }}%</span>
|
||||
@@ -110,7 +110,7 @@ const settingStore = useSettingStore()
|
||||
const canvasInteractions = useCanvasInteractions()
|
||||
const minimap = useMinimap()
|
||||
|
||||
const { isModalVisible, toggleModal, hideModal, hasActivePopup } =
|
||||
const { isPopoverOpen, togglePopover, hidePopover, hasActivePopup } =
|
||||
useZoomControls()
|
||||
|
||||
const stringifiedMinimapStyles = computed(() => {
|
||||
@@ -157,7 +157,7 @@ const minimapCommandText = computed(() =>
|
||||
// Computed properties for button classes and states
|
||||
const zoomButtonClass = computed(() => [
|
||||
'bg-comfy-menu-bg',
|
||||
isModalVisible.value ? 'not-active:bg-interface-panel-selected-surface!' : '',
|
||||
isPopoverOpen.value ? 'not-active:bg-interface-panel-selected-surface!' : '',
|
||||
'hover:bg-interface-button-hover-surface!',
|
||||
'p-0',
|
||||
'h-8',
|
||||
|
||||
@@ -76,7 +76,7 @@ import Load3DScene from '@/components/load3d/Load3DScene.vue'
|
||||
import AnimationControls from '@/components/load3d/controls/AnimationControls.vue'
|
||||
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
|
||||
import ViewerControls from '@/components/load3d/controls/ViewerControls.vue'
|
||||
import { useLoad3d } from '@/composables/useLoad3d'
|
||||
import { useLoad3d } from '@/extensions/core/load3d/composables/useLoad3d'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
|
||||
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
|
||||
import { useLoad3dDrag } from '@/extensions/core/load3d/composables/useLoad3dDrag'
|
||||
|
||||
const props = defineProps<{
|
||||
initializeLoad3d: (containerRef: HTMLElement) => Promise<void>
|
||||
|
||||
@@ -103,8 +103,8 @@ import LightControls from '@/components/load3d/controls/viewer/ViewerLightContro
|
||||
import ModelControls from '@/components/load3d/controls/viewer/ViewerModelControls.vue'
|
||||
import SceneControls from '@/components/load3d/controls/viewer/ViewerSceneControls.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
|
||||
import { useLoad3dViewer } from '@/composables/useLoad3dViewer'
|
||||
import { useLoad3dDrag } from '@/extensions/core/load3d/composables/useLoad3dDrag'
|
||||
import { useLoad3dViewer } from '@/extensions/core/load3d/composables/useLoad3dViewer'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
@@ -200,7 +200,7 @@ const onMaskOpacityChange = (value: number) => {
|
||||
maskLayerVisible.value = value !== 0
|
||||
}
|
||||
|
||||
const onBlendModeChange = async (event: Event) => {
|
||||
const onBlendModeChange = (event: Event) => {
|
||||
const value = (event.target as HTMLSelectElement).value
|
||||
let blendMode: MaskBlendMode
|
||||
|
||||
@@ -217,7 +217,7 @@ const onBlendModeChange = async (event: Event) => {
|
||||
|
||||
store.maskBlendMode = blendMode
|
||||
|
||||
await canvasManager.updateMaskColor()
|
||||
canvasManager.updateMaskColor()
|
||||
}
|
||||
|
||||
const setActiveLayer = (layer: ImageLayer) => {
|
||||
|
||||
@@ -149,7 +149,7 @@ const initUI = async () => {
|
||||
const imageLoader = useImageLoader()
|
||||
const image = await imageLoader.loadImages()
|
||||
|
||||
await panZoom.initializeCanvasPanZoom(
|
||||
panZoom.initializeCanvasPanZoom(
|
||||
image,
|
||||
containerRef.value,
|
||||
toolPanelRef.value?.$el as HTMLElement | undefined,
|
||||
@@ -181,9 +181,9 @@ onMounted(() => {
|
||||
keyboard.addListeners()
|
||||
|
||||
if (containerRef.value) {
|
||||
resizeObserver = new ResizeObserver(async () => {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (panZoom) {
|
||||
await panZoom.invalidatePanZoom()
|
||||
panZoom.invalidatePanZoom()
|
||||
}
|
||||
})
|
||||
resizeObserver.observe(containerRef.value)
|
||||
|
||||
@@ -79,16 +79,16 @@ const handleTouchStart = (event: TouchEvent) => {
|
||||
panZoom.handleTouchStart(event)
|
||||
}
|
||||
|
||||
const handleTouchMove = async (event: TouchEvent) => {
|
||||
await panZoom.handleTouchMove(event)
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
panZoom.handleTouchMove(event)
|
||||
}
|
||||
|
||||
const handleTouchEnd = (event: TouchEvent) => {
|
||||
panZoom.handleTouchEnd(event)
|
||||
}
|
||||
|
||||
const handleWheel = async (event: WheelEvent) => {
|
||||
await panZoom.zoom(event)
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
panZoom.zoom(event)
|
||||
const newCursorPoint = { x: event.clientX, y: event.clientY }
|
||||
panZoom.updateCursorPosition(newCursorPoint)
|
||||
}
|
||||
|
||||
@@ -161,33 +161,33 @@ const onRedo = () => {
|
||||
store.canvasHistory.redo()
|
||||
}
|
||||
|
||||
const onRotateLeft = async () => {
|
||||
const onRotateLeft = () => {
|
||||
try {
|
||||
await canvasTransform.rotateCounterclockwise()
|
||||
canvasTransform.rotateCounterclockwise()
|
||||
} catch (error) {
|
||||
console.error('[TopBarHeader] Rotate left failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const onRotateRight = async () => {
|
||||
const onRotateRight = () => {
|
||||
try {
|
||||
await canvasTransform.rotateClockwise()
|
||||
canvasTransform.rotateClockwise()
|
||||
} catch (error) {
|
||||
console.error('[TopBarHeader] Rotate right failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const onMirrorHorizontal = async () => {
|
||||
const onMirrorHorizontal = () => {
|
||||
try {
|
||||
await canvasTransform.mirrorHorizontal()
|
||||
canvasTransform.mirrorHorizontal()
|
||||
} catch (error) {
|
||||
console.error('[TopBarHeader] Mirror horizontal failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const onMirrorVertical = async () => {
|
||||
const onMirrorVertical = () => {
|
||||
try {
|
||||
await canvasTransform.mirrorVertical()
|
||||
canvasTransform.mirrorVertical()
|
||||
} catch (error) {
|
||||
console.error('[TopBarHeader] Mirror vertical failed:', error)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import QueueOverlayActive from './QueueOverlayActive.vue'
|
||||
import * as tooltipConfig from '@/composables/useTooltipConfig'
|
||||
import * as tooltipConfig from '@/utils/tooltipConfig'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
|
||||
@@ -95,7 +95,7 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
|
||||
defineProps<{
|
||||
totalProgressStyle: Record<string, string>
|
||||
|
||||
@@ -48,7 +48,7 @@ vi.mock('@/stores/workspace/sidebarTabStore', () => ({
|
||||
}))
|
||||
|
||||
import QueueOverlayHeader from './QueueOverlayHeader.vue'
|
||||
import * as tooltipConfig from '@/composables/useTooltipConfig'
|
||||
import * as tooltipConfig from '@/utils/tooltipConfig'
|
||||
|
||||
const tooltipDirectiveStub = {
|
||||
mounted: vi.fn(),
|
||||
|
||||
@@ -32,7 +32,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import JobHistoryActionsMenu from '@/components/queue/JobHistoryActionsMenu.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
|
||||
defineProps<{
|
||||
headerTitle: string
|
||||
|
||||
@@ -121,7 +121,7 @@ import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { jobSortModes } from '@/composables/queue/useJobList'
|
||||
import type { JobSortMode } from '@/composables/queue/useJobList'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
|
||||
const {
|
||||
hideShowAssetsAction = false,
|
||||
|
||||
@@ -193,8 +193,15 @@ import { useI18n } from 'vue-i18n'
|
||||
import JobDetailsPopover from '@/components/queue/job/JobDetailsPopover.vue'
|
||||
import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useProgressBarBackground } from '@/composables/useProgressBarBackground'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import {
|
||||
progressBarContainerClass,
|
||||
progressBarPrimaryClass,
|
||||
progressBarSecondaryClass,
|
||||
hasProgressPercent,
|
||||
hasAnyProgressPercent,
|
||||
progressPercentStyle
|
||||
} from '@/composables/useProgressBarBackground'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import type { JobState } from '@/types/queue'
|
||||
import { iconForJobState } from '@/utils/queueDisplay'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
@@ -237,14 +244,6 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const {
|
||||
progressBarContainerClass,
|
||||
progressBarPrimaryClass,
|
||||
progressBarSecondaryClass,
|
||||
hasProgressPercent,
|
||||
hasAnyProgressPercent,
|
||||
progressPercentStyle
|
||||
} = useProgressBarBackground()
|
||||
|
||||
const cancelTooltipConfig = computed(() => buildTooltipConfig(t('g.cancel')))
|
||||
const deleteTooltipConfig = computed(() => buildTooltipConfig(t('g.delete')))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -533,8 +533,8 @@ const handleBulkDelete = async (assets: AssetItem[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleBulkAddToWorkflow = async (assets: AssetItem[]) => {
|
||||
await addMultipleToWorkflow(assets)
|
||||
const handleBulkAddToWorkflow = (assets: AssetItem[]) => {
|
||||
addMultipleToWorkflow(assets)
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
|
||||
@@ -190,39 +190,35 @@ function useBillingContextInternal(): BillingContext {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStatus(): Promise<void> {
|
||||
function fetchStatus(): Promise<void> {
|
||||
return activeContext.value.fetchStatus()
|
||||
}
|
||||
|
||||
async function fetchBalance(): Promise<void> {
|
||||
function fetchBalance(): Promise<void> {
|
||||
return activeContext.value.fetchBalance()
|
||||
}
|
||||
|
||||
async function subscribe(
|
||||
planSlug: string,
|
||||
returnUrl?: string,
|
||||
cancelUrl?: string
|
||||
) {
|
||||
function subscribe(planSlug: string, returnUrl?: string, cancelUrl?: string) {
|
||||
return activeContext.value.subscribe(planSlug, returnUrl, cancelUrl)
|
||||
}
|
||||
|
||||
async function previewSubscribe(planSlug: string) {
|
||||
function previewSubscribe(planSlug: string) {
|
||||
return activeContext.value.previewSubscribe(planSlug)
|
||||
}
|
||||
|
||||
async function manageSubscription() {
|
||||
function manageSubscription() {
|
||||
return activeContext.value.manageSubscription()
|
||||
}
|
||||
|
||||
async function cancelSubscription() {
|
||||
function cancelSubscription() {
|
||||
return activeContext.value.cancelSubscription()
|
||||
}
|
||||
|
||||
async function fetchPlans() {
|
||||
function fetchPlans() {
|
||||
return activeContext.value.fetchPlans()
|
||||
}
|
||||
|
||||
async function requireActiveSubscription() {
|
||||
function requireActiveSubscription() {
|
||||
return activeContext.value.requireActiveSubscription()
|
||||
}
|
||||
|
||||
|
||||
@@ -137,11 +137,11 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
await legacySubscribe()
|
||||
}
|
||||
|
||||
async function previewSubscribe(
|
||||
function previewSubscribe(
|
||||
_planSlug: string
|
||||
): Promise<PreviewSubscribeResponse | null> {
|
||||
// Legacy billing doesn't support preview - returns null
|
||||
return null
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
async function manageSubscription(): Promise<void> {
|
||||
@@ -152,9 +152,10 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
await legacyManageSubscription()
|
||||
}
|
||||
|
||||
async function fetchPlans(): Promise<void> {
|
||||
function fetchPlans(): Promise<void> {
|
||||
// Legacy billing doesn't have workspace-style plans
|
||||
// Plans are hardcoded in the UI for legacy subscriptions
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
async function requireActiveSubscription(): Promise<void> {
|
||||
|
||||
@@ -4,28 +4,13 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { collectFromNodes } from '@/utils/graphTraversalUtil'
|
||||
|
||||
/**
|
||||
* Composable for handling selected LiteGraph items filtering and operations.
|
||||
* This provides utilities for working with selected items on the canvas,
|
||||
* including filtering out items that should not be included in selection operations.
|
||||
*/
|
||||
export function useSelectedLiteGraphItems() {
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
/**
|
||||
* Items that should not show in the selection overlay are ignored.
|
||||
* @param item - The item to check.
|
||||
* @returns True if the item should be ignored, false otherwise.
|
||||
*/
|
||||
const isIgnoredItem = (item: Positionable): boolean => {
|
||||
return item instanceof Reroute
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out items that should not show in the selection overlay.
|
||||
* @param items - The Set of items to filter.
|
||||
* @returns The filtered Set of items.
|
||||
*/
|
||||
const filterSelectableItems = (
|
||||
items: Set<Positionable>
|
||||
): Set<Positionable> => {
|
||||
@@ -38,37 +23,20 @@ export function useSelectedLiteGraphItems() {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filtered selected items from the canvas.
|
||||
* @returns The filtered Set of selected items.
|
||||
*/
|
||||
const getSelectableItems = (): Set<Positionable> => {
|
||||
const { selectedItems } = canvasStore.getCanvas()
|
||||
return filterSelectableItems(selectedItems)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any selectable items.
|
||||
* @returns True if there are selectable items, false otherwise.
|
||||
*/
|
||||
const hasSelectableItems = (): boolean => {
|
||||
return getSelectableItems().size > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are multiple selectable items.
|
||||
* @returns True if there are multiple selectable items, false otherwise.
|
||||
*/
|
||||
const hasMultipleSelectableItems = (): boolean => {
|
||||
return getSelectableItems().size > 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only the selected nodes (LGraphNode instances) from the canvas.
|
||||
* This filters out other types of selected items like groups or reroutes.
|
||||
* If a selected node is a subgraph, this also includes all nodes within it.
|
||||
* @returns Array of selected LGraphNode instances and their descendants.
|
||||
*/
|
||||
/** Includes descendant nodes from any selected subgraphs. */
|
||||
const getSelectedNodes = (): LGraphNode[] => {
|
||||
const selectedNodes = app.canvas.selected_nodes
|
||||
if (!selectedNodes) return []
|
||||
|
||||
@@ -2,13 +2,13 @@ import type { CSSProperties } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||
import type { Size, Vector2 } from '@/lib/litegraph/src/litegraph'
|
||||
import type { Point, Size } from '@/lib/litegraph/src/interfaces'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
|
||||
export interface PositionConfig {
|
||||
/* The position of the element on litegraph canvas */
|
||||
pos: Vector2
|
||||
pos: Point
|
||||
/* The size of the element on litegraph canvas */
|
||||
size: Size
|
||||
/* The scale factor of the canvas */
|
||||
|
||||
@@ -530,7 +530,6 @@ function captureDynamicSubmenu(
|
||||
return converted
|
||||
}
|
||||
|
||||
console.warn('[ContextMenuConverter] No items captured for:', item.content)
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export function useSubgraphOperations() {
|
||||
workflowStore.activeWorkflow?.changeTracker?.checkState()
|
||||
}
|
||||
|
||||
const doUnpack = (
|
||||
const unpackNodes = (
|
||||
subgraphNodes: SubgraphNode[],
|
||||
skipMissingNodes: boolean
|
||||
) => {
|
||||
@@ -65,7 +65,7 @@ export function useSubgraphOperations() {
|
||||
if (subgraphNodes.length === 0) {
|
||||
return
|
||||
}
|
||||
doUnpack(subgraphNodes, true)
|
||||
unpackNodes(subgraphNodes, true)
|
||||
}
|
||||
|
||||
const addSubgraphToLibrary = async () => {
|
||||
|
||||
@@ -64,8 +64,8 @@ function useVueNodeLifecycleIndividual() {
|
||||
|
||||
try {
|
||||
nodeManager.value.cleanup()
|
||||
} catch {
|
||||
/* empty */
|
||||
} catch (error) {
|
||||
console.warn('Node manager cleanup failed:', error)
|
||||
}
|
||||
nodeManager.value = null
|
||||
}
|
||||
|
||||
@@ -32,21 +32,12 @@ const saveBrushToCache = debounce(function (key: string, brush: Brush): void {
|
||||
}
|
||||
}, 300)
|
||||
|
||||
/**
|
||||
* Loads brush settings from local storage.
|
||||
* @param key - The storage key.
|
||||
* @returns The brush settings object or null if not found.
|
||||
*/
|
||||
function loadBrushFromCache(key: string): Brush | null {
|
||||
try {
|
||||
const brushString = getStorageValue(key)
|
||||
if (brushString) {
|
||||
return JSON.parse(brushString) as Brush
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load brush from cache:', error)
|
||||
if (brushString) return JSON.parse(brushString) as Brush
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -220,12 +211,12 @@ export function useBrushDrawing(initialSettings?: {
|
||||
// Sync GPU on Undo/Redo
|
||||
watch(
|
||||
() => store.canvasHistory.currentStateIndex,
|
||||
async () => {
|
||||
() => {
|
||||
// Skip update if state was just saved
|
||||
if (isSavingHistory.value) return
|
||||
|
||||
// Update GPU textures to match restored canvas state
|
||||
await updateGPUFromCanvas()
|
||||
updateGPUFromCanvas()
|
||||
|
||||
// Clear preview to remove artifacts
|
||||
if (renderer && previewContext) {
|
||||
@@ -238,7 +229,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
|
||||
watch(
|
||||
() => store.gpuTexturesNeedRecreation,
|
||||
async (needsRecreation) => {
|
||||
(needsRecreation) => {
|
||||
if (
|
||||
!needsRecreation ||
|
||||
!device ||
|
||||
@@ -303,7 +294,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
)
|
||||
} else {
|
||||
// Fallback: read from canvas
|
||||
await updateGPUFromCanvas()
|
||||
updateGPUFromCanvas()
|
||||
}
|
||||
|
||||
// Update preview canvas if it exists
|
||||
@@ -426,7 +417,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
/**
|
||||
* Updates the GPU textures from the current canvas state.
|
||||
*/
|
||||
async function updateGPUFromCanvas(): Promise<void> {
|
||||
function updateGPUFromCanvas(): void {
|
||||
if (
|
||||
!device ||
|
||||
!maskTexture ||
|
||||
@@ -523,7 +514,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
})
|
||||
|
||||
// Upload initial data
|
||||
await updateGPUFromCanvas()
|
||||
updateGPUFromCanvas()
|
||||
|
||||
console.warn('✅ GPU resources initialized successfully')
|
||||
|
||||
@@ -810,7 +801,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
* Draws a point using the stroke processor for smoothing.
|
||||
* @param point - The point to draw.
|
||||
*/
|
||||
async function drawWithBetterSmoothing(point: Point): Promise<void> {
|
||||
function drawWithBetterSmoothing(point: Point): void {
|
||||
if (!strokeProcessor) return
|
||||
|
||||
// Process point to generate equidistant points
|
||||
@@ -964,7 +955,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
strokeProcessor = new StrokeProcessor(targetSpacing)
|
||||
|
||||
// Process first point
|
||||
await drawWithBetterSmoothing(coords_canvas)
|
||||
drawWithBetterSmoothing(coords_canvas)
|
||||
|
||||
smoothingLastDrawTime.value = new Date()
|
||||
} catch (error) {
|
||||
@@ -986,18 +977,17 @@ export function useBrushDrawing(initialSettings?: {
|
||||
const currentTool = store.currentTool
|
||||
|
||||
if (diff > 20 && !isDrawing.value) {
|
||||
requestAnimationFrame(async () => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!isDrawing.value) return // Fix: Prevent race condition
|
||||
try {
|
||||
initShape(CompositionOperation.SourceOver)
|
||||
await gpuDrawPoint(coords_canvas)
|
||||
// smoothingCordsArray.value.push(coords_canvas) // Removed in favor of StrokeProcessor
|
||||
gpuDrawPoint(coords_canvas)
|
||||
} catch (error) {
|
||||
console.error('[useBrushDrawing] Drawing error:', error)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
requestAnimationFrame(async () => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!isDrawing.value) return // Fix: Prevent race condition
|
||||
try {
|
||||
if (currentTool === 'eraser' || event.buttons === 2) {
|
||||
@@ -1005,7 +995,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
} else {
|
||||
initShape(CompositionOperation.SourceOver)
|
||||
}
|
||||
await drawWithBetterSmoothing(coords_canvas)
|
||||
drawWithBetterSmoothing(coords_canvas)
|
||||
} catch (error) {
|
||||
console.error('[useBrushDrawing] Drawing error:', error)
|
||||
}
|
||||
@@ -1118,7 +1108,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
* Starts the brush adjustment interaction.
|
||||
* @param event - The pointer event.
|
||||
*/
|
||||
async function startBrushAdjustment(event: PointerEvent): Promise<void> {
|
||||
function startBrushAdjustment(event: PointerEvent): void {
|
||||
event.preventDefault()
|
||||
|
||||
const coords = { x: event.offsetX, y: event.offsetY }
|
||||
@@ -1132,7 +1122,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
* Handles the brush adjustment movement.
|
||||
* @param event - The pointer event.
|
||||
*/
|
||||
async function handleBrushAdjustment(event: PointerEvent): Promise<void> {
|
||||
function handleBrushAdjustment(event: PointerEvent): void {
|
||||
if (!initialPoint.value) {
|
||||
return
|
||||
}
|
||||
@@ -1366,7 +1356,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
* @param point - The point to draw.
|
||||
* @param opacity - The opacity of the point.
|
||||
*/
|
||||
async function gpuDrawPoint(point: Point, opacity: number = 1) {
|
||||
function gpuDrawPoint(point: Point, opacity: number = 1) {
|
||||
if (renderer) {
|
||||
const width = store.maskCanvas!.width
|
||||
const height = store.maskCanvas!.height
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { useCanvasHistory } from '@/composables/maskeditor/useCanvasHistory'
|
||||
|
||||
// Define the store shape to avoid 'any' and cast to the expected type
|
||||
interface MaskEditorStoreState {
|
||||
maskCanvas: HTMLCanvasElement | null
|
||||
rgbCanvas: HTMLCanvasElement | null
|
||||
imgCanvas: HTMLCanvasElement | null
|
||||
maskCtx: CanvasRenderingContext2D | null
|
||||
rgbCtx: CanvasRenderingContext2D | null
|
||||
imgCtx: CanvasRenderingContext2D | null
|
||||
}
|
||||
import type { CanvasLayers } from '@/composables/maskeditor/useCanvasHistory'
|
||||
import { useCanvasHistory } from '@/composables/maskeditor/useCanvasHistory'
|
||||
|
||||
// Use vi.hoisted to create isolated mock state container
|
||||
const mockRefs = vi.hoisted(() => ({
|
||||
@@ -22,49 +14,27 @@ const mockRefs = vi.hoisted(() => ({
|
||||
imgCtx: null as CanvasRenderingContext2D | null
|
||||
}))
|
||||
|
||||
const mockStore: MaskEditorStoreState = {
|
||||
const mockLayers: CanvasLayers = {
|
||||
get maskCanvas() {
|
||||
return mockRefs.maskCanvas
|
||||
},
|
||||
set maskCanvas(val) {
|
||||
mockRefs.maskCanvas = val
|
||||
},
|
||||
get rgbCanvas() {
|
||||
return mockRefs.rgbCanvas
|
||||
},
|
||||
set rgbCanvas(val) {
|
||||
mockRefs.rgbCanvas = val
|
||||
},
|
||||
get imgCanvas() {
|
||||
return mockRefs.imgCanvas
|
||||
},
|
||||
set imgCanvas(val) {
|
||||
mockRefs.imgCanvas = val
|
||||
},
|
||||
get maskCtx() {
|
||||
return mockRefs.maskCtx
|
||||
},
|
||||
set maskCtx(val) {
|
||||
mockRefs.maskCtx = val
|
||||
},
|
||||
get rgbCtx() {
|
||||
return mockRefs.rgbCtx
|
||||
},
|
||||
set rgbCtx(val) {
|
||||
mockRefs.rgbCtx = val
|
||||
},
|
||||
get imgCtx() {
|
||||
return mockRefs.imgCtx
|
||||
},
|
||||
set imgCtx(val) {
|
||||
mockRefs.imgCtx = val
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('@/stores/maskEditorStore', () => ({
|
||||
useMaskEditorStore: vi.fn(() => mockStore)
|
||||
}))
|
||||
|
||||
// Mock ImageBitmap using safe global augmentation pattern
|
||||
if (typeof globalThis.ImageBitmap === 'undefined') {
|
||||
globalThis.ImageBitmap = class ImageBitmap {
|
||||
@@ -140,14 +110,14 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with default values', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
expect(history.canUndo.value).toBe(false)
|
||||
expect(history.canRedo.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should save initial state', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
@@ -173,7 +143,7 @@ describe('useCanvasHistory', () => {
|
||||
height: 0
|
||||
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
|
||||
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
history.saveInitialState()
|
||||
|
||||
expect(rafSpy).toHaveBeenCalled()
|
||||
@@ -189,7 +159,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
mockRefs.maskCtx = null
|
||||
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
history.saveInitialState()
|
||||
|
||||
expect(rafSpy).toHaveBeenCalled()
|
||||
@@ -213,7 +183,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('saveState', () => {
|
||||
it('should save a new state', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
vi.mocked(mockRefs.maskCtx!.getImageData).mockClear()
|
||||
@@ -234,7 +204,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should clear redo states when saving new state', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -249,7 +219,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should respect maxStates limit', () => {
|
||||
const history = useCanvasHistory(3)
|
||||
const history = useCanvasHistory(mockLayers, 3)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -269,7 +239,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should call saveInitialState if not initialized', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveState()
|
||||
|
||||
@@ -279,7 +249,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should not save state if context is missing', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
@@ -299,7 +269,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('undo', () => {
|
||||
it('should undo to previous state', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -314,7 +284,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should not undo when no undo states available', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
@@ -330,7 +300,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should undo multiple times', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -348,7 +318,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should not undo beyond first state', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -369,7 +339,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('redo', () => {
|
||||
it('should redo to next state', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -389,7 +359,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should not redo when no redo states available', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
@@ -405,7 +375,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should redo multiple times', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -427,7 +397,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should not redo beyond last state', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -449,7 +419,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('clearStates', () => {
|
||||
it('should clear all states', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -462,7 +432,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should allow saving initial state after clear', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.clearStates()
|
||||
@@ -481,13 +451,13 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('canUndo computed', () => {
|
||||
it('should be false with no states', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
expect(history.canUndo.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should be false with only initial state', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
@@ -495,7 +465,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should be true after saving a state', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -504,7 +474,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should be false after undoing to first state', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -516,7 +486,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('canRedo computed', () => {
|
||||
it('should be false with no undo', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -525,7 +495,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should be true after undo', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -535,7 +505,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should be false after redo to last state', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -546,7 +516,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should be false after saving new state', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -562,7 +532,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('restoreState', () => {
|
||||
it('should not restore if context is missing', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -583,7 +553,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle rapid state saves', async () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
@@ -596,7 +566,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should handle maxStates of 1', () => {
|
||||
const history = useCanvasHistory(1)
|
||||
const history = useCanvasHistory(mockLayers, 1)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -605,7 +575,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should handle undo/redo cycling', () => {
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -629,7 +599,7 @@ describe('useCanvasHistory', () => {
|
||||
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
|
||||
}
|
||||
|
||||
const history = useCanvasHistory()
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
||||
|
||||
// Define the state interface for better readability
|
||||
interface CanvasState {
|
||||
mask: ImageData | ImageBitmap
|
||||
rgb: ImageData | ImageBitmap
|
||||
img: ImageData | ImageBitmap
|
||||
}
|
||||
|
||||
export function useCanvasHistory(maxStates = 20) {
|
||||
const store = useMaskEditorStore()
|
||||
export interface CanvasLayers {
|
||||
maskCanvas: HTMLCanvasElement | null
|
||||
maskCtx: CanvasRenderingContext2D | null
|
||||
rgbCanvas: HTMLCanvasElement | null
|
||||
rgbCtx: CanvasRenderingContext2D | null
|
||||
imgCanvas: HTMLCanvasElement | null
|
||||
imgCtx: CanvasRenderingContext2D | null
|
||||
}
|
||||
|
||||
export function useCanvasHistory(layers: CanvasLayers, maxStates = 20) {
|
||||
const states = ref<CanvasState[]>([])
|
||||
const currentStateIndex = ref(-1)
|
||||
const initialized = ref(false)
|
||||
@@ -27,7 +32,7 @@ export function useCanvasHistory(maxStates = 20) {
|
||||
})
|
||||
|
||||
const saveInitialState = () => {
|
||||
const { maskCtx, rgbCtx, imgCtx, maskCanvas, rgbCanvas, imgCanvas } = store
|
||||
const { maskCtx, rgbCtx, imgCtx, maskCanvas, rgbCanvas, imgCanvas } = layers
|
||||
|
||||
// Ensure all 3 contexts and canvases are ready
|
||||
if (
|
||||
@@ -79,7 +84,7 @@ export function useCanvasHistory(maxStates = 20) {
|
||||
providedRgbData?: ImageData | ImageBitmap,
|
||||
providedImgData?: ImageData | ImageBitmap
|
||||
) => {
|
||||
const { maskCtx, rgbCtx, imgCtx, maskCanvas, rgbCanvas, imgCanvas } = store
|
||||
const { maskCtx, rgbCtx, imgCtx, maskCanvas, rgbCanvas, imgCanvas } = layers
|
||||
|
||||
if (
|
||||
!maskCtx ||
|
||||
@@ -144,7 +149,7 @@ export function useCanvasHistory(maxStates = 20) {
|
||||
}
|
||||
|
||||
const restoreState = (state: CanvasState) => {
|
||||
const { maskCtx, rgbCtx, imgCtx, maskCanvas, rgbCanvas, imgCanvas } = store
|
||||
const { maskCtx, rgbCtx, imgCtx, maskCanvas, rgbCanvas, imgCanvas } = layers
|
||||
if (
|
||||
!maskCtx ||
|
||||
!rgbCtx ||
|
||||
@@ -169,13 +174,13 @@ export function useCanvasHistory(maxStates = 20) {
|
||||
imgCanvas.height = newHeight
|
||||
}
|
||||
|
||||
const layers = [
|
||||
const canvasLayers = [
|
||||
{ ctx: maskCtx, data: state.mask },
|
||||
{ ctx: rgbCtx, data: state.rgb },
|
||||
{ ctx: imgCtx, data: state.img }
|
||||
]
|
||||
|
||||
layers.forEach(({ ctx, data }) => {
|
||||
canvasLayers.forEach(({ ctx, data }) => {
|
||||
if (data instanceof ImageBitmap) {
|
||||
ctx.clearRect(0, 0, data.width, data.height)
|
||||
ctx.drawImage(data, 0, 0)
|
||||
|
||||
@@ -89,13 +89,13 @@ describe('useCanvasManager', () => {
|
||||
})
|
||||
|
||||
describe('invalidateCanvas', () => {
|
||||
it('should set canvas dimensions', async () => {
|
||||
it('should set canvas dimensions', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
expect(mockStore.imgCanvas.width).toBe(512)
|
||||
expect(mockStore.imgCanvas.height).toBe(512)
|
||||
@@ -105,13 +105,13 @@ describe('useCanvasManager', () => {
|
||||
expect(mockStore.rgbCanvas.height).toBe(512)
|
||||
})
|
||||
|
||||
it('should draw original image', async () => {
|
||||
it('should draw original image', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
expect(mockStore.imgCtx.drawImage).toHaveBeenCalledWith(
|
||||
origImage,
|
||||
@@ -122,14 +122,14 @@ describe('useCanvasManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should draw paint image when provided', async () => {
|
||||
it('should draw paint image when provided', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
const paintImage = createMockImage(512, 512)
|
||||
|
||||
await manager.invalidateCanvas(origImage, maskImage, paintImage)
|
||||
manager.invalidateCanvas(origImage, maskImage, paintImage)
|
||||
|
||||
expect(mockStore.rgbCtx.drawImage).toHaveBeenCalledWith(
|
||||
paintImage,
|
||||
@@ -140,31 +140,31 @@ describe('useCanvasManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should not draw paint image when null', async () => {
|
||||
it('should not draw paint image when null', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
expect(mockStore.rgbCtx.drawImage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should prepare mask', async () => {
|
||||
it('should prepare mask', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
expect(mockStore.maskCtx.drawImage).toHaveBeenCalled()
|
||||
expect(mockStore.maskCtx.getImageData).toHaveBeenCalled()
|
||||
expect(mockStore.maskCtx.putImageData).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should throw error when canvas missing', async () => {
|
||||
it('should throw error when canvas missing', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.imgCanvas = null! as HTMLCanvasElement
|
||||
@@ -172,12 +172,12 @@ describe('useCanvasManager', () => {
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
|
||||
await expect(
|
||||
expect(() =>
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
).rejects.toThrow('Canvas elements or contexts not available')
|
||||
).toThrow('Canvas elements or contexts not available')
|
||||
})
|
||||
|
||||
it('should throw error when context missing', async () => {
|
||||
it('should throw error when context missing', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.imgCtx = null! as CanvasRenderingContext2D
|
||||
@@ -185,20 +185,20 @@ describe('useCanvasManager', () => {
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
|
||||
await expect(
|
||||
expect(() =>
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
).rejects.toThrow('Canvas elements or contexts not available')
|
||||
).toThrow('Canvas elements or contexts not available')
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateMaskColor', () => {
|
||||
it('should update mask color for black blend mode', async () => {
|
||||
it('should update mask color for black blend mode', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskBlendMode = MaskBlendMode.Black
|
||||
mockStore.maskColor = { r: 0, g: 0, b: 0 }
|
||||
|
||||
await manager.updateMaskColor()
|
||||
manager.updateMaskColor()
|
||||
|
||||
expect(mockStore.maskCtx.fillStyle).toBe('rgb(0, 0, 0)')
|
||||
expect(mockStore.maskCanvas.style.mixBlendMode).toBe('initial')
|
||||
@@ -208,13 +208,13 @@ describe('useCanvasManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should update mask color for white blend mode', async () => {
|
||||
it('should update mask color for white blend mode', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskBlendMode = MaskBlendMode.White
|
||||
mockStore.maskColor = { r: 255, g: 255, b: 255 }
|
||||
|
||||
await manager.updateMaskColor()
|
||||
manager.updateMaskColor()
|
||||
|
||||
expect(mockStore.maskCtx.fillStyle).toBe('rgb(255, 255, 255)')
|
||||
expect(mockStore.maskCanvas.style.mixBlendMode).toBe('initial')
|
||||
@@ -223,13 +223,13 @@ describe('useCanvasManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should update mask color for negative blend mode', async () => {
|
||||
it('should update mask color for negative blend mode', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskBlendMode = MaskBlendMode.Negative
|
||||
mockStore.maskColor = { r: 255, g: 255, b: 255 }
|
||||
|
||||
await manager.updateMaskColor()
|
||||
manager.updateMaskColor()
|
||||
|
||||
expect(mockStore.maskCanvas.style.mixBlendMode).toBe('difference')
|
||||
expect(mockStore.maskCanvas.style.opacity).toBe('1')
|
||||
@@ -238,14 +238,14 @@ describe('useCanvasManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should update all pixels with mask color', async () => {
|
||||
it('should update all pixels with mask color', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskColor = { r: 128, g: 64, b: 32 }
|
||||
mockStore.maskCanvas.width = 100
|
||||
mockStore.maskCanvas.height = 100
|
||||
|
||||
await manager.updateMaskColor()
|
||||
manager.updateMaskColor()
|
||||
|
||||
for (let i = 0; i < mockImageData.data.length; i += 4) {
|
||||
expect(mockImageData.data[i]).toBe(128)
|
||||
@@ -260,39 +260,39 @@ describe('useCanvasManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should return early when canvas missing', async () => {
|
||||
it('should return early when canvas missing', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskCanvas = null! as HTMLCanvasElement
|
||||
|
||||
await manager.updateMaskColor()
|
||||
manager.updateMaskColor()
|
||||
|
||||
expect(mockStore.maskCtx.getImageData).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return early when context missing', async () => {
|
||||
it('should return early when context missing', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskCtx = null! as CanvasRenderingContext2D
|
||||
|
||||
await manager.updateMaskColor()
|
||||
manager.updateMaskColor()
|
||||
|
||||
expect(mockStore.canvasBackground.style.backgroundColor).toBe('')
|
||||
})
|
||||
|
||||
it('should handle different opacity values', async () => {
|
||||
it('should handle different opacity values', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskOpacity = 0.5
|
||||
|
||||
await manager.updateMaskColor()
|
||||
manager.updateMaskColor()
|
||||
|
||||
expect(mockStore.maskCanvas.style.opacity).toBe('0.5')
|
||||
})
|
||||
})
|
||||
|
||||
describe('prepareMask', () => {
|
||||
it('should invert mask alpha', async () => {
|
||||
it('should invert mask alpha', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
for (let i = 0; i < mockImageData.data.length; i += 4) {
|
||||
@@ -302,14 +302,14 @@ describe('useCanvasManager', () => {
|
||||
const origImage = createMockImage(100, 100)
|
||||
const maskImage = createMockImage(100, 100)
|
||||
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
for (let i = 0; i < mockImageData.data.length; i += 4) {
|
||||
expect(mockImageData.data[i + 3]).toBe(127)
|
||||
}
|
||||
})
|
||||
|
||||
it('should apply mask color to all pixels', async () => {
|
||||
it('should apply mask color to all pixels', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskColor = { r: 100, g: 150, b: 200 }
|
||||
@@ -317,7 +317,7 @@ describe('useCanvasManager', () => {
|
||||
const origImage = createMockImage(100, 100)
|
||||
const maskImage = createMockImage(100, 100)
|
||||
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
for (let i = 0; i < mockImageData.data.length; i += 4) {
|
||||
expect(mockImageData.data[i]).toBe(100)
|
||||
@@ -326,13 +326,13 @@ describe('useCanvasManager', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('should set composite operation', async () => {
|
||||
it('should set composite operation', () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
const origImage = createMockImage(100, 100)
|
||||
const maskImage = createMockImage(100, 100)
|
||||
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
expect(mockStore.maskCtx.globalCompositeOperation).toBe('source-over')
|
||||
})
|
||||
|
||||
@@ -5,11 +5,11 @@ import { MaskBlendMode } from '@/extensions/core/maskeditor/types'
|
||||
export function useCanvasManager() {
|
||||
const store = useMaskEditorStore()
|
||||
|
||||
const prepareMask = async (
|
||||
const prepareMask = (
|
||||
image: HTMLImageElement,
|
||||
maskCanvasEl: HTMLCanvasElement,
|
||||
maskContext: CanvasRenderingContext2D
|
||||
): Promise<void> => {
|
||||
): void => {
|
||||
const maskColor = store.maskColor
|
||||
|
||||
maskContext.drawImage(image, 0, 0, maskCanvasEl.width, maskCanvasEl.height)
|
||||
@@ -33,11 +33,11 @@ export function useCanvasManager() {
|
||||
maskContext.putImageData(maskData, 0, 0)
|
||||
}
|
||||
|
||||
const invalidateCanvas = async (
|
||||
const invalidateCanvas = (
|
||||
origImage: HTMLImageElement,
|
||||
maskImage: HTMLImageElement,
|
||||
paintImage: HTMLImageElement | null
|
||||
): Promise<void> => {
|
||||
): void => {
|
||||
const { imgCanvas, maskCanvas, rgbCanvas, imgCtx, maskCtx, rgbCtx } = store
|
||||
|
||||
if (
|
||||
@@ -64,7 +64,7 @@ export function useCanvasManager() {
|
||||
rgbCtx.drawImage(paintImage, 0, 0, paintImage.width, paintImage.height)
|
||||
}
|
||||
|
||||
await prepareMask(maskImage, maskCanvas, maskCtx)
|
||||
prepareMask(maskImage, maskCanvas, maskCtx)
|
||||
}
|
||||
|
||||
const setCanvasBackground = (): void => {
|
||||
@@ -81,7 +81,7 @@ export function useCanvasManager() {
|
||||
}
|
||||
}
|
||||
|
||||
const updateMaskColor = async (): Promise<void> => {
|
||||
const updateMaskColor = (): void => {
|
||||
const { maskCanvas, maskCtx, maskColor, maskBlendMode, maskOpacity } = store
|
||||
|
||||
if (!maskCanvas || !maskCtx) return
|
||||
|
||||
@@ -118,10 +118,7 @@ export function useCanvasTransform() {
|
||||
* Recreates and updates GPU textures after transformation
|
||||
* This is required because GPU textures have immutable dimensions
|
||||
*/
|
||||
const recreateGPUTextures = async (
|
||||
width: number,
|
||||
height: number
|
||||
): Promise<void> => {
|
||||
const recreateGPUTextures = (width: number, height: number): void => {
|
||||
if (
|
||||
!store.tgpuRoot ||
|
||||
!store.maskCanvas ||
|
||||
@@ -181,7 +178,7 @@ export function useCanvasTransform() {
|
||||
/**
|
||||
* Rotates all canvas layers 90 degrees clockwise and updates GPU
|
||||
*/
|
||||
const rotateClockwise = async (): Promise<void> => {
|
||||
const rotateClockwise = (): void => {
|
||||
const { maskCanvas, maskCtx, rgbCanvas, rgbCtx, imgCanvas, imgCtx } = store
|
||||
|
||||
if (
|
||||
@@ -220,7 +217,7 @@ export function useCanvasTransform() {
|
||||
|
||||
// Recreate GPU textures with new dimensions if GPU is active
|
||||
if (store.tgpuRoot) {
|
||||
await recreateGPUTextures(origHeight, origWidth)
|
||||
recreateGPUTextures(origHeight, origWidth)
|
||||
}
|
||||
|
||||
// Save to history
|
||||
@@ -230,7 +227,7 @@ export function useCanvasTransform() {
|
||||
/**
|
||||
* Rotates all canvas layers 90 degrees counter-clockwise and updates GPU
|
||||
*/
|
||||
const rotateCounterclockwise = async (): Promise<void> => {
|
||||
const rotateCounterclockwise = (): void => {
|
||||
const { maskCanvas, maskCtx, rgbCanvas, rgbCtx, imgCanvas, imgCtx } = store
|
||||
|
||||
if (
|
||||
@@ -269,7 +266,7 @@ export function useCanvasTransform() {
|
||||
|
||||
// Recreate GPU textures with new dimensions if GPU is active
|
||||
if (store.tgpuRoot) {
|
||||
await recreateGPUTextures(origHeight, origWidth)
|
||||
recreateGPUTextures(origHeight, origWidth)
|
||||
}
|
||||
|
||||
// Save to history
|
||||
@@ -279,7 +276,7 @@ export function useCanvasTransform() {
|
||||
/**
|
||||
* Mirrors all canvas layers horizontally and updates GPU
|
||||
*/
|
||||
const mirrorHorizontal = async (): Promise<void> => {
|
||||
const mirrorHorizontal = (): void => {
|
||||
const { maskCanvas, maskCtx, rgbCanvas, rgbCtx, imgCanvas, imgCtx } = store
|
||||
|
||||
if (
|
||||
@@ -306,7 +303,7 @@ export function useCanvasTransform() {
|
||||
|
||||
// Update GPU textures if GPU is active (dimensions unchanged, just data)
|
||||
if (store.tgpuRoot) {
|
||||
await recreateGPUTextures(maskCanvas.width, maskCanvas.height)
|
||||
recreateGPUTextures(maskCanvas.width, maskCanvas.height)
|
||||
}
|
||||
|
||||
// Save to history
|
||||
@@ -316,7 +313,7 @@ export function useCanvasTransform() {
|
||||
/**
|
||||
* Mirrors all canvas layers vertically and updates GPU
|
||||
*/
|
||||
const mirrorVertical = async (): Promise<void> => {
|
||||
const mirrorVertical = (): void => {
|
||||
const { maskCanvas, maskCtx, rgbCanvas, rgbCtx, imgCanvas, imgCtx } = store
|
||||
|
||||
if (
|
||||
@@ -343,7 +340,7 @@ export function useCanvasTransform() {
|
||||
|
||||
// Update GPU textures if GPU is active (dimensions unchanged, just data)
|
||||
if (store.tgpuRoot) {
|
||||
await recreateGPUTextures(maskCanvas.width, maskCanvas.height)
|
||||
recreateGPUTextures(maskCanvas.width, maskCanvas.height)
|
||||
}
|
||||
|
||||
// Save to history
|
||||
|
||||
@@ -35,13 +35,9 @@ function useImageLoaderInternal() {
|
||||
|
||||
store.image = baseImage
|
||||
|
||||
await canvasManager.invalidateCanvas(
|
||||
baseImage,
|
||||
maskImage,
|
||||
paintImage || null
|
||||
)
|
||||
canvasManager.invalidateCanvas(baseImage, maskImage, paintImage || null)
|
||||
|
||||
await canvasManager.updateMaskColor()
|
||||
canvasManager.updateMaskColor()
|
||||
|
||||
return baseImage
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
// Private layer filename functions
|
||||
interface ImageLayerFilenames {
|
||||
maskedImage: string
|
||||
paint: string
|
||||
@@ -30,6 +29,32 @@ function imageLayerFilenamesByTimestamp(
|
||||
}
|
||||
}
|
||||
|
||||
function getContext2D(canvas: HTMLCanvasElement): CanvasRenderingContext2D {
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Failed to get 2D rendering context')
|
||||
return ctx
|
||||
}
|
||||
|
||||
function applyInvertedMaskAlpha(
|
||||
targetCtx: CanvasRenderingContext2D,
|
||||
maskCanvas: HTMLCanvasElement
|
||||
): void {
|
||||
const maskCtx = getContext2D(maskCanvas)
|
||||
const maskData = maskCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
maskCanvas.width,
|
||||
maskCanvas.height
|
||||
)
|
||||
|
||||
const { width, height } = targetCtx.canvas
|
||||
const imageData = targetCtx.getImageData(0, 0, width, height)
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
imageData.data[i + 3] = 255 - maskData.data[i + 3]
|
||||
}
|
||||
targetCtx.putImageData(imageData, 0, 0)
|
||||
}
|
||||
|
||||
export function useMaskEditorSaver() {
|
||||
const dataStore = useMaskEditorDataStore()
|
||||
const editorStore = useMaskEditorStore()
|
||||
@@ -99,31 +124,10 @@ export function useMaskEditorSaver() {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = imgCanvas.width
|
||||
canvas.height = imgCanvas.height
|
||||
const ctx = canvas.getContext('2d')!
|
||||
const ctx = getContext2D(canvas)
|
||||
|
||||
ctx.drawImage(imgCanvas, 0, 0)
|
||||
|
||||
const maskCtx = maskCanvas.getContext('2d')!
|
||||
const maskData = maskCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
maskCanvas.width,
|
||||
maskCanvas.height
|
||||
)
|
||||
|
||||
const refinedMaskData = new Uint8ClampedArray(maskData.data.length)
|
||||
for (let i = 0; i < maskData.data.length; i += 4) {
|
||||
refinedMaskData[i] = 0
|
||||
refinedMaskData[i + 1] = 0
|
||||
refinedMaskData[i + 2] = 0
|
||||
refinedMaskData[i + 3] = 255 - maskData.data[i + 3]
|
||||
}
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
imageData.data[i + 3] = refinedMaskData[i + 3]
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
applyInvertedMaskAlpha(ctx, maskCanvas)
|
||||
|
||||
const blob = await canvasToBlob(canvas)
|
||||
const ref = createFileRef(filename)
|
||||
@@ -150,11 +154,9 @@ export function useMaskEditorSaver() {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = imgCanvas.width
|
||||
canvas.height = imgCanvas.height
|
||||
const ctx = canvas.getContext('2d')!
|
||||
const ctx = getContext2D(canvas)
|
||||
|
||||
ctx.drawImage(imgCanvas, 0, 0)
|
||||
|
||||
ctx.globalCompositeOperation = 'source-over'
|
||||
ctx.drawImage(paintCanvas, 0, 0)
|
||||
|
||||
const blob = await canvasToBlob(canvas)
|
||||
@@ -172,34 +174,11 @@ export function useMaskEditorSaver() {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = imgCanvas.width
|
||||
canvas.height = imgCanvas.height
|
||||
const ctx = canvas.getContext('2d')!
|
||||
const ctx = getContext2D(canvas)
|
||||
|
||||
ctx.drawImage(imgCanvas, 0, 0)
|
||||
|
||||
ctx.globalCompositeOperation = 'source-over'
|
||||
ctx.drawImage(paintCanvas, 0, 0)
|
||||
|
||||
const maskCtx = maskCanvas.getContext('2d')!
|
||||
const maskData = maskCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
maskCanvas.width,
|
||||
maskCanvas.height
|
||||
)
|
||||
|
||||
const refinedMaskData = new Uint8ClampedArray(maskData.data.length)
|
||||
for (let i = 0; i < maskData.data.length; i += 4) {
|
||||
refinedMaskData[i] = 0
|
||||
refinedMaskData[i + 1] = 0
|
||||
refinedMaskData[i + 2] = 0
|
||||
refinedMaskData[i + 3] = 255 - maskData.data[i + 3]
|
||||
}
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
imageData.data[i + 3] = refinedMaskData[i + 3]
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
applyInvertedMaskAlpha(ctx, maskCanvas)
|
||||
|
||||
const blob = await canvasToBlob(canvas)
|
||||
const ref = createFileRef(filename)
|
||||
@@ -210,16 +189,26 @@ export function useMaskEditorSaver() {
|
||||
async function uploadAllLayers(outputData: EditorOutputData): Promise<void> {
|
||||
const sourceRef = dataStore.inputData!.sourceRef
|
||||
|
||||
const actualMaskedRef = await uploadMask(outputData.maskedImage, sourceRef)
|
||||
const actualPaintRef = await uploadImage(outputData.paintLayer, sourceRef)
|
||||
const actualPaintedRef = await uploadImage(
|
||||
const actualMaskedRef = await uploadLayer(
|
||||
outputData.maskedImage,
|
||||
sourceRef,
|
||||
'/upload/mask'
|
||||
)
|
||||
const actualPaintRef = await uploadLayer(
|
||||
outputData.paintLayer,
|
||||
sourceRef,
|
||||
'/upload/image'
|
||||
)
|
||||
const actualPaintedRef = await uploadLayer(
|
||||
outputData.paintedImage,
|
||||
sourceRef
|
||||
sourceRef,
|
||||
'/upload/image'
|
||||
)
|
||||
|
||||
const actualPaintedMaskedRef = await uploadMask(
|
||||
const actualPaintedMaskedRef = await uploadLayer(
|
||||
outputData.paintedMaskedImage,
|
||||
actualPaintedRef
|
||||
actualPaintedRef,
|
||||
'/upload/mask'
|
||||
)
|
||||
|
||||
outputData.maskedImage.ref = actualMaskedRef
|
||||
@@ -228,9 +217,10 @@ export function useMaskEditorSaver() {
|
||||
outputData.paintedMaskedImage.ref = actualPaintedMaskedRef
|
||||
}
|
||||
|
||||
async function uploadMask(
|
||||
async function uploadLayer(
|
||||
layer: EditorOutputLayer,
|
||||
originalRef: ImageRef
|
||||
originalRef: ImageRef,
|
||||
endpoint: '/upload/mask' | '/upload/image'
|
||||
): Promise<ImageRef> {
|
||||
const formData = new FormData()
|
||||
formData.append('image', layer.blob, layer.ref.filename)
|
||||
@@ -238,61 +228,22 @@ export function useMaskEditorSaver() {
|
||||
formData.append('type', 'input')
|
||||
formData.append('subfolder', 'clipspace')
|
||||
|
||||
const response = await api.fetchApi('/upload/mask', {
|
||||
const response = await api.fetchApi(endpoint, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload mask: ${layer.ref.filename}`)
|
||||
throw new Error(`Failed to upload to ${endpoint}: ${layer.ref.filename}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await response.json()
|
||||
if (data?.name) {
|
||||
return {
|
||||
filename: data.name,
|
||||
subfolder: data.subfolder || layer.ref.subfolder,
|
||||
type: data.type || layer.ref.type
|
||||
}
|
||||
const data = await response.json()
|
||||
if (data?.name) {
|
||||
return {
|
||||
filename: data.name,
|
||||
subfolder: data.subfolder || layer.ref.subfolder,
|
||||
type: data.type || layer.ref.type
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[MaskEditorSaver] Failed to parse upload response:', error)
|
||||
}
|
||||
|
||||
return layer.ref
|
||||
}
|
||||
|
||||
async function uploadImage(
|
||||
layer: EditorOutputLayer,
|
||||
originalRef: ImageRef
|
||||
): Promise<ImageRef> {
|
||||
const formData = new FormData()
|
||||
formData.append('image', layer.blob, layer.ref.filename)
|
||||
formData.append('original_ref', JSON.stringify(originalRef))
|
||||
formData.append('type', 'input')
|
||||
formData.append('subfolder', 'clipspace')
|
||||
|
||||
const response = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload image: ${layer.ref.filename}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await response.json()
|
||||
if (data?.name) {
|
||||
return {
|
||||
filename: data.name,
|
||||
subfolder: data.subfolder || layer.ref.subfolder,
|
||||
type: data.type || layer.ref.type
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[MaskEditorSaver] Failed to parse upload response:', error)
|
||||
}
|
||||
|
||||
return layer.ref
|
||||
@@ -373,7 +324,7 @@ export function useMaskEditorSaver() {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = source.width
|
||||
canvas.height = source.height
|
||||
const ctx = canvas.getContext('2d')!
|
||||
const ctx = getContext2D(canvas)
|
||||
ctx.drawImage(source, 0, 0)
|
||||
return canvas
|
||||
}
|
||||
|
||||
@@ -59,14 +59,9 @@ export function usePanAndZoom() {
|
||||
store.canvasHistory.undo()
|
||||
}
|
||||
|
||||
const invalidatePanZoom = async (): Promise<void> => {
|
||||
const invalidatePanZoom = (): void => {
|
||||
// Single validation check upfront
|
||||
if (
|
||||
!image.value?.width ||
|
||||
!image.value?.height ||
|
||||
!pan_offset.value ||
|
||||
!zoom_ratio.value
|
||||
) {
|
||||
if (!image.value?.width || !image.value?.height || !zoom_ratio.value) {
|
||||
console.warn('Missing required properties for pan/zoom')
|
||||
return
|
||||
}
|
||||
@@ -113,7 +108,7 @@ export function usePanAndZoom() {
|
||||
initialPan.value = { ...pan_offset.value }
|
||||
}
|
||||
|
||||
const handlePanMove = async (event: PointerEvent): Promise<void> => {
|
||||
const handlePanMove = (event: PointerEvent): void => {
|
||||
if (mouseDownPoint.value === null) {
|
||||
throw new Error('mouseDownPoint is null')
|
||||
}
|
||||
@@ -126,10 +121,10 @@ export function usePanAndZoom() {
|
||||
|
||||
pan_offset.value = { x: pan_x, y: pan_y }
|
||||
|
||||
await invalidatePanZoom()
|
||||
invalidatePanZoom()
|
||||
}
|
||||
|
||||
const handleSingleTouchPan = async (touch: Touch): Promise<void> => {
|
||||
const handleSingleTouchPan = (touch: Touch): void => {
|
||||
if (lastTouchPoint.value === null) {
|
||||
lastTouchPoint.value = { x: touch.clientX, y: touch.clientY }
|
||||
return
|
||||
@@ -141,7 +136,7 @@ export function usePanAndZoom() {
|
||||
pan_offset.value.x += deltaX
|
||||
pan_offset.value.y += deltaY
|
||||
|
||||
await invalidatePanZoom()
|
||||
invalidatePanZoom()
|
||||
|
||||
lastTouchPoint.value = { x: touch.clientX, y: touch.clientY }
|
||||
}
|
||||
@@ -175,7 +170,7 @@ export function usePanAndZoom() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleTouchMove = async (event: TouchEvent): Promise<void> => {
|
||||
const handleTouchMove = (event: TouchEvent): void => {
|
||||
event.preventDefault()
|
||||
|
||||
if (penPointerIdList.value.length > 0) return
|
||||
@@ -215,11 +210,11 @@ export function usePanAndZoom() {
|
||||
pan_offset.value.x += touchX - touchX * scaleFactor
|
||||
pan_offset.value.y += touchY - touchY * scaleFactor
|
||||
|
||||
await invalidatePanZoom()
|
||||
invalidatePanZoom()
|
||||
lastTouchZoomDistance.value = newDistance
|
||||
lastTouchMidPoint.value = midpoint
|
||||
} else if (event.touches.length === 1) {
|
||||
await handleSingleTouchPan(event.touches[0])
|
||||
handleSingleTouchPan(event.touches[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +234,7 @@ export function usePanAndZoom() {
|
||||
}
|
||||
}
|
||||
|
||||
const zoom = async (event: WheelEvent): Promise<void> => {
|
||||
const zoom = (event: WheelEvent): void => {
|
||||
const cursorPosition = { x: event.clientX, y: event.clientY }
|
||||
|
||||
const oldZoom = zoom_ratio.value
|
||||
@@ -263,7 +258,7 @@ export function usePanAndZoom() {
|
||||
pan_offset.value.x += mouseX - mouseX * scaleFactor
|
||||
pan_offset.value.y += mouseY - mouseY * scaleFactor
|
||||
|
||||
await invalidatePanZoom()
|
||||
invalidatePanZoom()
|
||||
|
||||
const newImageWidth = maskCanvas.value.clientWidth
|
||||
|
||||
@@ -287,7 +282,7 @@ export function usePanAndZoom() {
|
||||
return { sidePanelWidth, toolPanelWidth }
|
||||
}
|
||||
|
||||
const smoothResetView = async (duration: number = 500): Promise<void> => {
|
||||
const smoothResetView = (duration: number = 500): void => {
|
||||
if (!image.value || !rootElement.value) return
|
||||
|
||||
const startZoom = zoom_ratio.value
|
||||
@@ -321,7 +316,7 @@ export function usePanAndZoom() {
|
||||
}
|
||||
|
||||
const startTime = performance.now()
|
||||
const animate = async (currentTime: number) => {
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const eased = 1 - Math.pow(1 - progress, 3)
|
||||
@@ -330,7 +325,7 @@ export function usePanAndZoom() {
|
||||
pan_offset.value.x = startPan.x + (targetPan.x - startPan.x) * eased
|
||||
pan_offset.value.y = startPan.y + (targetPan.y - startPan.y) * eased
|
||||
|
||||
await invalidatePanZoom()
|
||||
invalidatePanZoom()
|
||||
|
||||
const interpolatedRatio = startZoom + (1.0 - startZoom) * eased
|
||||
store.displayZoomRatio = interpolatedRatio
|
||||
@@ -344,12 +339,12 @@ export function usePanAndZoom() {
|
||||
interpolatedZoomRatio.value = 1.0
|
||||
}
|
||||
|
||||
const initializeCanvasPanZoom = async (
|
||||
const initializeCanvasPanZoom = (
|
||||
img: HTMLImageElement,
|
||||
root: HTMLElement,
|
||||
toolPanel?: HTMLElement | null,
|
||||
sidePanel?: HTMLElement | null
|
||||
): Promise<void> => {
|
||||
): void => {
|
||||
rootElement.value = root
|
||||
toolPanelElement.value = toolPanel || null
|
||||
sidePanelElement.value = sidePanel || null
|
||||
@@ -391,14 +386,14 @@ export function usePanAndZoom() {
|
||||
|
||||
penPointerIdList.value = []
|
||||
|
||||
await invalidatePanZoom()
|
||||
invalidatePanZoom()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => store.resetZoomTrigger,
|
||||
async () => {
|
||||
() => {
|
||||
if (interpolatedZoomRatio.value === 1) return
|
||||
await smoothResetView()
|
||||
smoothResetView()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ export function useToolManager(
|
||||
const isSpacePressed = keyboard.isKeyDown(' ')
|
||||
|
||||
if (event.buttons === 4 || (event.buttons === 1 && isSpacePressed)) {
|
||||
await panZoom.handlePanMove(event)
|
||||
panZoom.handlePanMove(event)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -331,9 +331,6 @@ export const formatPricingResult = (
|
||||
}
|
||||
|
||||
if (!isPricingResult(result)) {
|
||||
if (result !== undefined && result !== null) {
|
||||
console.warn('[pricing/jsonata] invalid result format:', result)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
@@ -46,16 +46,13 @@ export const useComputedWithWidgetWatch = (
|
||||
) => {
|
||||
const { widgetNames, triggerCanvasRedraw = false } = options
|
||||
|
||||
// Create a reactive trigger based on widget values
|
||||
const widgetValues = ref<Record<string, unknown>>({})
|
||||
|
||||
// Initialize widget observers
|
||||
if (node.widgets) {
|
||||
const widgetsToObserve = widgetNames
|
||||
? node.widgets.filter((widget) => widgetNames.includes(widget.name))
|
||||
: node.widgets
|
||||
|
||||
// Initialize current values
|
||||
const currentValues: Record<string, unknown> = {}
|
||||
widgetsToObserve.forEach((widget) => {
|
||||
currentValues[widget.name] = widget.value
|
||||
@@ -64,20 +61,17 @@ export const useComputedWithWidgetWatch = (
|
||||
|
||||
widgetsToObserve.forEach((widget) => {
|
||||
widget.callback = useChainCallback(widget.callback, () => {
|
||||
// Update the reactive widget values
|
||||
widgetValues.value = {
|
||||
...widgetValues.value,
|
||||
[widget.name]: widget.value
|
||||
}
|
||||
|
||||
// Optionally trigger a canvas redraw
|
||||
if (triggerCanvasRedraw) {
|
||||
node.graph?.setDirtyCanvas(true, true)
|
||||
}
|
||||
})
|
||||
})
|
||||
if (widgetNames && widgetNames.length > widgetsToObserve.length) {
|
||||
//Inputs have been included
|
||||
const indexesToObserve = widgetNames
|
||||
.map((name) =>
|
||||
widgetsToObserve.some((w) => w.name == name)
|
||||
@@ -101,8 +95,6 @@ export const useComputedWithWidgetWatch = (
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a function that creates a computed that responds to widget changes.
|
||||
// The computed will be re-evaluated whenever any observed widget changes.
|
||||
return <T>(computeFn: () => T): ComputedRef<T> => {
|
||||
return computedWithControl(widgetValues, computeFn)
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ export type JobGroup = {
|
||||
|
||||
const ADDED_HINT_DURATION_MS = 3000
|
||||
const relativeTimeFormatterCache = new Map<string, Intl.RelativeTimeFormat>()
|
||||
const taskIdToKey = (id: string | number | undefined) => {
|
||||
if (id === null || id === undefined) return null
|
||||
function taskIdToKey(id: string | number | undefined) {
|
||||
if (id === undefined) return null
|
||||
const key = String(id)
|
||||
return key.length ? key : null
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@ vi.mock('@/scripts/api', () => ({
|
||||
|
||||
const downloadBlobMock = vi.fn()
|
||||
vi.mock('@/scripts/utils', () => ({
|
||||
downloadBlob: (filename: string, blob: Blob) =>
|
||||
downloadBlobMock(filename, blob)
|
||||
downloadBlob: (blob: Blob, filename: string) =>
|
||||
downloadBlobMock(blob, filename)
|
||||
}))
|
||||
|
||||
const dialogServiceMock = {
|
||||
@@ -594,7 +594,7 @@ describe('useJobMenu', () => {
|
||||
|
||||
expect(dialogServiceMock.prompt).not.toHaveBeenCalled()
|
||||
expect(downloadBlobMock).toHaveBeenCalledTimes(1)
|
||||
const [filename, blob] = downloadBlobMock.mock.calls[0]
|
||||
const [blob, filename] = downloadBlobMock.mock.calls[0]
|
||||
expect(filename).toBe('Job 7.json')
|
||||
await expect(blob.text()).resolves.toBe(
|
||||
JSON.stringify({ foo: 'bar' }, null, 2)
|
||||
@@ -621,7 +621,7 @@ describe('useJobMenu', () => {
|
||||
message: expect.stringContaining('workflowService.enterFilename'),
|
||||
defaultValue: 'Job job-1.json'
|
||||
})
|
||||
const [filename] = downloadBlobMock.mock.calls[0]
|
||||
const [, filename] = downloadBlobMock.mock.calls[0]
|
||||
expect(filename).toBe('custom-name.json')
|
||||
})
|
||||
|
||||
@@ -642,7 +642,7 @@ describe('useJobMenu', () => {
|
||||
await entry?.onClick?.()
|
||||
|
||||
expect(appendJsonExtMock).toHaveBeenCalledWith('existing.json')
|
||||
const [filename] = downloadBlobMock.mock.calls[0]
|
||||
const [, filename] = downloadBlobMock.mock.calls[0]
|
||||
expect(filename).toBe('existing.json')
|
||||
})
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ export function useJobMenu(
|
||||
|
||||
// This is very magical only because it matches the respective backend implementation
|
||||
// There is or will be a better way to do this
|
||||
const addOutputLoaderNode = async () => {
|
||||
const addOutputLoaderNode = () => {
|
||||
const item = currentMenuItem()
|
||||
if (!item) return
|
||||
const result: ResultItemImpl | undefined = item.taskRef?.previewOutput
|
||||
@@ -200,7 +200,7 @@ export function useJobMenu(
|
||||
|
||||
const json = JSON.stringify(data, null, 2)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
downloadBlob(filename, blob)
|
||||
downloadBlob(blob, filename)
|
||||
}
|
||||
|
||||
const deleteJobAsset = async () => {
|
||||
|
||||
@@ -22,7 +22,7 @@ export const useBrowserTabTitle = () => {
|
||||
: `[${Math.round(executionStore.executionProgress * 100)}%]`
|
||||
)
|
||||
|
||||
const newMenuEnabled = computed(
|
||||
const isMenuBarActive = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
@@ -37,13 +37,12 @@ export const useBrowserTabTitle = () => {
|
||||
() => !!workflowStore.activeWorkflow?.isPersisted
|
||||
)
|
||||
|
||||
const shouldShowUnsavedIndicator = computed(() => {
|
||||
if (workspaceStore.shiftDown) return false
|
||||
if (isAutoSaveEnabled.value) return false
|
||||
if (!isActiveWorkflowPersisted.value) return true
|
||||
if (isActiveWorkflowModified.value) return true
|
||||
return false
|
||||
})
|
||||
const shouldShowUnsavedIndicator = computed(
|
||||
() =>
|
||||
!workspaceStore.shiftDown &&
|
||||
!isAutoSaveEnabled.value &&
|
||||
(!isActiveWorkflowPersisted.value || isActiveWorkflowModified.value)
|
||||
)
|
||||
|
||||
const isUnsavedText = computed(() =>
|
||||
shouldShowUnsavedIndicator.value ? ' *' : ''
|
||||
@@ -87,7 +86,7 @@ export const useBrowserTabTitle = () => {
|
||||
const workflowTitle = computed(
|
||||
() =>
|
||||
executionText.value +
|
||||
(newMenuEnabled.value ? workflowNameText.value : DEFAULT_TITLE)
|
||||
(isMenuBarActive.value ? workflowNameText.value : DEFAULT_TITLE)
|
||||
)
|
||||
|
||||
const title = computed(() => nodeExecutionTitle.value || workflowTitle.value)
|
||||
|
||||
@@ -48,7 +48,7 @@ export function useCachedRequest<TParams, TResult>(
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
// Set cache on error to prevent retrying bad requests
|
||||
console.warn('Cached request failed, caching null result:', err)
|
||||
cache.set(cacheKey, null)
|
||||
return null
|
||||
} finally {
|
||||
@@ -86,11 +86,11 @@ export function useCachedRequest<TParams, TResult>(
|
||||
/**
|
||||
* Cached version of the request function
|
||||
*/
|
||||
const call = async (params: TParams): Promise<TResult | null> => {
|
||||
const call = (params: TParams): Promise<TResult | null> => {
|
||||
const cacheKey = cacheKeyFn(params)
|
||||
|
||||
const cachedResult = cache.get(cacheKey)
|
||||
if (cachedResult !== undefined) return cachedResult
|
||||
if (cachedResult !== undefined) return Promise.resolve(cachedResult)
|
||||
|
||||
const pendingRequest = pendingRequests.get(cacheKey)
|
||||
if (pendingRequest) return handlePendingRequest(pendingRequest)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useSubgraphOperations } from '@/composables/graph/useSubgraphOperations'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { useHelpCommands } from '@/composables/useHelpCommands'
|
||||
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
|
||||
import {
|
||||
DEFAULT_DARK_COLOR_PALETTE,
|
||||
@@ -22,7 +21,6 @@ import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
|
||||
import { createModelNodeFromAsset } from '@/platform/assets/utils/createModelNodeFromAsset'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { buildSupportUrl } from '@/platform/support/config'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { ExecutionTriggerSource } from '@/platform/telemetry/types'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
@@ -83,7 +81,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
const canvasStore = useCanvasStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const telemetry = useTelemetry()
|
||||
const { staticUrls, buildDocsUrl } = useExternalLink()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const bottomPanelStore = useBottomPanelStore()
|
||||
@@ -775,51 +772,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Help.OpenComfyUIIssues',
|
||||
icon: 'pi pi-github',
|
||||
label: 'Open ComfyUI Issues',
|
||||
menubarLabel: 'ComfyUI Issues',
|
||||
versionAdded: '1.5.5',
|
||||
function: () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'github',
|
||||
is_external: true,
|
||||
source: 'menu'
|
||||
})
|
||||
window.open(staticUrls.githubIssues, '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Help.OpenComfyUIDocs',
|
||||
icon: 'pi pi-info-circle',
|
||||
label: 'Open ComfyUI Docs',
|
||||
menubarLabel: 'ComfyUI Docs',
|
||||
versionAdded: '1.5.5',
|
||||
function: () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'docs',
|
||||
is_external: true,
|
||||
source: 'menu'
|
||||
})
|
||||
window.open(buildDocsUrl('/', { includeLocale: true }), '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Help.OpenComfyOrgDiscord',
|
||||
icon: 'pi pi-discord',
|
||||
label: 'Open Comfy-Org Discord',
|
||||
menubarLabel: 'Comfy-Org Discord',
|
||||
versionAdded: '1.5.5',
|
||||
function: () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'discord',
|
||||
is_external: true,
|
||||
source: 'menu'
|
||||
})
|
||||
window.open(staticUrls.discord, '_blank')
|
||||
}
|
||||
},
|
||||
...useHelpCommands(),
|
||||
{
|
||||
id: 'Workspace.SearchBox.Toggle',
|
||||
icon: 'pi pi-search',
|
||||
@@ -829,16 +782,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
useSearchBoxStore().toggleVisible()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Help.AboutComfyUI',
|
||||
icon: 'pi pi-info-circle',
|
||||
label: 'Open About ComfyUI',
|
||||
menubarLabel: 'About ComfyUI',
|
||||
versionAdded: '1.6.4',
|
||||
function: () => {
|
||||
settingsDialog.showAbout()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.DuplicateWorkflow',
|
||||
icon: 'pi pi-clone',
|
||||
@@ -858,35 +801,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
await workflowService.closeWorkflow(workflowStore.activeWorkflow)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ContactSupport',
|
||||
icon: 'pi pi-question',
|
||||
label: 'Contact Support',
|
||||
versionAdded: '1.17.8',
|
||||
function: () => {
|
||||
const { userEmail, resolvedUserInfo } = useCurrentUser()
|
||||
const supportUrl = buildSupportUrl({
|
||||
userEmail: userEmail.value,
|
||||
userId: resolvedUserInfo.value?.id
|
||||
})
|
||||
window.open(supportUrl, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Help.OpenComfyUIForum',
|
||||
icon: 'pi pi-comments',
|
||||
label: 'Open ComfyUI Forum',
|
||||
menubarLabel: 'ComfyUI Forum',
|
||||
versionAdded: '1.8.2',
|
||||
function: () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'help_feedback',
|
||||
is_external: true,
|
||||
source: 'menu'
|
||||
})
|
||||
window.open(staticUrls.forum, '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.CopySelected',
|
||||
icon: 'icon-[lucide--copy]',
|
||||
@@ -1263,7 +1177,29 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
})
|
||||
return
|
||||
}
|
||||
await api.freeMemory({ freeExecutionCache: false })
|
||||
try {
|
||||
const res = await api.freeMemory({ freeExecutionCache: false })
|
||||
if (res.status === 200) {
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: 'Models have been unloaded.',
|
||||
life: 3000
|
||||
})
|
||||
} else {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary:
|
||||
'Unloading of models failed. Installed ComfyUI may be an outdated version.',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: 'An error occurred while trying to unload models.',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1283,7 +1219,29 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
})
|
||||
return
|
||||
}
|
||||
await api.freeMemory({ freeExecutionCache: true })
|
||||
try {
|
||||
const res = await api.freeMemory({ freeExecutionCache: true })
|
||||
if (res.status === 200) {
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: 'Models and Execution Cache have been cleared.',
|
||||
life: 3000
|
||||
})
|
||||
} else {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary:
|
||||
'Unloading of models failed. Installed ComfyUI may be an outdated version.',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: 'An error occurred while trying to unload models.',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,23 +5,7 @@ import { electronAPI } from '@/utils/envUtil'
|
||||
import { i18n } from '@/i18n'
|
||||
|
||||
/**
|
||||
* Composable for building docs.comfy.org URLs with automatic locale and platform detection
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { buildDocsUrl } = useExternalLink()
|
||||
*
|
||||
* // Simple usage
|
||||
* const changelogUrl = buildDocsUrl('/changelog', { includeLocale: true })
|
||||
* // => 'https://docs.comfy.org/zh-CN/changelog' (if Chinese)
|
||||
*
|
||||
* // With platform detection
|
||||
* const desktopUrl = buildDocsUrl('/installation/desktop', {
|
||||
* includeLocale: true,
|
||||
* platform: true
|
||||
* })
|
||||
* // => 'https://docs.comfy.org/zh-CN/installation/desktop/macos' (if Chinese + macOS)
|
||||
* ```
|
||||
* Composable for building docs.comfy.org URLs with automatic locale and platform detection.
|
||||
*/
|
||||
export function useExternalLink() {
|
||||
const locale = computed(() => String(i18n.global.locale.value))
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import type { ContextMenu as ContextMenuType } from '@/lib/litegraph/src/ContextMenu'
|
||||
import type { DragAndScale as DragAndScaleType } from '@/lib/litegraph/src/DragAndScale'
|
||||
import type { LGraph as LGraphType } from '@/lib/litegraph/src/LGraph'
|
||||
import type { LGraphBadge as LGraphBadgeType } from '@/lib/litegraph/src/LGraphBadge'
|
||||
import type { LGraphCanvas as LGraphCanvasType } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import type { LGraphGroup as LGraphGroupType } from '@/lib/litegraph/src/LGraphGroup'
|
||||
import type { LGraphNode as LGraphNodeType } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LiteGraphGlobal } from '@/lib/litegraph/src/LiteGraphGlobal'
|
||||
import type { LLink as LLinkType } from '@/lib/litegraph/src/LLink'
|
||||
|
||||
import {
|
||||
ContextMenu,
|
||||
DragAndScale,
|
||||
@@ -10,26 +20,31 @@ import {
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
LiteGraph: LiteGraphGlobal
|
||||
LGraph: typeof LGraphType
|
||||
LLink: typeof LLinkType
|
||||
LGraphNode: typeof LGraphNodeType
|
||||
LGraphGroup: typeof LGraphGroupType
|
||||
DragAndScale: typeof DragAndScaleType
|
||||
LGraphCanvas: typeof LGraphCanvasType
|
||||
ContextMenu: typeof ContextMenuType
|
||||
LGraphBadge: typeof LGraphBadgeType
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign all properties of LiteGraph to window to make it backward compatible.
|
||||
*/
|
||||
export const useGlobalLitegraph = () => {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['LiteGraph'] = LiteGraph
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['LGraph'] = LGraph
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['LLink'] = LLink
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['LGraphNode'] = LGraphNode
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['LGraphGroup'] = LGraphGroup
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['DragAndScale'] = DragAndScale
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['LGraphCanvas'] = LGraphCanvas
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['ContextMenu'] = ContextMenu
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['LGraphBadge'] = LGraphBadge
|
||||
export function useGlobalLitegraph() {
|
||||
window.LiteGraph = LiteGraph
|
||||
window.LGraph = LGraph
|
||||
window.LLink = LLink
|
||||
window.LGraphNode = LGraphNode
|
||||
window.LGraphGroup = LGraphGroup
|
||||
window.DragAndScale = DragAndScale
|
||||
window.LGraphCanvas = LGraphCanvas
|
||||
window.ContextMenu = ContextMenu
|
||||
window.LGraphBadge = LGraphBadge
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const clipboardHTMLWrapper = [
|
||||
/**
|
||||
* Adds a handler on copy that serializes selected nodes to JSON
|
||||
*/
|
||||
export const useCopy = () => {
|
||||
export const useGraphCopyHandler = () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
useEventListener(document, 'copy', (e) => {
|
||||
90
src/composables/useHelpCommands.test.ts
Normal file
90
src/composables/useHelpCommands.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useHelpCommands } from '@/composables/useHelpCommands'
|
||||
|
||||
const mockLocale = ref('en')
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: vi.fn(() => ({
|
||||
locale: mockLocale
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuth', () => ({
|
||||
useFirebaseAuth: vi.fn(() => null)
|
||||
}))
|
||||
|
||||
vi.mock('firebase/auth', () => ({
|
||||
setPersistence: vi.fn(),
|
||||
browserLocalPersistence: {},
|
||||
onAuthStateChanged: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/composables/useSettingsDialog', () => ({
|
||||
useSettingsDialog: vi.fn(() => ({
|
||||
showAbout: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('useHelpCommands', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('returns all help command objects', () => {
|
||||
const commands = useHelpCommands()
|
||||
expect(commands.length).toBe(6)
|
||||
})
|
||||
|
||||
it('includes expected command IDs', () => {
|
||||
const commands = useHelpCommands()
|
||||
const ids = commands.map((c) => c.id)
|
||||
expect(ids).toContain('Comfy.Help.OpenComfyUIIssues')
|
||||
expect(ids).toContain('Comfy.Help.OpenComfyUIDocs')
|
||||
expect(ids).toContain('Comfy.Help.OpenComfyOrgDiscord')
|
||||
expect(ids).toContain('Comfy.Help.AboutComfyUI')
|
||||
expect(ids).toContain('Comfy.Help.OpenComfyUIForum')
|
||||
expect(ids).toContain('Comfy.ContactSupport')
|
||||
})
|
||||
|
||||
it('all commands have required function property', () => {
|
||||
const commands = useHelpCommands()
|
||||
for (const command of commands) {
|
||||
expect(typeof command.function).toBe('function')
|
||||
}
|
||||
})
|
||||
|
||||
it('all commands have labels and icons', () => {
|
||||
const commands = useHelpCommands()
|
||||
for (const command of commands) {
|
||||
expect(command.label).toBeTruthy()
|
||||
expect(command.icon).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('opens external URLs when help commands are invoked', () => {
|
||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
|
||||
const commands = useHelpCommands()
|
||||
const issuesCommand = commands.find(
|
||||
(c) => c.id === 'Comfy.Help.OpenComfyUIIssues'
|
||||
)!
|
||||
void issuesCommand.function()
|
||||
expect(openSpy).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
)
|
||||
|
||||
openSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
103
src/composables/useHelpCommands.ts
Normal file
103
src/composables/useHelpCommands.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { buildSupportUrl } from '@/platform/support/config'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
|
||||
export function useHelpCommands(): ComfyCommand[] {
|
||||
const telemetry = useTelemetry()
|
||||
const { staticUrls, buildDocsUrl } = useExternalLink()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'Comfy.Help.OpenComfyUIIssues',
|
||||
icon: 'pi pi-github',
|
||||
label: 'Open ComfyUI Issues',
|
||||
menubarLabel: 'ComfyUI Issues',
|
||||
versionAdded: '1.5.5',
|
||||
function: () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'github',
|
||||
is_external: true,
|
||||
source: 'menu'
|
||||
})
|
||||
window.open(staticUrls.githubIssues, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Help.OpenComfyUIDocs',
|
||||
icon: 'pi pi-info-circle',
|
||||
label: 'Open ComfyUI Docs',
|
||||
menubarLabel: 'ComfyUI Docs',
|
||||
versionAdded: '1.5.5',
|
||||
function: () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'docs',
|
||||
is_external: true,
|
||||
source: 'menu'
|
||||
})
|
||||
window.open(
|
||||
buildDocsUrl('/', { includeLocale: true }),
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Help.OpenComfyOrgDiscord',
|
||||
icon: 'pi pi-discord',
|
||||
label: 'Open Comfy-Org Discord',
|
||||
menubarLabel: 'Comfy-Org Discord',
|
||||
versionAdded: '1.5.5',
|
||||
function: () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'discord',
|
||||
is_external: true,
|
||||
source: 'menu'
|
||||
})
|
||||
window.open(staticUrls.discord, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Help.AboutComfyUI',
|
||||
icon: 'pi pi-info-circle',
|
||||
label: 'Open About ComfyUI',
|
||||
menubarLabel: 'About ComfyUI',
|
||||
versionAdded: '1.6.4',
|
||||
function: () => {
|
||||
settingsDialog.showAbout()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Help.OpenComfyUIForum',
|
||||
icon: 'pi pi-comments',
|
||||
label: 'Open ComfyUI Forum',
|
||||
menubarLabel: 'ComfyUI Forum',
|
||||
versionAdded: '1.8.2',
|
||||
function: () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'help_feedback',
|
||||
is_external: true,
|
||||
source: 'menu'
|
||||
})
|
||||
window.open(staticUrls.forum, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ContactSupport',
|
||||
icon: 'pi pi-question',
|
||||
label: 'Contact Support',
|
||||
versionAdded: '1.17.8',
|
||||
function: () => {
|
||||
const { userEmail, resolvedUserInfo } = useCurrentUser()
|
||||
const supportUrl = buildSupportUrl({
|
||||
userEmail: userEmail.value,
|
||||
userId: resolvedUserInfo.value?.id
|
||||
})
|
||||
window.open(supportUrl, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -56,16 +56,13 @@ export function useLazyPagination<T>(
|
||||
return Math.ceil(itemData.length / itemsPerPage)
|
||||
})
|
||||
|
||||
const loadNextPage = async () => {
|
||||
const loadNextPage = () => {
|
||||
if (isLoading.value || !hasMoreItems.value) return
|
||||
|
||||
isLoading.value = true
|
||||
const loadedPagesArray = Array.from(loadedPages.value)
|
||||
const nextPage = Math.max(...loadedPagesArray, 0) + 1
|
||||
|
||||
// Simulate network delay
|
||||
// await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
|
||||
const newLoadedPages = new Set(loadedPages.value)
|
||||
newLoadedPages.add(nextPage)
|
||||
loadedPages.value = newLoadedPages
|
||||
|
||||
@@ -35,13 +35,11 @@ function progressPercentStyle(value: ProgressPercent) {
|
||||
return { width: `${normalized}%` }
|
||||
}
|
||||
|
||||
export function useProgressBarBackground() {
|
||||
return {
|
||||
progressBarContainerClass,
|
||||
progressBarPrimaryClass,
|
||||
progressBarSecondaryClass,
|
||||
hasProgressPercent,
|
||||
hasAnyProgressPercent,
|
||||
progressPercentStyle
|
||||
}
|
||||
export {
|
||||
progressBarContainerClass,
|
||||
progressBarPrimaryClass,
|
||||
progressBarSecondaryClass,
|
||||
hasProgressPercent,
|
||||
hasAnyProgressPercent,
|
||||
progressPercentStyle
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export function useTemplateFiltering(
|
||||
template.models.forEach((model) => modelSet.add(model))
|
||||
}
|
||||
})
|
||||
return Array.from(modelSet).sort()
|
||||
return Array.from(modelSet).sort((a, b) => a.localeCompare(b))
|
||||
})
|
||||
|
||||
const availableUseCases = computed(() => {
|
||||
@@ -78,7 +78,7 @@ export function useTemplateFiltering(
|
||||
template.tags.forEach((tag) => tagSet.add(tag))
|
||||
}
|
||||
})
|
||||
return Array.from(tagSet).sort()
|
||||
return Array.from(tagSet).sort((a, b) => a.localeCompare(b))
|
||||
})
|
||||
|
||||
const availableRunsOn = computed(() => {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Build a tooltip configuration object compatible with v-tooltip.
|
||||
* Consumers pass the translated text value.
|
||||
*/
|
||||
export const buildTooltipConfig = (value: string) => ({
|
||||
value,
|
||||
showDelay: 300,
|
||||
hideDelay: 0,
|
||||
pt: {
|
||||
text: {
|
||||
class:
|
||||
'border-node-component-tooltip-border bg-node-component-tooltip-surface text-node-component-tooltip border rounded-md px-2 py-1 text-xs leading-none shadow-none'
|
||||
},
|
||||
arrow: {
|
||||
class: 'border-t-node-component-tooltip-border'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,48 +1,4 @@
|
||||
import type { HintedString } from '@primevue/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
/**
|
||||
* Options for configuring transform-compatible overlay props
|
||||
* Props to keep PrimeVue overlays within CSS-transformed parent elements.
|
||||
*/
|
||||
interface TransformCompatOverlayOptions {
|
||||
/**
|
||||
* Where to append the overlay. 'self' keeps overlay within component
|
||||
* for proper transform inheritance, 'body' teleports to document body
|
||||
*/
|
||||
appendTo?: HintedString<'body' | 'self'> | undefined | HTMLElement
|
||||
// Future: other props needed for transform compatibility
|
||||
// scrollTarget?: string | HTMLElement
|
||||
// autoZIndex?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable that provides props to make PrimeVue overlay components
|
||||
* compatible with CSS-transformed parent elements.
|
||||
*
|
||||
* Vue nodes use CSS transforms for positioning/scaling. PrimeVue overlay
|
||||
* components (Select, MultiSelect, TreeSelect, etc.) teleport to document
|
||||
* body by default, breaking transform inheritance. This composable provides
|
||||
* the necessary props to keep overlays within their component elements.
|
||||
*
|
||||
* @param overrides - Optional overrides for specific use cases
|
||||
* @returns Computed props object to spread on PrimeVue overlay components
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <template>
|
||||
* <Select v-bind="overlayProps" />
|
||||
* </template>
|
||||
*
|
||||
* <script setup>
|
||||
* const overlayProps = useTransformCompatOverlayProps()
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useTransformCompatOverlayProps(
|
||||
overrides: TransformCompatOverlayOptions = {}
|
||||
) {
|
||||
return computed(() => ({
|
||||
appendTo: 'self' as const,
|
||||
...overrides
|
||||
}))
|
||||
}
|
||||
export const TRANSFORM_COMPAT_OVERLAY_PROPS = { appendTo: 'self' as const }
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export function useZoomControls() {
|
||||
const isModalVisible = ref(false)
|
||||
const isPopoverOpen = ref(false)
|
||||
|
||||
const showModal = () => {
|
||||
isModalVisible.value = true
|
||||
const showPopover = () => {
|
||||
isPopoverOpen.value = true
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
isModalVisible.value = false
|
||||
const hidePopover = () => {
|
||||
isPopoverOpen.value = false
|
||||
}
|
||||
|
||||
const toggleModal = () => {
|
||||
isModalVisible.value = !isModalVisible.value
|
||||
const togglePopover = () => {
|
||||
isPopoverOpen.value = !isPopoverOpen.value
|
||||
}
|
||||
|
||||
const hasActivePopup = computed(() => isModalVisible.value)
|
||||
const hasActivePopup = computed(() => isPopoverOpen.value)
|
||||
|
||||
return {
|
||||
isModalVisible,
|
||||
showModal,
|
||||
hideModal,
|
||||
toggleModal,
|
||||
isPopoverOpen,
|
||||
showPopover,
|
||||
hidePopover,
|
||||
togglePopover,
|
||||
hasActivePopup
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
/** Default panel size (%) for sidebar and builder panels */
|
||||
export const SIDE_PANEL_SIZE = 20
|
||||
|
||||
/** Default panel size (%) for the center/main panel */
|
||||
export const CENTER_PANEL_SIZE = 80
|
||||
|
||||
/** Minimum panel size (%) for the sidebar */
|
||||
export const SIDEBAR_MIN_SIZE = 10
|
||||
|
||||
/** Minimum panel size (%) for the builder panel */
|
||||
export const BUILDER_MIN_SIZE = 15
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Toolkit (Essentials) node detection constants.
|
||||
*
|
||||
* Re-exported from essentialsNodes.ts — the single source of truth.
|
||||
* Used by telemetry to track toolkit node adoption and popularity.
|
||||
*/
|
||||
export {
|
||||
TOOLKIT_NOVEL_NODE_NAMES as TOOLKIT_NODE_NAMES,
|
||||
TOOLKIT_BLUEPRINT_MODULES
|
||||
} from './essentialsNodes'
|
||||
@@ -1,19 +1,7 @@
|
||||
interface UVMirror {
|
||||
/**
|
||||
* The setting id defined for the mirror.
|
||||
*/
|
||||
settingId: string
|
||||
/**
|
||||
* The default mirror to use.
|
||||
*/
|
||||
mirror: string
|
||||
/**
|
||||
* The fallback mirror to use.
|
||||
*/
|
||||
fallbackMirror: string
|
||||
/**
|
||||
* The path suffix to validate the mirror is reachable.
|
||||
*/
|
||||
validationPathSuffix?: string
|
||||
}
|
||||
|
||||
|
||||
20
src/core/graph/subgraph/promotedWidgetTypes.test.ts
Normal file
20
src/core/graph/subgraph/promotedWidgetTypes.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isPromotedWidgetView } from './promotedWidgetTypes'
|
||||
|
||||
describe('isPromotedWidgetView', () => {
|
||||
it('returns true for widgets with sourceNodeId and sourceWidgetName', () => {
|
||||
const widget = {
|
||||
name: 'test',
|
||||
value: 0,
|
||||
sourceNodeId: 'node-1',
|
||||
sourceWidgetName: 'steps'
|
||||
}
|
||||
expect(isPromotedWidgetView(widget as never)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for regular widgets', () => {
|
||||
const widget = { name: 'test', value: 0 }
|
||||
expect(isPromotedWidgetView(widget as never)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -228,7 +228,7 @@ export function promoteRecommendedWidgets(subgraphNode: SubgraphNode) {
|
||||
if (hasPreviewWidget()) continue
|
||||
|
||||
// Nodes in CANVAS_IMAGE_PREVIEW_NODE_TYPES support a virtual $$
|
||||
// preview widget. Eagerly promote it so getPseudoWidgetPreviewTargets
|
||||
// preview widget. Eagerly promote it so resolvePseudoWidgetPreviewTargets
|
||||
// includes this node and onDrawBackground can call updatePreviews on it
|
||||
// once execution outputs arrive.
|
||||
if (supportsVirtualCanvasImagePreview(node)) {
|
||||
|
||||
@@ -9,10 +9,17 @@ type ProxyWidgetsProperty = z.infer<typeof proxyWidgetsPropertySchema>
|
||||
export function parseProxyWidgets(
|
||||
property: NodeProperty | undefined
|
||||
): ProxyWidgetsProperty {
|
||||
if (typeof property === 'string') property = JSON.parse(property)
|
||||
const result = proxyWidgetsPropertySchema.safeParse(
|
||||
typeof property === 'string' ? JSON.parse(property) : property
|
||||
)
|
||||
let parsed = property
|
||||
if (typeof parsed === 'string') {
|
||||
try {
|
||||
parsed = JSON.parse(parsed)
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Invalid assignment for properties.proxyWidgets:\nMalformed JSON string`
|
||||
)
|
||||
}
|
||||
}
|
||||
const result = proxyWidgetsPropertySchema.safeParse(parsed)
|
||||
if (result.success) return result.data
|
||||
|
||||
const error = fromZodError(result.error)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyApp } from '../../scripts/app'
|
||||
import { $el, ComfyDialog } from '../../scripts/ui'
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComfyApp } from '@/scripts/app'
|
||||
import { $el, ComfyDialog } from '@/scripts/ui'
|
||||
|
||||
export class ClipspaceDialog extends ComfyDialog {
|
||||
class ClipspaceDialog extends ComfyDialog {
|
||||
static items: Array<
|
||||
HTMLButtonElement & {
|
||||
contextPredicate?: () => boolean
|
||||
@@ -85,10 +85,8 @@ export class ClipspaceDialog extends ComfyDialog {
|
||||
override createButtons() {
|
||||
const buttons = []
|
||||
|
||||
for (let idx in ClipspaceDialog.items) {
|
||||
const item = ClipspaceDialog.items[idx]
|
||||
if (!item.contextPredicate || item.contextPredicate())
|
||||
buttons.push(ClipspaceDialog.items[idx])
|
||||
for (const item of ClipspaceDialog.items) {
|
||||
if (!item.contextPredicate || item.contextPredicate()) buttons.push(item)
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
isComboWidget
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
// Adds filtering to combo context menus
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app } from '../../scripts/app'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ import { ExecutableGroupNodeChildDTO } from '@/utils/executableGroupNodeChildDTO
|
||||
import { GROUP } from '@/utils/executableGroupNodeDto'
|
||||
import { deserialiseAndCreate, serialise } from '@/utils/vintageClipboard'
|
||||
|
||||
import { api } from '../../scripts/api'
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { ManageGroupDialog } from './groupNodeManage'
|
||||
import { mergeIfValid } from './widgetInputs'
|
||||
|
||||
@@ -732,7 +732,7 @@ export class GroupNodeConfig {
|
||||
) {
|
||||
// Add converted widgets sorted into their index order (ordered as they were converted) so link ids match up
|
||||
const convertedSlots = [...converted.keys()]
|
||||
.sort()
|
||||
.sort((a, b) => a - b)
|
||||
.map((k) => converted.get(k))
|
||||
for (let i = 0; i < convertedSlots.length; i++) {
|
||||
const inputName = convertedSlots[i]
|
||||
|
||||
@@ -8,11 +8,16 @@ import type {
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
|
||||
import { type ComfyApp, app } from '../../scripts/app'
|
||||
import { $el } from '../../scripts/ui'
|
||||
import { ComfyDialog } from '../../scripts/ui/dialog'
|
||||
import { DraggableList } from '../../scripts/ui/draggableList'
|
||||
import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import { app } from '@/scripts/app'
|
||||
import { $el } from '@/scripts/ui'
|
||||
import { ComfyDialog } from '@/scripts/ui/dialog'
|
||||
import { DraggableList } from '@/scripts/ui/draggableList'
|
||||
import type { GroupNodeConfig } from './groupNode'
|
||||
|
||||
// Lazy import to break circular dependency with groupNode.ts
|
||||
// These are only called at runtime (in methods), not at module init time.
|
||||
const lazyGroupNode = () => import('./groupNode')
|
||||
import './groupNodeManage.css'
|
||||
|
||||
const ORDER: symbol = Symbol()
|
||||
@@ -112,16 +117,17 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
this.changeTab(this.selectedTab)
|
||||
}
|
||||
|
||||
getGroupData() {
|
||||
async getGroupData() {
|
||||
this.groupNodeType = LiteGraph.registered_node_types[
|
||||
`${PREFIX}${SEPARATOR}` + this.selectedGroup
|
||||
] as unknown as LGraphNodeConstructor<LGraphNode>
|
||||
const { GroupNodeHandler } = await lazyGroupNode()
|
||||
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType)!
|
||||
}
|
||||
|
||||
changeGroup(group: string, reset = true): void {
|
||||
async changeGroup(group: string, reset = true): Promise<void> {
|
||||
this.selectedGroup = group
|
||||
this.getGroupData()
|
||||
await this.getGroupData()
|
||||
|
||||
const nodes = this.groupData.nodeData.nodes
|
||||
this.nodeItems = nodes.map(
|
||||
@@ -344,7 +350,7 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
return !!outputs.length
|
||||
}
|
||||
|
||||
override show(groupNodeType?: string | HTMLElement | HTMLElement[]): void {
|
||||
override show(groupNodeType?: string | HTMLElement | HTMLElement[]) {
|
||||
// Extract string type - this method repurposes the show signature
|
||||
const nodeType =
|
||||
typeof groupNodeType === 'string' ? groupNodeType : undefined
|
||||
@@ -392,8 +398,8 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
$el(
|
||||
'select',
|
||||
{
|
||||
onchange: (e: Event) => {
|
||||
this.changeGroup((e.target as HTMLSelectElement).value)
|
||||
onchange: async (e: Event) => {
|
||||
await this.changeGroup((e.target as HTMLSelectElement).value)
|
||||
}
|
||||
},
|
||||
groupNodes.map((g) =>
|
||||
@@ -539,6 +545,7 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
if (groupTypeNodes) recreateNodes.push(...groupTypeNodes)
|
||||
}
|
||||
|
||||
const { GroupNodeConfig } = await lazyGroupNode()
|
||||
await GroupNodeConfig.registerFromWorkflow(types, [])
|
||||
|
||||
for (const node of recreateNodes) {
|
||||
@@ -547,7 +554,7 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
|
||||
this.modifications = {}
|
||||
this.app.canvas.setDirty(true, true)
|
||||
this.changeGroup(this.selectedGroup!, false)
|
||||
await this.changeGroup(this.selectedGroup!, false)
|
||||
}
|
||||
},
|
||||
'Save'
|
||||
@@ -561,13 +568,13 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
])
|
||||
|
||||
this.element.replaceChildren(outer)
|
||||
this.changeGroup(
|
||||
this.element.showModal()
|
||||
void this.changeGroup(
|
||||
nodeType
|
||||
? (groupNodes.find((g) => `${PREFIX}${SEPARATOR}${g}` === nodeType) ??
|
||||
groupNodes[0])
|
||||
: groupNodes[0]
|
||||
)
|
||||
this.element.showModal()
|
||||
|
||||
this.element.addEventListener('close', () => {
|
||||
this.draggable?.dispose()
|
||||
|
||||
@@ -2,15 +2,12 @@ import type {
|
||||
IContextMenuValue,
|
||||
Positionable
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LGraphGroup,
|
||||
type LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas, LGraphGroup } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
function setNodeMode(node: LGraphNode, mode: number) {
|
||||
node.mode = mode
|
||||
|
||||
@@ -2,7 +2,10 @@ import { nextTick } from 'vue'
|
||||
|
||||
import Load3D from '@/components/load3d/Load3D.vue'
|
||||
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||
import { nodeToLoad3dMap, useLoad3d } from '@/composables/useLoad3d'
|
||||
import {
|
||||
nodeToLoad3dMap,
|
||||
useLoad3d
|
||||
} from '@/extensions/core/load3d/composables/useLoad3d'
|
||||
import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper'
|
||||
import type {
|
||||
CameraConfig,
|
||||
|
||||
@@ -64,9 +64,7 @@ export class AnimationManager implements AnimationManagerInterface {
|
||||
|
||||
this.currentAnimation = new THREE.AnimationMixer(model)
|
||||
|
||||
if (this.animationClips.length > 0) {
|
||||
this.updateSelectedAnimation(0)
|
||||
}
|
||||
this.updateSelectedAnimation(0)
|
||||
} else {
|
||||
this.animationClips = []
|
||||
}
|
||||
|
||||
@@ -73,11 +73,9 @@ export class CameraManager implements CameraManagerInterface {
|
||||
setControls(controls: OrbitControls): void {
|
||||
this.controls = controls
|
||||
|
||||
if (this.controls) {
|
||||
this.controls.addEventListener('end', () => {
|
||||
this.eventManager.emitEvent('cameraChanged', this.getCameraState())
|
||||
})
|
||||
}
|
||||
this.controls.addEventListener('end', () => {
|
||||
this.eventManager.emitEvent('cameraChanged', this.getCameraState())
|
||||
})
|
||||
}
|
||||
|
||||
getCurrentCameraType(): CameraType {
|
||||
@@ -117,13 +115,11 @@ export class CameraManager implements CameraManagerInterface {
|
||||
this.activeCamera.position.copy(position)
|
||||
this.activeCamera.rotation.copy(rotation)
|
||||
|
||||
if (this.activeCamera instanceof THREE.OrthographicCamera) {
|
||||
this.activeCamera.zoom = oldZoom
|
||||
this.activeCamera.updateProjectionMatrix()
|
||||
} else if (this.activeCamera instanceof THREE.PerspectiveCamera) {
|
||||
this.activeCamera.zoom = oldZoom
|
||||
this.activeCamera.updateProjectionMatrix()
|
||||
}
|
||||
const cam = this.activeCamera as
|
||||
| THREE.PerspectiveCamera
|
||||
| THREE.OrthographicCamera
|
||||
cam.zoom = oldZoom
|
||||
cam.updateProjectionMatrix()
|
||||
|
||||
if (this.controls) {
|
||||
this.controls.object = this.activeCamera
|
||||
@@ -160,13 +156,11 @@ export class CameraManager implements CameraManagerInterface {
|
||||
|
||||
this.controls?.target.copy(state.target)
|
||||
|
||||
if (this.activeCamera instanceof THREE.OrthographicCamera) {
|
||||
this.activeCamera.zoom = state.zoom
|
||||
this.activeCamera.updateProjectionMatrix()
|
||||
} else if (this.activeCamera instanceof THREE.PerspectiveCamera) {
|
||||
this.activeCamera.zoom = state.zoom
|
||||
this.activeCamera.updateProjectionMatrix()
|
||||
}
|
||||
const cam = this.activeCamera as
|
||||
| THREE.PerspectiveCamera
|
||||
| THREE.OrthographicCamera
|
||||
cam.zoom = state.zoom
|
||||
cam.updateProjectionMatrix()
|
||||
|
||||
this.controls?.update()
|
||||
}
|
||||
|
||||
@@ -29,10 +29,9 @@ export class ControlsManager implements ControlsManagerInterface {
|
||||
const cameraState = {
|
||||
position: this.camera.position.clone(),
|
||||
target: this.controls.target.clone(),
|
||||
zoom:
|
||||
this.camera instanceof THREE.OrthographicCamera
|
||||
? (this.camera as THREE.OrthographicCamera).zoom
|
||||
: (this.camera as THREE.PerspectiveCamera).zoom,
|
||||
zoom: (
|
||||
this.camera as THREE.PerspectiveCamera | THREE.OrthographicCamera
|
||||
).zoom,
|
||||
cameraType:
|
||||
this.camera instanceof THREE.PerspectiveCamera
|
||||
? 'perspective'
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
ModelConfig,
|
||||
SceneConfig
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import type { Dictionary } from '@/lib/litegraph/src/interfaces'
|
||||
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
@@ -25,7 +24,7 @@ type Load3DConfigurationSettings = {
|
||||
class Load3DConfiguration {
|
||||
constructor(
|
||||
private load3d: Load3d,
|
||||
private properties?: Dictionary<NodeProperty | undefined>
|
||||
private properties?: Record<string, NodeProperty | undefined>
|
||||
) {}
|
||||
|
||||
configureForSaveMesh(loadFolder: 'input' | 'output', filePath: string) {
|
||||
|
||||
@@ -268,6 +268,36 @@ class Load3d {
|
||||
return this.isViewerMode || (this.targetWidth > 0 && this.targetHeight > 0)
|
||||
}
|
||||
|
||||
private calculateLetterbox(
|
||||
containerWidth: number,
|
||||
containerHeight: number
|
||||
): {
|
||||
renderWidth: number
|
||||
renderHeight: number
|
||||
offsetX: number
|
||||
offsetY: number
|
||||
} {
|
||||
const containerAspectRatio = containerWidth / containerHeight
|
||||
if (containerAspectRatio > this.targetAspectRatio) {
|
||||
const renderHeight = containerHeight
|
||||
const renderWidth = renderHeight * this.targetAspectRatio
|
||||
return {
|
||||
renderWidth,
|
||||
renderHeight,
|
||||
offsetX: (containerWidth - renderWidth) / 2,
|
||||
offsetY: 0
|
||||
}
|
||||
}
|
||||
const renderWidth = containerWidth
|
||||
const renderHeight = renderWidth / this.targetAspectRatio
|
||||
return {
|
||||
renderWidth,
|
||||
renderHeight,
|
||||
offsetX: 0,
|
||||
offsetY: (containerHeight - renderHeight) / 2
|
||||
}
|
||||
}
|
||||
|
||||
forceRender(): void {
|
||||
const delta = this.clock.getDelta()
|
||||
this.animationManager.update(delta)
|
||||
@@ -299,22 +329,8 @@ class Load3d {
|
||||
}
|
||||
|
||||
if (this.shouldMaintainAspectRatio()) {
|
||||
const containerAspectRatio = containerWidth / containerHeight
|
||||
|
||||
let renderWidth: number
|
||||
let renderHeight: number
|
||||
let offsetX: number = 0
|
||||
let offsetY: number = 0
|
||||
|
||||
if (containerAspectRatio > this.targetAspectRatio) {
|
||||
renderHeight = containerHeight
|
||||
renderWidth = renderHeight * this.targetAspectRatio
|
||||
offsetX = (containerWidth - renderWidth) / 2
|
||||
} else {
|
||||
renderWidth = containerWidth
|
||||
renderHeight = renderWidth / this.targetAspectRatio
|
||||
offsetY = (containerHeight - renderHeight) / 2
|
||||
}
|
||||
const { renderWidth, renderHeight, offsetX, offsetY } =
|
||||
this.calculateLetterbox(containerWidth, containerHeight)
|
||||
|
||||
this.renderer.setViewport(0, 0, containerWidth, containerHeight)
|
||||
this.renderer.setScissor(0, 0, containerWidth, containerHeight)
|
||||
@@ -325,8 +341,7 @@ class Load3d {
|
||||
this.renderer.setViewport(offsetX, offsetY, renderWidth, renderHeight)
|
||||
this.renderer.setScissor(offsetX, offsetY, renderWidth, renderHeight)
|
||||
|
||||
const renderAspectRatio = renderWidth / renderHeight
|
||||
this.cameraManager.updateAspectRatio(renderAspectRatio)
|
||||
this.cameraManager.updateAspectRatio(renderWidth / renderHeight)
|
||||
} else {
|
||||
// No aspect ratio constraint: fill the entire container
|
||||
this.renderer.setViewport(0, 0, containerWidth, containerHeight)
|
||||
@@ -436,7 +451,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
|
||||
default:
|
||||
throw new Error(`Unsupported export format: ${format}`)
|
||||
@@ -468,18 +483,10 @@ class Load3d {
|
||||
const containerHeight = this.renderer.domElement.clientHeight
|
||||
|
||||
if (this.shouldMaintainAspectRatio()) {
|
||||
const containerAspectRatio = containerWidth / containerHeight
|
||||
|
||||
let renderWidth: number
|
||||
let renderHeight: number
|
||||
|
||||
if (containerAspectRatio > this.targetAspectRatio) {
|
||||
renderHeight = containerHeight
|
||||
renderWidth = renderHeight * this.targetAspectRatio
|
||||
} else {
|
||||
renderWidth = containerWidth
|
||||
renderHeight = renderWidth / this.targetAspectRatio
|
||||
}
|
||||
const { renderWidth, renderHeight } = this.calculateLetterbox(
|
||||
containerWidth,
|
||||
containerHeight
|
||||
)
|
||||
|
||||
this.sceneManager.updateBackgroundSize(
|
||||
this.sceneManager.backgroundTexture,
|
||||
@@ -558,7 +565,9 @@ class Load3d {
|
||||
if (this.loadingPromise) {
|
||||
try {
|
||||
await this.loadingPromise
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// Previous load error is irrelevant — we only need to wait for it to settle
|
||||
}
|
||||
}
|
||||
|
||||
this.loadingPromise = this._loadModelInternal(url, originalFileName)
|
||||
@@ -653,17 +662,10 @@ class Load3d {
|
||||
}
|
||||
|
||||
if (this.shouldMaintainAspectRatio()) {
|
||||
const containerAspectRatio = containerWidth / containerHeight
|
||||
let renderWidth: number
|
||||
let renderHeight: number
|
||||
|
||||
if (containerAspectRatio > this.targetAspectRatio) {
|
||||
renderHeight = containerHeight
|
||||
renderWidth = renderHeight * this.targetAspectRatio
|
||||
} else {
|
||||
renderWidth = containerWidth
|
||||
renderHeight = renderWidth / this.targetAspectRatio
|
||||
}
|
||||
const { renderWidth, renderHeight } = this.calculateLetterbox(
|
||||
containerWidth,
|
||||
containerHeight
|
||||
)
|
||||
|
||||
this.renderer.setSize(containerWidth, containerHeight)
|
||||
this.cameraManager.handleResize(renderWidth, renderHeight)
|
||||
|
||||
@@ -2,7 +2,8 @@ import type Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
import { getResourceURL, splitFilePath } from '@/utils/resourceUrl'
|
||||
|
||||
class Load3dUtils {
|
||||
static async generateThumbnailIfNeeded(
|
||||
@@ -114,31 +115,9 @@ class Load3dUtils {
|
||||
return uploadPath
|
||||
}
|
||||
|
||||
static splitFilePath(path: string): [string, string] {
|
||||
const folder_separator = path.lastIndexOf('/')
|
||||
if (folder_separator === -1) {
|
||||
return ['', path]
|
||||
}
|
||||
return [
|
||||
path.substring(0, folder_separator),
|
||||
path.substring(folder_separator + 1)
|
||||
]
|
||||
}
|
||||
static splitFilePath = splitFilePath
|
||||
|
||||
static getResourceURL(
|
||||
subfolder: string,
|
||||
filename: string,
|
||||
type: string = 'input'
|
||||
): string {
|
||||
const params = [
|
||||
'filename=' + encodeURIComponent(filename),
|
||||
'type=' + type,
|
||||
'subfolder=' + subfolder,
|
||||
app.getRandParam().substring(1)
|
||||
].join('&')
|
||||
|
||||
return `/view?${params}`
|
||||
}
|
||||
static getResourceURL = getResourceURL
|
||||
|
||||
static async uploadMultipleFiles(files: FileList, subfolder: string = '3d') {
|
||||
const uploadPromises = Array.from(files).map((file) =>
|
||||
|
||||
@@ -39,7 +39,7 @@ export class ModelExporter {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const blob = await response.blob()
|
||||
downloadBlob(desiredFilename, blob)
|
||||
downloadBlob(blob, desiredFilename)
|
||||
} catch (error) {
|
||||
console.error('Error downloading from URL:', error)
|
||||
useToastStore().addAlert(t('toastMessages.failedToDownloadFile'))
|
||||
@@ -147,11 +147,11 @@ export class ModelExporter {
|
||||
|
||||
private static saveArrayBuffer(buffer: ArrayBuffer, filename: string): void {
|
||||
const blob = new Blob([buffer], { type: 'application/octet-stream' })
|
||||
downloadBlob(filename, blob)
|
||||
downloadBlob(blob, filename)
|
||||
}
|
||||
|
||||
private static saveString(text: string, filename: string): void {
|
||||
const blob = new Blob([text], { type: 'text/plain' })
|
||||
downloadBlob(filename, blob)
|
||||
downloadBlob(blob, filename)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ export class RecordingManager {
|
||||
|
||||
try {
|
||||
const blob = new Blob(this.recordedChunks, { type: 'video/webm' })
|
||||
downloadBlob(filename, blob)
|
||||
downloadBlob(blob, filename)
|
||||
|
||||
this.eventManager.emitEvent('recordingExported', null)
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
import Load3dUtils from './Load3dUtils'
|
||||
import { getResourceURL, splitFilePath } from '@/utils/resourceUrl'
|
||||
import {
|
||||
type BackgroundRenderModeType,
|
||||
type EventManagerInterface,
|
||||
@@ -148,7 +148,7 @@ export class SceneManager implements SceneManagerInterface {
|
||||
this.eventManager.emitEvent('backgroundImageLoadingStart', null)
|
||||
|
||||
let type = 'input'
|
||||
let pathParts = Load3dUtils.splitFilePath(uploadPath)
|
||||
let pathParts = splitFilePath(uploadPath)
|
||||
let subfolder = pathParts[0]
|
||||
let filename = pathParts[1]
|
||||
|
||||
@@ -160,7 +160,7 @@ export class SceneManager implements SceneManagerInterface {
|
||||
pathParts = ['', filename]
|
||||
}
|
||||
|
||||
let imageUrl = Load3dUtils.getResourceURL(...pathParts, type)
|
||||
let imageUrl = getResourceURL(...pathParts, type)
|
||||
|
||||
if (!imageUrl.startsWith('/api')) {
|
||||
imageUrl = '/api' + imageUrl
|
||||
|
||||
@@ -92,13 +92,8 @@ export class ViewHelperManager implements ViewHelperManagerInterface {
|
||||
handleResize(): void {}
|
||||
|
||||
visibleViewHelper(visible: boolean) {
|
||||
if (visible) {
|
||||
this.viewHelper.visible = true
|
||||
this.viewHelperContainer.style.display = 'block'
|
||||
} else {
|
||||
this.viewHelper.visible = false
|
||||
this.viewHelperContainer.style.display = 'none'
|
||||
}
|
||||
this.viewHelper.visible = visible
|
||||
this.viewHelperContainer.style.display = visible ? 'block' : 'none'
|
||||
}
|
||||
|
||||
recreateViewHelper(): void {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref, shallowRef } from 'vue'
|
||||
|
||||
import { nodeToLoad3dMap, useLoad3d } from '@/composables/useLoad3d'
|
||||
import {
|
||||
nodeToLoad3dMap,
|
||||
useLoad3d
|
||||
} from '@/extensions/core/load3d/composables/useLoad3d'
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import type { Size } from '@/lib/litegraph/src/interfaces'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user