Compare commits

..

6 Commits

Author SHA1 Message Date
christian-byrne
633ddb2099 [chore] Update Comfy Registry API types from comfy-api@728924b 2025-05-07 20:22:18 +00:00
Chenlei Hu
973a1eb0a9 [Bug] Guard link fixer with try-catch (#3806) 2025-05-07 16:10:21 -04:00
filtered
b9d9ce78f9 [TS] Widget typing (#3804) 2025-05-08 04:38:17 +10:00
Benjamin Lu
bb1ac32ccd Revert "Remove Release Summary section in README (#3607)" (#3802) 2025-05-07 13:56:05 -04:00
Christian Byrne
1ef3c007e6 [Auth] Allow user select GitHub account on login (#3801) 2025-05-07 12:03:23 -04:00
Terry Jia
db81b62274 [3d] add record video support for load3d animation node (#3798) 2025-05-07 10:12:33 -04:00
21 changed files with 7736 additions and 866 deletions

443
README.md
View File

@@ -67,6 +67,449 @@ The development of successive minor versions overlaps. For example, while versio
| 3 | Mar 15-21 | Released | Feature Freeze | Development | 1.1.7 through 1.1.13 (daily)<br>1.2.0 through 1.2.6 (daily) |
| 4 | Mar 22-28 | - | Released | Feature Freeze | 1.2.7 through 1.2.13 (daily)<br>1.3.0 through 1.3.6 (daily) |
## Release Summary
### Major features
<details id='feature-native-translation'>
<summary>v1.5: Native translation (i18n)</summary>
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, or Korean. This native
implementation offers better performance, reliability, and maintainability compared to previous solutions.<br>
More details available here: https://blog.comfy.org/p/native-localization-support-i18n
</details>
<details id='feature-mask-editor'>
<summary>v1.4: New mask editor</summary>
https://github.com/Comfy-Org/ComfyUI_frontend/pull/1284 implements a new mask editor.
![image](https://github.com/user-attachments/assets/f0ea6ee5-00ee-4e5d-a09c-6938e86a1f17)
</details>
<details id='feature-integrated-server-terminal'>
<summary>v1.3.22: Integrated server terminal</summary>
Press Ctrl + ` to toggle integrated terminal.
https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
</details>
<details id='feature-keybinding-customization'>
<summary>v1.3.7: Keybinding customization</summary>
## Basic UI
![image](https://github.com/user-attachments/assets/c84a1609-3880-48e0-a746-011f36beda68)
## Reset button
![image](https://github.com/user-attachments/assets/4d2922da-bb4f-4f90-8017-a8e4a0db07c7)
## Edit Keybinding
![image](https://github.com/user-attachments/assets/77626b7a-cb46-48f8-9465-e03120aac66a)
![image](https://github.com/user-attachments/assets/79131a4e-75c6-4715-bd11-c6aaed887779)
[rec.webm](https://github.com/user-attachments/assets/a3984ed9-eb28-4d47-86c0-7fc3efc2b5d0)
</details>
<details id='feature-node-library-sidebar'>
<summary>v1.2.4: Node library sidebar tab</summary>
#### Drag & Drop
https://github.com/user-attachments/assets/853e20b7-bc0e-49c9-bbce-a2ba7566f92f
#### Filter
https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
</details>
<details id='feature-queue-sidebar'>
<summary>v1.2.0: Queue/History sidebar tab</summary>
https://github.com/user-attachments/assets/86e264fe-4d26-4f07-aa9a-83bdd2d02b8f
</details>
<details id='feature-node-search'>
<summary>v1.1.0: Node search box</summary>
#### Fuzzy search & Node preview
![image](https://github.com/user-attachments/assets/94733e32-ea4e-4a9c-b321-c1a05db48709)
#### Release link with shift
https://github.com/user-attachments/assets/a1b2b5c3-10d1-4256-b620-345de6858f25
</details>
### QoL changes
<details id='feature-nested-group'>
<summary>v1.3.32: **Litegraph** Nested group</summary>
https://github.com/user-attachments/assets/f51adeb1-028e-40af-81e4-0ac13075198a
</details>
<details id='feature-group-selection'>
<summary>v1.3.24: **Litegraph** Group selection</summary>
https://github.com/user-attachments/assets/e6230a94-411e-4fba-90cb-6c694200adaa
</details>
<details id='feature-toggle-link-visibility'>
<summary>v1.3.6: **Litegraph** Toggle link visibility</summary>
[rec.webm](https://github.com/user-attachments/assets/34e460ac-fbbc-44ef-bfbb-99a84c2ae2be)
</details>
<details id='feature-auto-widget-conversion'>
<summary>v1.3.4: **Litegraph** Auto widget to input conversion</summary>
Dropping a link of correct type on node widget will automatically convert the widget to input.
[rec.webm](https://github.com/user-attachments/assets/15cea0b0-b225-4bec-af50-2cdb16dc46bf)
</details>
<details id='feature-pan-mode'>
<summary>v1.3.4: **Litegraph** Canvas pan mode</summary>
The canvas becomes readonly in pan mode. Pan mode is activated by clicking the pan mode button on the canvas menu
or by holding the space key.
[rec.webm](https://github.com/user-attachments/assets/c7872532-a2ac-44c1-9e7d-9e03b5d1a80b)
</details>
<details id='feature-shift-drag-link-creation'>
<summary>v1.3.1: **Litegraph** Shift drag link to create a new link</summary>
[rec.webm](https://github.com/user-attachments/assets/7e73aaf9-79e2-4c3c-a26a-911cba3b85e4)
</details>
<details id='feature-optional-input-donuts'>
<summary>v1.2.62: **Litegraph** Show optional input slots as donuts</summary>
![GYEIRidb0AYGO-v](https://github.com/user-attachments/assets/e6cde0b6-654b-4afd-a117-133657a410b1)
</details>
<details id='feature-group-title-edit'>
<summary>v1.2.44: **Litegraph** Double click group title to edit</summary>
https://github.com/user-attachments/assets/5bf0e2b6-8b3a-40a7-b44f-f0879e9ad26f
</details>
<details id='feature-group-selection-shortcut'>
<summary>v1.2.39: **Litegraph** Group selected nodes with Ctrl + G</summary>
https://github.com/user-attachments/assets/7805dc54-0854-4a28-8bcd-4b007fa01151
</details>
<details id='feature-node-title-edit'>
<summary>v1.2.38: **Litegraph** Double click node title to edit</summary>
https://github.com/user-attachments/assets/d61d5d0e-f200-4153-b293-3e3f6a212b30
</details>
<details id='feature-drag-multi-link'>
<summary>v1.2.7: **Litegraph** drags multiple links with shift pressed</summary>
https://github.com/user-attachments/assets/68826715-bb55-4b2a-be6e-675cfc424afe
https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
</details>
<details id='feature-auto-connect-link'>
<summary>v1.2.2: **Litegraph** auto connects to correct slot</summary>
#### Before
https://github.com/user-attachments/assets/c253f778-82d5-4e6f-aec0-ea2ccf421651
#### After
https://github.com/user-attachments/assets/b6360ac0-f0d2-447c-9daa-8a2e20c0dc1d
</details>
<details id='feature-hide-text-overflow'>
<summary>v1.1.8: **Litegraph** hides text overflow on widget value</summary>
https://github.com/user-attachments/assets/5696a89d-4a47-4fcc-9e8c-71e1264943f2
</details>
### Developer APIs
<details>
<summary>v1.6.13: prompt/confirm/alert replacements for ComfyUI desktop</summary>
Several browser-only APIs are not available in ComfyUI desktop's electron environment.
- `window.prompt`
- `window.confirm`
- `window.alert`
Please use the following APIs as replacements.
```js
// window.prompt
window['app'].extensionManager.dialog
.prompt({
title: 'Test Prompt',
message: 'Test Prompt Message'
})
.then((value: string) => {
// Do something with the value user entered
})
```
![image](https://github.com/user-attachments/assets/c73f74d0-9bb4-4555-8d56-83f1be4a1d7e)
```js
// window.confirm
window['app'].extensionManager.dialog
.confirm({
title: 'Test Confirm',
message: 'Test Confirm Message'
})
.then((value: boolean) => {
// Do something with the value user entered
})
```
![image](https://github.com/user-attachments/assets/8dec7a42-7443-4245-85be-ceefb1116e96)
```js
// window.alert
window['app'].extensionManager.toast
.addAlert("Test Alert")
```
![image](https://github.com/user-attachments/assets/9b18bdca-76ef-4432-95de-5cd2369684f2)
</details>
<details>
<summary>v1.3.34: Register about panel badges</summary>
```js
app.registerExtension({
name: 'TestExtension1',
aboutPageBadges: [
{
label: 'Test Badge',
url: 'https://example.com',
icon: 'pi pi-box'
}
]
})
```
![image](https://github.com/user-attachments/assets/099e77ee-16ad-4141-b2fc-5e9d5075188b)
</details>
<details id='extension-api-bottom-panel-tabs'>
<summary>v1.3.22: Register bottom panel tabs</summary>
```js
app.registerExtension({
name: 'TestExtension',
bottomPanelTabs: [
{
id: 'TestTab',
title: 'Test Tab',
type: 'custom',
render: (el) => {
el.innerHTML = '<div>Custom tab</div>'
}
}
]
})
```
![image](https://github.com/user-attachments/assets/2114f8b8-2f55-414b-b027-78e61c870b64)
</details>
<details id='extension-api-settings'>
<summary>v1.3.22: New settings API</summary>
Legacy settings API.
```js
// Register a new setting
app.ui.settings.addSetting({
id: 'TestSetting',
name: 'Test Setting',
type: 'text',
defaultValue: 'Hello, world!'
})
// Get the value of a setting
const value = app.ui.settings.getSettingValue('TestSetting')
// Set the value of a setting
app.ui.settings.setSettingValue('TestSetting', 'Hello, universe!')
```
New settings API.
```js
// Register a new setting
app.registerExtension({
name: 'TestExtension1',
settings: [
{
id: 'TestSetting',
name: 'Test Setting',
type: 'text',
defaultValue: 'Hello, world!'
}
]
})
// Get the value of a setting
const value = app.extensionManager.setting.get('TestSetting')
// Set the value of a setting
app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
```
</details>
<details id='extension-api-commands-keybindings'>
<summary>v1.3.7: Register commands and keybindings</summary>
Extensions can call the following API to register commands and keybindings. Do
note that keybindings defined in core cannot be overwritten, and some keybindings
are reserved by the browser.
```js
app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'TestCommand',
function: () => {
alert('TestCommand')
}
}
],
keybindings: [
{
combo: { key: 'k' },
commandId: 'TestCommand'
}
]
})
```
</details>
<details id='extension-api-topbar-menu'>
<summary>v1.3.1: Extension API to register custom topbar menu items</summary>
Extensions can call the following API to register custom topbar menu items.
```js
app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'foo-id',
label: 'foo',
function: () => {
alert(1)
}
}
],
menuCommands: [
{
path: ['ext', 'ext2'],
commands: ['foo-id']
}
]
})
```
![image](https://github.com/user-attachments/assets/ae7b082f-7ce9-4549-a446-4563567102fe)
</details>
<details id='extension-api-toast'>
<summary>v1.2.27: Extension API to add toast message</summary>i
Extensions can call the following API to add toast messages.
```js
app.extensionManager.toast.add({
severity: 'info',
summary: 'Loaded!',
detail: 'Extension loaded!',
life: 3000
})
```
Documentation of all supported options can be found here: <https://primevue.org/toast/#api.toast.interfaces.ToastMessageOptions>
![image](https://github.com/user-attachments/assets/de02cd7e-cd81-43d1-a0b0-bccef92ff487)
</details>
<details id='extension-api-sidebar-tab'>
<summary>v1.2.4: Extension API to register custom sidebar tab</summary>
Extensions now can call the following API to register a sidebar tab.
```js
app.extensionManager.registerSidebarTab({
id: "search",
icon: "pi pi-search",
title: "search",
tooltip: "search",
type: "custom",
render: (el) => {
el.innerHTML = "<div>Custom search tab</div>";
},
});
```
The list of supported icons can be found here: <https://primevue.org/icons/#list>
We will support custom icons later.
![image](https://github.com/user-attachments/assets/7bff028a-bf91-4cab-bf97-55c243b3f5e0)
</details>
<details id='extension-api-selection-toolbox'>
<summary>v1.10.9: Selection Toolbox API</summary>
Extensions can register commands that appear in the selection toolbox when specific items are selected on the canvas.
```js
app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'test.selection.command',
label: 'Test Command',
icon: 'pi pi-star',
function: () => {
// Command logic here
}
}
],
// Return an array of command IDs to show in the selection toolbox
// when an item is selected
getSelectionToolboxCommands: (selectedItem) => ['test.selection.command']
})
```
The selection toolbox will display the command button when items are selected:
![Image](https://github.com/user-attachments/assets/28d91267-c0a9-4bd5-a7c4-36e8ec44c9bd)
</details>
## Contributing
We're building this frontend together and would love your help — no matter how you'd like to pitch in! You don't need to write code to make a difference.

View File

@@ -32,6 +32,7 @@
@background-image-change="listenBackgroundImageChange"
@animation-list-change="animationListChange"
@up-direction-change="listenUpDirectionChange"
@recording-status-change="listenRecordingStatusChange"
/>
<div class="absolute top-0 left-0 w-full h-full pointer-events-none">
<Load3DControls
@@ -66,6 +67,21 @@
@animation-change="animationChange"
/>
</div>
<div
v-if="showRecordingControls"
class="absolute top-12 right-2 z-20 pointer-events-auto"
>
<RecordingControls
:node="node"
:is-recording="isRecording"
:has-recording="hasRecording"
:recording-duration="recordingDuration"
@start-recording="handleStartRecording"
@stop-recording="handleStopRecording"
@export-recording="handleExportRecording"
@clear-recording="handleClearRecording"
/>
</div>
</div>
</template>
@@ -75,6 +91,7 @@ import { computed, ref } from 'vue'
import Load3DAnimationControls from '@/components/load3d/Load3DAnimationControls.vue'
import Load3DAnimationScene from '@/components/load3d/Load3DAnimationScene.vue'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import {
AnimationItem,
@@ -111,6 +128,11 @@ const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const backgroundImage = ref('')
const isRecording = ref(false)
const hasRecording = ref(false)
const recordingDuration = ref(0)
const showRecordingControls = ref(!inputSpec.isPreview)
const showPreviewButton = computed(() => {
return !type.includes('Preview')
})
@@ -133,6 +155,54 @@ const handleMouseLeave = () => {
}
}
const handleStartRecording = async () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
await sceneRef.load3d.startRecording()
isRecording.value = true
}
}
const handleStopRecording = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
sceneRef.load3d.stopRecording()
isRecording.value = false
hasRecording.value = true
recordingDuration.value = sceneRef.load3d.getRecordingDuration()
}
}
const handleExportRecording = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const filename = `${timestamp}-animation-recording.mp4`
sceneRef.load3d.exportRecording(filename)
}
}
const handleClearRecording = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
sceneRef.load3d.clearRecording()
hasRecording.value = false
recordingDuration.value = 0
}
}
const listenRecordingStatusChange = (value: boolean) => {
isRecording.value = value
if (!value) {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
hasRecording.value = true
recordingDuration.value = sceneRef.load3d.getRecordingDuration()
}
}
}
const switchCamera = () => {
cameraType.value =
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'

View File

@@ -20,6 +20,7 @@
@camera-type-change="listenCameraTypeChange"
@show-grid-change="listenShowGridChange"
@show-preview-change="listenShowPreviewChange"
@recording-status-change="listenRecordingStatusChange"
/>
</template>
<script setup lang="ts">
@@ -148,6 +149,15 @@ watch(
const emit = defineEmits<{
(e: 'animationListChange', animationList: string): void
(e: 'materialModeChange', materialMode: string): void
(e: 'backgroundColorChange', color: string): void
(e: 'lightIntensityChange', lightIntensity: number): void
(e: 'fovChange', fov: number): void
(e: 'cameraTypeChange', cameraType: string): void
(e: 'showGridChange', showGrid: boolean): void
(e: 'showPreviewChange', showPreview: boolean): void
(e: 'upDirectionChange', direction: string): void
(e: 'recording-status-change', status: boolean): void
}>()
const listenMaterialModeChange = (mode: MaterialMode) => {
@@ -182,6 +192,10 @@ const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
const listenRecordingStatusChange = (value: boolean) => {
emit('recording-status-change', value)
}
const animationListeners = {
animationListChange: (newValue: any) => {
emit('animationListChange', newValue)

View File

@@ -75,7 +75,7 @@
</template>
<script setup lang="ts">
import { IWidget, LGraphNode } from '@comfyorg/litegraph'
import { LGraphNode } from '@comfyorg/litegraph'
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
@@ -101,13 +101,13 @@ const emit = defineEmits<{
const resizeNodeMatchOutput = () => {
console.log('resizeNodeMatchOutput')
const outputWidth = node.widgets?.find((w: IWidget) => w.name === 'width')
const outputHeight = node.widgets?.find((w: IWidget) => w.name === 'height')
const outputWidth = node.widgets?.find((w) => w.name === 'width')
const outputHeight = node.widgets?.find((w) => w.name === 'height')
if (outputWidth && outputHeight && outputHeight.value && outputWidth.value) {
const [oldWidth, oldHeight] = node.size
const scene = node.widgets?.find((w: IWidget) => w.name === 'image')
const scene = node.widgets?.find((w) => w.name === 'image')
const sceneHeight = scene?.computedHeight

View File

@@ -7,15 +7,55 @@ import { fixBadLinks } from '@/utils/linkFixer'
export interface ValidationResult {
graphData: ComfyWorkflowJSON | null
linksFixes?: {
patched: number
deleted: number
}
}
export function useWorkflowValidation() {
const toastStore = useToastStore()
function tryFixLinks(
graphData: ComfyWorkflowJSON,
options: { silent?: boolean } = {}
) {
const { silent = false } = options
// Collect all logs in an array
const logs: string[] = []
// Then validate and fix links if schema validation passed
const linkValidation = fixBadLinks(
graphData as unknown as ISerialisedGraph,
{
fix: true,
silent,
logger: {
log: (message: string) => {
logs.push(message)
}
}
}
)
if (!silent && logs.length > 0) {
toastStore.add({
severity: 'warn',
summary: 'Workflow Validation',
detail: logs.join('\n')
})
}
// If links were fixed, notify the user
if (linkValidation.fixed) {
if (!silent) {
toastStore.add({
severity: 'success',
summary: 'Workflow Links Fixed',
detail: `Fixed ${linkValidation.patched} node connections and removed ${linkValidation.deleted} invalid links.`
})
}
}
return linkValidation.graph as unknown as ComfyWorkflowJSON
}
/**
* Validates a workflow, including link validation and schema validation
*/
@@ -27,7 +67,6 @@ export function useWorkflowValidation() {
): Promise<ValidationResult> {
const { silent = false } = options
let linksFixes
let validatedData: ComfyWorkflowJSON | null = null
// First do schema validation
@@ -41,51 +80,16 @@ export function useWorkflowValidation() {
)
if (validatedGraphData) {
// Collect all logs in an array
const logs: string[] = []
// Then validate and fix links if schema validation passed
const linkValidation = fixBadLinks(
validatedGraphData as unknown as ISerialisedGraph,
{
fix: true,
silent,
logger: {
log: (message: string) => {
logs.push(message)
}
}
}
)
if (!silent && logs.length > 0) {
toastStore.add({
severity: 'warn',
summary: 'Workflow Validation',
detail: logs.join('\n')
})
}
// If links were fixed, notify the user
if (linkValidation.fixed) {
if (!silent) {
toastStore.add({
severity: 'success',
summary: 'Workflow Links Fixed',
detail: `Fixed ${linkValidation.patched} node connections and removed ${linkValidation.deleted} invalid links.`
})
}
}
validatedData = linkValidation.graph as unknown as ComfyWorkflowJSON
linksFixes = {
patched: linkValidation.patched,
deleted: linkValidation.deleted
try {
validatedData = tryFixLinks(validatedGraphData, { silent })
} catch (err) {
// Link fixer itself is throwing an error
console.error(err)
}
}
return {
graphData: validatedData,
linksFixes
graphData: validatedData
}
}

View File

@@ -50,7 +50,8 @@ export const useFloatWidget = () => {
Math.max(0, -Math.floor(Math.log10(step)))
const enableRounding = !settingStore.get('Comfy.DisableFloatRounding')
const defaultValue = inputSpec.default ?? 0
/** Assertion {@link inputSpec.default} */
const defaultValue = (inputSpec.default as number | undefined) ?? 0
return node.addWidget(
widgetType,
inputSpec.name,

View File

@@ -55,7 +55,8 @@ export const useIntWidget = () => {
: 'number'
const step = inputSpec.step ?? 1
const defaultValue = inputSpec.default ?? 0
/** Assertion {@link inputSpec.default} */
const defaultValue = (inputSpec.default as number | undefined) ?? 0
const widget = node.addWidget(
widgetType,
inputSpec.name,

View File

@@ -36,7 +36,7 @@ const ext = {
w.type === 'combo' && w.options.values?.length === values.length
)
.find((w) =>
// @ts-ignore Poorly typed; filter above "should" mitigate exceptions
// @ts-expect-error Poorly typed; filter above "should" mitigate exceptions
w.options.values?.every((v, i) => v === values[i])
)?.value

View File

@@ -1,5 +1,4 @@
import { IWidget } from '@comfyorg/litegraph'
import { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
import type { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
@@ -116,7 +115,7 @@ useExtensionService().registerExtension({
fileInput.onchange = async () => {
if (fileInput.files?.length) {
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
(w) => w.name === 'model_file'
) as IStringWidget
node.properties['Texture'] = undefined
@@ -139,7 +138,7 @@ useExtensionService().registerExtension({
if (uploadPath && modelWidget) {
if (!modelWidget.options?.values?.includes(uploadPath)) {
// @ts-ignore Fails due to earlier type-assertion of IStringWidget
// @ts-expect-error Fails due to earlier type-assertion of IStringWidget
modelWidget.options?.values?.push(uploadPath)
}
@@ -155,9 +154,7 @@ useExtensionService().registerExtension({
node.addWidget('button', 'clear', 'clear', () => {
useLoad3dService().getLoad3d(node)?.clearModel()
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
if (modelWidget) {
modelWidget.value = ''
@@ -203,12 +200,10 @@ useExtensionService().registerExtension({
const config = new Load3DConfiguration(load3d)
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
const width = node.widgets?.find((w: IWidget) => w.name === 'width')
const height = node.widgets?.find((w: IWidget) => w.name === 'height')
const sceneWidget = node.widgets?.find((w: IWidget) => w.name === 'image')
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
const width = node.widgets?.find((w) => w.name === 'width')
const height = node.widgets?.find((w) => w.name === 'height')
const sceneWidget = node.widgets?.find((w) => w.name === 'image')
if (modelWidget && width && height && sceneWidget) {
config.configure('input', modelWidget, cameraState, width, height)
@@ -276,7 +271,7 @@ useExtensionService().registerExtension({
fileInput.onchange = async () => {
if (fileInput.files?.length) {
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
(w) => w.name === 'model_file'
) as IStringWidget
const uploadPath = await Load3dUtils.uploadFile(
@@ -297,7 +292,7 @@ useExtensionService().registerExtension({
if (uploadPath && modelWidget) {
if (!modelWidget.options?.values?.includes(uploadPath)) {
// @ts-ignore Fails due to earlier type-assertion of IStringWidget
// @ts-expect-error Fails due to earlier type-assertion of IStringWidget
modelWidget.options?.values?.push(uploadPath)
}
@@ -313,9 +308,7 @@ useExtensionService().registerExtension({
node.addWidget('button', 'clear', 'clear', () => {
useLoad3dService().getLoad3d(node)?.clearModel()
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
if (modelWidget) {
modelWidget.value = ''
}
@@ -352,20 +345,18 @@ useExtensionService().registerExtension({
await nextTick()
const sceneWidget = node.widgets?.find((w: IWidget) => w.name === 'image')
const sceneWidget = node.widgets?.find((w) => w.name === 'image')
const load3d = useLoad3dService().getLoad3d(node) as Load3dAnimation
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
let cameraState = node.properties['Camera Info']
const width = node.widgets?.find((w: IWidget) => w.name === 'width')
const height = node.widgets?.find((w: IWidget) => w.name === 'height')
const width = node.widgets?.find((w) => w.name === 'width')
const height = node.widgets?.find((w) => w.name === 'height')
if (modelWidget && width && height && sceneWidget) {
if (modelWidget && width && height && sceneWidget && load3d) {
const config = new Load3DConfiguration(load3d)
config.configure('input', modelWidget, cameraState, width, height)
@@ -375,6 +366,10 @@ useExtensionService().registerExtension({
load3d.toggleAnimation(false)
if (load3d.isRecording()) {
load3d.stopRecording()
}
const {
scene: imageData,
mask: maskData,
@@ -392,12 +387,23 @@ useExtensionService().registerExtension({
load3d.handleResize()
return {
const returnVal = {
image: `threed/${data.name} [temp]`,
mask: `threed/${dataMask.name} [temp]`,
normal: `threed/${dataNormal.name} [temp]`,
camera_info: node.properties['Camera Info']
camera_info: node.properties['Camera Info'],
recording: ''
}
const recordingData = load3d.getRecordingData()
if (recordingData) {
const [recording] = await Promise.all([
Load3dUtils.uploadTempImage(recordingData, 'recording', 'mp4')
])
returnVal['recording'] = `threed/${recording.name} [temp]`
}
return returnVal
}
}
}
@@ -464,9 +470,7 @@ useExtensionService().registerExtension({
let cameraState = message.result[1]
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
if (load3d && modelWidget) {
modelWidget.value = filePath.replaceAll('\\', '/')
@@ -540,9 +544,7 @@ useExtensionService().registerExtension({
const load3d = useLoad3dService().getLoad3d(node)
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
if (load3d && modelWidget) {
modelWidget.value = filePath.replaceAll('\\', '/')

View File

@@ -229,6 +229,7 @@ class Load3d {
return (
this.STATUS_MOUSE_ON_NODE ||
this.STATUS_MOUSE_ON_SCENE ||
this.isRecording() ||
!this.INITIAL_RENDER_DONE
)
}
@@ -461,7 +462,7 @@ class Load3d {
}
public isRecording(): boolean {
return this.recordingManager.hasRecording()
return this.recordingManager.getIsRecording()
}
public getRecordingDuration(): number {

View File

@@ -106,7 +106,7 @@ export class RecordingManager {
return
}
this.recordingDuration = (Date.now() - this.recordingStartTime) / 1000 // In seconds
this.recordingDuration = (Date.now() - this.recordingStartTime) / 1000
this.mediaRecorder.stop()
if (this.recordingStream) {
@@ -114,6 +114,10 @@ export class RecordingManager {
}
}
public getIsRecording(): boolean {
return this.isRecording
}
public hasRecording(): boolean {
return this.recordedChunks.length > 0
}

View File

@@ -3,8 +3,6 @@ Preview Any - original implement from
https://github.com/rgthree/rgthree-comfy/blob/main/py/display_any.py
upstream requested in https://github.com/Kosinkadink/rfcs/blob/main/rfcs/0000-corenodes.md#preview-nodes
*/
import { IWidget } from '@comfyorg/litegraph'
import { DOMWidget } from '@/scripts/domWidget'
import { ComfyWidgets } from '@/scripts/widgets'
import { useExtensionService } from '@/services/extensionService'
@@ -37,9 +35,7 @@ useExtensionService().registerExtension({
? void 0
: onExecuted.apply(this, [message])
const previewWidget = this.widgets?.find(
(w: IWidget) => w.name === 'preview'
)
const previewWidget = this.widgets?.find((w) => w.name === 'preview')
if (previewWidget) {
previewWidget.value = message.text[0]

View File

@@ -1,4 +1,3 @@
import { IWidget } from '@comfyorg/litegraph'
import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
@@ -61,7 +60,7 @@ useExtensionService().registerExtension({
const load3d = useLoad3dService().getLoad3d(node)
const modelWidget = node.widgets?.find((w: IWidget) => w.name === 'image')
const modelWidget = node.widgets?.find((w) => w.name === 'image')
if (load3d && modelWidget) {
const filePath = fileInfo['subfolder'] + '/' + fileInfo['filename']

View File

@@ -1,4 +1,4 @@
import type { IWidget, LGraphNode } from '@comfyorg/litegraph'
import type { LGraphNode } from '@comfyorg/litegraph'
import type { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
@@ -164,11 +164,11 @@ app.registerExtension({
// The widget that allows user to select file.
// @ts-expect-error fixme ts strict error
const audioWidget = node.widgets.find(
(w: IWidget) => w.name === 'audio'
(w) => w.name === 'audio'
) as IStringWidget
// @ts-expect-error fixme ts strict error
const audioUIWidget = node.widgets.find(
(w: IWidget) => w.name === 'audioUI'
(w) => w.name === 'audioUI'
) as unknown as DOMWidget<HTMLAudioElement, string>
const onAudioWidgetUpdate = () => {

View File

@@ -36,7 +36,7 @@ export const zNumericInputOptions = zBaseInputOptions.extend({
min: z.number().optional(),
max: z.number().optional(),
step: z.number().optional(),
// Note: Many node authors are using INT/FLOAT to pass list of INT/FLOAT.
/** Note: Many node authors are using INT/FLOAT to pass list of INT/FLOAT. */
default: z.union([z.number(), z.array(z.number())]).optional(),
display: z.enum(['slider', 'number', 'knob']).optional()
})

View File

@@ -78,7 +78,7 @@ export function addValueControlWidget(
_values?: unknown,
widgetName?: string,
inputData?: InputSpec
): IWidget {
): IComboWidget {
let name = inputData?.[1]?.control_after_generate
if (typeof name !== 'string') {
name = widgetName
@@ -102,7 +102,7 @@ export function addValueControlWidgets(
defaultValue?: string,
options?: Record<string, any>,
inputData?: InputSpec
): IWidget[] {
): [IComboWidget, ...IStringWidget[]] {
if (!defaultValue) defaultValue = 'randomize'
if (!options) options = {}
@@ -118,7 +118,6 @@ export function addValueControlWidgets(
return name
}
const widgets: IWidget[] = []
const valueControl = node.addWidget(
'combo',
getName('control_after_generate', 'controlAfterGenerateName'),
@@ -135,12 +134,12 @@ export function addValueControlWidgets(
// @ts-ignore index with symbol
valueControl[IS_CONTROL_WIDGET] = true
updateControlWidgetLabel(valueControl)
widgets.push(valueControl)
const widgets: [IComboWidget, ...IStringWidget[]] = [valueControl]
const isCombo = targetWidget.type === 'combo'
let comboFilter: IStringWidget
if (isCombo && valueControl.options.values) {
// @ts-ignore Combo widget values may be a dictionary or legacy function type
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
valueControl.options.values.push('increment-wrap')
}
if (isCombo && options.addFilterList !== false) {
@@ -184,7 +183,7 @@ export function addValueControlWidgets(
const lower = filter.toLocaleLowerCase()
check = (item: string) => item.toLocaleLowerCase().includes(lower)
}
// @ts-ignore Combo widget values may be a dictionary or legacy function type
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
values = values.filter((item: string) => check(item))
if (!values.length && targetWidget.options.values?.length) {
console.warn(
@@ -211,17 +210,17 @@ export function addValueControlWidgets(
current_index -= 1
break
case 'randomize':
// @ts-ignore Combo widget values may be a dictionary or legacy function type
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
current_index = Math.floor(Math.random() * current_length)
break
default:
break
}
current_index = Math.max(0, current_index)
// @ts-ignore Combo widget values may be a dictionary or legacy function type
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
current_index = Math.min(current_length - 1, current_index)
if (current_index >= 0) {
// @ts-ignore Combo widget values may be a dictionary or legacy function type
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
let value = values[current_index]
targetWidget.value = value
targetWidget.callback?.(value)

View File

@@ -60,6 +60,9 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
prompt: 'select_account'
})
const githubProvider = new GithubAuthProvider()
githubProvider.setCustomParameters({
prompt: 'select_account'
})
// Getters
const isAuthenticated = computed(() => !!currentUser.value)

File diff suppressed because it is too large Load Diff

View File

@@ -35,10 +35,7 @@ declare module '@comfyorg/litegraph/dist/types/widgets' {
onRemove?: () => void
beforeQueued?: () => unknown
afterQueued?: () => unknown
serializeValue?: (
node: LGraphNode,
index: number
) => Promise<unknown> | unknown
serializeValue?(node: LGraphNode, index: number): Promise<unknown> | unknown
/**
* Refreshes the widget's value or options from its remote source.
@@ -66,6 +63,14 @@ declare module '@comfyorg/litegraph' {
new (): T
}
interface TextWidget {
dynamicPrompts?: boolean
}
interface BaseWidget {
serializeValue?(node: LGraphNode, index: number): Promise<unknown> | unknown
}
interface LGraphNode {
constructor: LGraphNodeConstructor

View File

@@ -35,11 +35,9 @@ export function isAudioNode(node: LGraphNode | undefined): boolean {
export function addToComboValues(widget: IComboWidget, value: string) {
if (!widget.options) widget.options = { values: [] }
if (!widget.options.values) widget.options.values = []
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Combo widget values may be a dictionary or legacy function type
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
if (!widget.options.values.includes(value)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Combo widget values may be a dictionary or legacy function type
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
widget.options.values.push(value)
}
}

View File

@@ -55,7 +55,9 @@ vi.mock('firebase/auth', () => ({
GoogleAuthProvider: class {
setCustomParameters = vi.fn()
},
GithubAuthProvider: vi.fn(),
GithubAuthProvider: class {
setCustomParameters = vi.fn()
},
browserLocalPersistence: 'browserLocalPersistence',
setPersistence: vi.fn().mockResolvedValue(undefined)
}))