mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-06 06:01:58 +00:00
Compare commits
1 Commits
main
...
refactor/l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
207487eca2 |
@@ -1,42 +0,0 @@
|
||||
import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Preview as Text node', () => {
|
||||
test('does not include preview widget values in the API prompt', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window.LiteGraph!.createNode('PreviewAny')!
|
||||
node.pos = [500, 200]
|
||||
window.app!.graph.add(node)
|
||||
})
|
||||
|
||||
// Simulate a previous execution: backend returned text and the frontend
|
||||
// populated the preview widget values. The next prompt submission must
|
||||
// NOT echo those values back as inputs (which would change the cache
|
||||
// signature and trigger a redundant re-execution).
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window.app!.graph.nodes.find((n) => n.type === 'PreviewAny')!
|
||||
for (const widget of node.widgets ?? []) {
|
||||
if (widget.name?.startsWith('preview_')) {
|
||||
widget.value = 'rendered preview content from previous execution'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const apiWorkflow = await comfyPage.workflow.getExportedWorkflow({
|
||||
api: true
|
||||
})
|
||||
|
||||
const previewEntry = Object.values(apiWorkflow).find(
|
||||
(n) => n.class_type === 'PreviewAny'
|
||||
)
|
||||
expect(previewEntry).toBeDefined()
|
||||
|
||||
expect(previewEntry!.inputs).not.toHaveProperty('preview_markdown')
|
||||
expect(previewEntry!.inputs).not.toHaveProperty('preview_text')
|
||||
expect(previewEntry!.inputs).not.toHaveProperty('previewMode')
|
||||
})
|
||||
})
|
||||
@@ -21,20 +21,40 @@
|
||||
</Button>
|
||||
|
||||
<Select
|
||||
v-model="selectedSpeed"
|
||||
:options="speedOptions"
|
||||
option-label="name"
|
||||
option-value="value"
|
||||
class="w-24"
|
||||
/>
|
||||
:model-value="String(selectedSpeed)"
|
||||
@update:model-value="(val) => (selectedSpeed = Number(val))"
|
||||
>
|
||||
<SelectTrigger size="md" class="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="opt in speedOptions"
|
||||
:key="opt.value"
|
||||
:value="String(opt.value)"
|
||||
>
|
||||
{{ opt.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
v-model="selectedAnimation"
|
||||
:options="animations"
|
||||
option-label="name"
|
||||
option-value="index"
|
||||
class="w-32"
|
||||
/>
|
||||
:model-value="String(selectedAnimation)"
|
||||
@update:model-value="(val) => (selectedAnimation = Number(val))"
|
||||
>
|
||||
<SelectTrigger size="md" class="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="anim in animations"
|
||||
:key="anim.index"
|
||||
:value="String(anim.index)"
|
||||
>
|
||||
{{ anim.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full max-w-xs items-center gap-2 px-4">
|
||||
@@ -54,10 +74,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Select from '@/components/ui/select/Select.vue'
|
||||
import SelectContent from '@/components/ui/select/SelectContent.vue'
|
||||
import SelectItem from '@/components/ui/select/SelectItem.vue'
|
||||
import SelectTrigger from '@/components/ui/select/SelectTrigger.vue'
|
||||
import SelectValue from '@/components/ui/select/SelectValue.vue'
|
||||
import Slider from '@/components/ui/slider/Slider.vue'
|
||||
|
||||
type Animation = { name: string; index: number }
|
||||
|
||||
@@ -15,21 +15,22 @@
|
||||
class="absolute top-0 left-12 w-[150px] rounded-lg bg-interface-menu-surface p-4 shadow-lg"
|
||||
>
|
||||
<Slider
|
||||
v-model="value"
|
||||
:model-value="sliderValue"
|
||||
class="w-full"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
@update:model-value="onSliderUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Slider from 'primevue/slider'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Slider from '@/components/ui/slider/Slider.vue'
|
||||
|
||||
const {
|
||||
icon = 'pi-expand',
|
||||
@@ -47,6 +48,12 @@ const {
|
||||
const value = defineModel<number>()
|
||||
const showSlider = ref(false)
|
||||
|
||||
const sliderValue = computed(() => [value.value ?? min])
|
||||
|
||||
function onSliderUpdate(val: number[] | undefined) {
|
||||
if (val?.length) value.value = val[0]
|
||||
}
|
||||
|
||||
const toggleSlider = () => {
|
||||
showSlider.value = !showSlider.value
|
||||
}
|
||||
|
||||
@@ -2,34 +2,46 @@
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label>{{ t('load3d.viewer.cameraType') }}</label>
|
||||
<Select
|
||||
v-model="cameraType"
|
||||
:options="cameras"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
>
|
||||
<Select v-model="cameraType">
|
||||
<SelectTrigger size="md">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="cam in cameras"
|
||||
:key="cam.value"
|
||||
:value="cam.value"
|
||||
>
|
||||
{{ cam.title }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div v-if="showFOVButton" class="flex flex-col gap-2">
|
||||
<label>{{ t('load3d.fov') }}</label>
|
||||
<Slider
|
||||
v-model="fov"
|
||||
:model-value="fovSliderValue"
|
||||
:min="10"
|
||||
:max="150"
|
||||
:step="1"
|
||||
:aria-label="t('load3d.fov')"
|
||||
@update:model-value="onFovUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select'
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Select from '@/components/ui/select/Select.vue'
|
||||
import SelectContent from '@/components/ui/select/SelectContent.vue'
|
||||
import SelectItem from '@/components/ui/select/SelectItem.vue'
|
||||
import SelectTrigger from '@/components/ui/select/SelectTrigger.vue'
|
||||
import SelectValue from '@/components/ui/select/SelectValue.vue'
|
||||
import Slider from '@/components/ui/slider/Slider.vue'
|
||||
import type { CameraType } from '@/extensions/core/load3d/interfaces'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -41,4 +53,10 @@ const cameras = [
|
||||
const cameraType = defineModel<CameraType>('cameraType')
|
||||
const fov = defineModel<number>('fov')
|
||||
const showFOVButton = computed(() => cameraType.value === 'perspective')
|
||||
|
||||
const fovSliderValue = computed(() => [fov.value ?? 10])
|
||||
|
||||
function onFovUpdate(val: number[] | undefined) {
|
||||
if (val?.length) fov.value = val[0]
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
v-model="exportFormat"
|
||||
:options="exportFormats"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
>
|
||||
<Select v-model="exportFormat">
|
||||
<SelectTrigger size="md">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="fmt in exportFormats"
|
||||
:key="fmt.value"
|
||||
:value="fmt.value"
|
||||
>
|
||||
{{ fmt.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
@@ -19,10 +26,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Select from '@/components/ui/select/Select.vue'
|
||||
import SelectContent from '@/components/ui/select/SelectContent.vue'
|
||||
import SelectItem from '@/components/ui/select/SelectItem.vue'
|
||||
import SelectTrigger from '@/components/ui/select/SelectTrigger.vue'
|
||||
import SelectValue from '@/components/ui/select/SelectValue.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'exportModel', format: string): void
|
||||
|
||||
@@ -3,18 +3,20 @@
|
||||
<label>{{ $t('load3d.lightIntensity') }}</label>
|
||||
|
||||
<Slider
|
||||
v-model="lightIntensity"
|
||||
:model-value="sliderValue"
|
||||
class="w-full"
|
||||
:min="lightIntensityMinimum"
|
||||
:max="lightIntensityMaximum"
|
||||
:step="lightAdjustmentIncrement"
|
||||
@update:model-value="onSliderUpdate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Slider from '@/components/ui/slider/Slider.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
const lightIntensity = defineModel<number>('lightIntensity')
|
||||
@@ -28,4 +30,12 @@ const lightIntensityMinimum = useSettingStore().get(
|
||||
const lightAdjustmentIncrement = useSettingStore().get(
|
||||
'Comfy.Load3D.LightAdjustmentIncrement'
|
||||
)
|
||||
|
||||
const sliderValue = computed(() => [
|
||||
lightIntensity.value ?? lightIntensityMinimum
|
||||
])
|
||||
|
||||
function onSliderUpdate(val: number[] | undefined) {
|
||||
if (val?.length) lightIntensity.value = val[0]
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,31 +2,51 @@
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label>{{ $t('load3d.upDirection') }}</label>
|
||||
<Select
|
||||
v-model="upDirection"
|
||||
:options="upDirectionOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
/>
|
||||
<Select v-model="upDirection">
|
||||
<SelectTrigger size="md">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="opt in upDirectionOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div v-if="materialModes.length > 0" class="flex flex-col gap-2">
|
||||
<label>{{ $t('load3d.materialMode') }}</label>
|
||||
<Select
|
||||
v-model="materialMode"
|
||||
:options="materialModeOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
/>
|
||||
<Select v-model="materialMode">
|
||||
<SelectTrigger size="md">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="opt in materialModeOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Select from '@/components/ui/select/Select.vue'
|
||||
import SelectContent from '@/components/ui/select/SelectContent.vue'
|
||||
import SelectItem from '@/components/ui/select/SelectItem.vue'
|
||||
import SelectTrigger from '@/components/ui/select/SelectTrigger.vue'
|
||||
import SelectValue from '@/components/ui/select/SelectValue.vue'
|
||||
import type {
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
|
||||
@@ -7,9 +7,15 @@
|
||||
<input v-model="backgroundColor" type="color" class="h-8 w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Checkbox v-model="showGrid" input-id="showGrid" binary name="showGrid" />
|
||||
<label for="showGrid" class="pl-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="showGrid"
|
||||
v-model="showGrid"
|
||||
type="checkbox"
|
||||
name="showGrid"
|
||||
class="size-4 cursor-pointer accent-node-component-surface-highlight"
|
||||
/>
|
||||
<label for="showGrid" class="cursor-pointer">
|
||||
{{ $t('load3d.showGrid') }}
|
||||
</label>
|
||||
</div>
|
||||
@@ -58,7 +64,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
|
||||
const capturedExtensions: ComfyExtension[] = []
|
||||
|
||||
vi.mock('@/services/extensionService', () => ({
|
||||
useExtensionService: () => ({
|
||||
registerExtension: (ext: ComfyExtension) => {
|
||||
capturedExtensions.push(ext)
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({ app: {} }))
|
||||
|
||||
interface MockWidget {
|
||||
name: string
|
||||
options: Record<string, unknown>
|
||||
element: { readOnly: boolean }
|
||||
callback?: (value: unknown) => void
|
||||
value: unknown
|
||||
hidden: boolean
|
||||
label: string
|
||||
serialize?: boolean
|
||||
}
|
||||
|
||||
const createdWidgets: MockWidget[] = []
|
||||
|
||||
vi.mock('@/scripts/widgets', () => {
|
||||
const create =
|
||||
(kind: string) =>
|
||||
(
|
||||
node: { widgets?: MockWidget[] },
|
||||
name: string,
|
||||
_info: unknown,
|
||||
_app: unknown
|
||||
) => {
|
||||
const widget: MockWidget = {
|
||||
name,
|
||||
options: {},
|
||||
element: { readOnly: false },
|
||||
value: kind === 'BOOLEAN' ? false : '',
|
||||
hidden: false,
|
||||
label: ''
|
||||
}
|
||||
node.widgets = node.widgets ?? []
|
||||
node.widgets.push(widget)
|
||||
createdWidgets.push(widget)
|
||||
return { widget }
|
||||
}
|
||||
return {
|
||||
ComfyWidgets: {
|
||||
MARKDOWN: create('MARKDOWN'),
|
||||
STRING: create('STRING'),
|
||||
BOOLEAN: create('BOOLEAN')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('PreviewAny extension', () => {
|
||||
beforeEach(async () => {
|
||||
capturedExtensions.length = 0
|
||||
createdWidgets.length = 0
|
||||
vi.resetModules()
|
||||
await import('./previewAny')
|
||||
})
|
||||
|
||||
async function setupNode() {
|
||||
const ext = capturedExtensions.find((e) => e.name === 'Comfy.PreviewAny')
|
||||
expect(ext).toBeDefined()
|
||||
|
||||
const nodeType = { prototype: {} } as unknown as Parameters<
|
||||
NonNullable<ComfyExtension['beforeRegisterNodeDef']>
|
||||
>[0]
|
||||
const nodeData = { name: 'PreviewAny' } as Parameters<
|
||||
NonNullable<ComfyExtension['beforeRegisterNodeDef']>
|
||||
>[1]
|
||||
|
||||
await ext!.beforeRegisterNodeDef!(
|
||||
nodeType,
|
||||
nodeData,
|
||||
{} as Parameters<NonNullable<ComfyExtension['beforeRegisterNodeDef']>>[2]
|
||||
)
|
||||
|
||||
const node: { widgets?: MockWidget[] } = {}
|
||||
const proto = nodeType.prototype as { onNodeCreated?: () => void }
|
||||
proto.onNodeCreated!.call(node)
|
||||
return node
|
||||
}
|
||||
|
||||
it('excludes preview widgets from the API prompt to prevent re-execution', async () => {
|
||||
await setupNode()
|
||||
|
||||
const previewMarkdown = createdWidgets.find(
|
||||
(w) => w.name === 'preview_markdown'
|
||||
)
|
||||
const previewText = createdWidgets.find((w) => w.name === 'preview_text')
|
||||
const previewMode = createdWidgets.find((w) => w.name === 'previewMode')
|
||||
|
||||
expect(previewMarkdown).toBeDefined()
|
||||
expect(previewText).toBeDefined()
|
||||
expect(previewMode).toBeDefined()
|
||||
|
||||
// widget.options.serialize === false is what executionUtil.graphToPrompt
|
||||
// checks to exclude a widget from the API prompt sent to the backend.
|
||||
// Without this, post-execution widget value updates (the rendered preview
|
||||
// text) get serialized as inputs, change the cache signature, and cause
|
||||
// the node to re-execute on the next prompt.
|
||||
expect(previewMarkdown!.options.serialize).toBe(false)
|
||||
expect(previewText!.options.serialize).toBe(false)
|
||||
expect(previewMode!.options.serialize).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -57,7 +57,6 @@ useExtensionService().registerExtension({
|
||||
showValueWidget.hidden = true
|
||||
showValueWidget.options.hidden = true
|
||||
showValueWidget.options.read_only = true
|
||||
showValueWidget.options.serialize = false
|
||||
showValueWidget.element.readOnly = true
|
||||
showValueWidget.serialize = false
|
||||
|
||||
@@ -65,14 +64,8 @@ useExtensionService().registerExtension({
|
||||
showValueWidgetPlain.hidden = false
|
||||
showValueWidgetPlain.options.hidden = false
|
||||
showValueWidgetPlain.options.read_only = true
|
||||
showValueWidgetPlain.options.serialize = false
|
||||
showValueWidgetPlain.element.readOnly = true
|
||||
showValueWidgetPlain.serialize = false
|
||||
|
||||
// The previewMode toggle is a frontend-only display preference and
|
||||
// is not declared in the backend INPUT_TYPES, so it must not be
|
||||
// serialized into the API prompt (would alter the cache signature).
|
||||
showAsPlaintextWidget.widget.options.serialize = false
|
||||
}
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
|
||||
Reference in New Issue
Block a user