mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-10 14:29:59 +00:00
Compare commits
3 Commits
main
...
test/previ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0986db81a | ||
|
|
ea3c289a07 | ||
|
|
fa7477b8c4 |
88
browser_tests/assets/3d/preview3d_pipeline.json
Normal file
88
browser_tests/assets/3d/preview3d_pipeline.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "Load3D",
|
||||
"pos": [50, 50],
|
||||
"size": [400, 650],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "mesh_path",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "normal",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "camera_info",
|
||||
"type": "LOAD3D_CAMERA",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "recording_video",
|
||||
"type": "VIDEO",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "model_3d",
|
||||
"type": "FILE_3D",
|
||||
"links": [1]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Load3D"
|
||||
},
|
||||
"widgets_values": ["", 1024, 1024, "#000000"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "Preview3D",
|
||||
"pos": [520, 50],
|
||||
"size": [450, 600],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model_file",
|
||||
"type": "FILE_3D",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "Preview3D"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [[1, 1, 6, 2, 0, "*"]],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
121
browser_tests/fixtures/helpers/Preview3DPipelineFixture.ts
Normal file
121
browser_tests/fixtures/helpers/Preview3DPipelineFixture.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { Load3DHelper } from '@e2e/tests/load3d/Load3DHelper'
|
||||
|
||||
export async function waitForAppBootstrapped(
|
||||
comfyPage: ComfyPage
|
||||
): Promise<void> {
|
||||
await comfyPage.page.waitForFunction(() =>
|
||||
Boolean(window.app?.extensionManager)
|
||||
)
|
||||
await comfyPage.page.waitForSelector('.p-blockui-mask', { state: 'hidden' })
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
export function vecClose(
|
||||
a: { x: number; y: number; z: number },
|
||||
b: { x: number; y: number; z: number },
|
||||
eps: number
|
||||
): boolean {
|
||||
return (
|
||||
Math.abs(a.x - b.x) < eps &&
|
||||
Math.abs(a.y - b.y) < eps &&
|
||||
Math.abs(a.z - b.z) < eps
|
||||
)
|
||||
}
|
||||
|
||||
export function cameraStatesClose(
|
||||
a: unknown,
|
||||
b: unknown,
|
||||
eps: number
|
||||
): boolean {
|
||||
if (
|
||||
a === null ||
|
||||
b === null ||
|
||||
typeof a !== 'object' ||
|
||||
typeof b !== 'object'
|
||||
)
|
||||
return false
|
||||
const ca = a as {
|
||||
position?: { x: number; y: number; z: number }
|
||||
target?: { x: number; y: number; z: number }
|
||||
zoom?: number
|
||||
cameraType?: string
|
||||
}
|
||||
const cb = b as typeof ca
|
||||
if (
|
||||
!ca.position ||
|
||||
!ca.target ||
|
||||
!cb.position ||
|
||||
!cb.target ||
|
||||
ca.cameraType !== cb.cameraType
|
||||
)
|
||||
return false
|
||||
if (Math.abs((ca.zoom ?? 0) - (cb.zoom ?? 0)) > eps) return false
|
||||
return (
|
||||
vecClose(ca.position, cb.position, eps) &&
|
||||
vecClose(ca.target, cb.target, eps)
|
||||
)
|
||||
}
|
||||
|
||||
export class Preview3DPipelineContext {
|
||||
static readonly loadNodeId = '1'
|
||||
static readonly previewNodeId = '2'
|
||||
|
||||
readonly load3d: Load3DHelper
|
||||
readonly preview3d: Load3DHelper
|
||||
|
||||
constructor(readonly comfyPage: ComfyPage) {
|
||||
this.load3d = new Load3DHelper(
|
||||
comfyPage.vueNodes.getNodeLocator(Preview3DPipelineContext.loadNodeId)
|
||||
)
|
||||
this.preview3d = new Load3DHelper(
|
||||
comfyPage.vueNodes.getNodeLocator(Preview3DPipelineContext.previewNodeId)
|
||||
)
|
||||
}
|
||||
|
||||
async getModelFileWidgetValue(nodeId: string): Promise<string> {
|
||||
return this.comfyPage.page.evaluate((id) => {
|
||||
const n = window.app!.graph.getNodeById(Number(id))
|
||||
const w = n?.widgets?.find((x) => x.name === 'model_file')
|
||||
return typeof w?.value === 'string' ? w.value : ''
|
||||
}, nodeId)
|
||||
}
|
||||
|
||||
async getLastTimeModelFile(nodeId: string): Promise<string> {
|
||||
return this.comfyPage.page.evaluate((id) => {
|
||||
const n = window.app!.graph.getNodeById(Number(id))
|
||||
const v = n?.properties?.['Last Time Model File']
|
||||
return typeof v === 'string' ? v : ''
|
||||
}, nodeId)
|
||||
}
|
||||
|
||||
async getCameraStateFromProperties(nodeId: string): Promise<unknown> {
|
||||
return this.comfyPage.page.evaluate((id) => {
|
||||
const n = window.app!.graph.getNodeById(Number(id))
|
||||
const cfg = n?.properties?.['Camera Config'] as
|
||||
| { state?: unknown }
|
||||
| undefined
|
||||
return cfg?.state ?? null
|
||||
}, nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
export const preview3dPipelineTest = comfyPageFixture.extend<{
|
||||
preview3dPipeline: Preview3DPipelineContext
|
||||
}>({
|
||||
preview3dPipeline: async ({ comfyPage }, use) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Sidebar'
|
||||
)
|
||||
await comfyPage.workflow.loadWorkflow('3d/preview3d_pipeline')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const pipeline = new Preview3DPipelineContext(comfyPage)
|
||||
await use(pipeline)
|
||||
|
||||
await comfyPage.workflow.setupWorkflowsDirectory({})
|
||||
}
|
||||
})
|
||||
209
browser_tests/tests/load3d/preview3dExecution.spec.ts
Normal file
209
browser_tests/tests/load3d/preview3dExecution.spec.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import {
|
||||
cameraStatesClose,
|
||||
preview3dPipelineTest as test,
|
||||
Preview3DPipelineContext,
|
||||
waitForAppBootstrapped
|
||||
} from '@e2e/fixtures/helpers/Preview3DPipelineFixture'
|
||||
import { assetPath } from '@e2e/fixtures/utils/paths'
|
||||
|
||||
async function seedLoad3dWithCubeObj(
|
||||
pipeline: Preview3DPipelineContext
|
||||
): Promise<void> {
|
||||
const { comfyPage, load3d } = pipeline
|
||||
const fileChooserPromise = comfyPage.page.waitForEvent('filechooser')
|
||||
await load3d.getUploadButton('upload 3d model').click()
|
||||
const fileChooser = await fileChooserPromise
|
||||
await fileChooser.setFiles(assetPath('cube.obj'))
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getModelFileWidgetValue(Preview3DPipelineContext.loadNodeId),
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toContain('cube.obj')
|
||||
|
||||
await load3d.waitForModelLoaded()
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
async function setNonDefaultLoad3dCameraState(
|
||||
pipeline: Preview3DPipelineContext
|
||||
): Promise<void> {
|
||||
const { comfyPage, load3d } = pipeline
|
||||
const box = await load3d.canvas.boundingBox()
|
||||
expect(box, 'Load3D canvas bounding box should exist').not.toBeNull()
|
||||
|
||||
const startX = box!.x + box!.width * 0.5
|
||||
const startY = box!.y + box!.height * 0.5
|
||||
await comfyPage.page.mouse.move(startX, startY)
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.page.mouse.move(startX + 80, startY + 20, {
|
||||
steps: 10
|
||||
})
|
||||
await comfyPage.page.mouse.up()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getCameraStateFromProperties(
|
||||
Preview3DPipelineContext.loadNodeId
|
||||
),
|
||||
{ timeout: 10_000 }
|
||||
)
|
||||
.not.toBeNull()
|
||||
}
|
||||
|
||||
async function alignPreview3dWorkflowUiSettings(
|
||||
pipeline: Preview3DPipelineContext
|
||||
): Promise<void> {
|
||||
await pipeline.comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await pipeline.comfyPage.settings.setSetting(
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Sidebar'
|
||||
)
|
||||
}
|
||||
|
||||
test.describe('Preview3D execution flow', { tag: ['@node', '@slow'] }, () => {
|
||||
test('Preview3D loads model from execution output', async ({
|
||||
preview3dPipeline: pipeline
|
||||
}) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
await seedLoad3dWithCubeObj(pipeline)
|
||||
|
||||
await pipeline.comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getModelFileWidgetValue(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
),
|
||||
{ timeout: 90_000 }
|
||||
)
|
||||
.not.toBe('')
|
||||
|
||||
const modelPath = await pipeline.getModelFileWidgetValue(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
)
|
||||
await expect(
|
||||
modelPath.length,
|
||||
'Preview3D model path populated'
|
||||
).toBeGreaterThan(4)
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getLastTimeModelFile(Preview3DPipelineContext.previewNodeId),
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
.toBe(modelPath)
|
||||
|
||||
await pipeline.preview3d.waitForModelLoaded()
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const b = await pipeline.preview3d.canvas.boundingBox()
|
||||
return (b?.width ?? 0) > 0 && (b?.height ?? 0) > 0
|
||||
})
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
test('Preview3D restores last model and camera after save and full reload', async ({
|
||||
preview3dPipeline: pipeline
|
||||
}) => {
|
||||
test.setTimeout(180_000)
|
||||
|
||||
await seedLoad3dWithCubeObj(pipeline)
|
||||
await setNonDefaultLoad3dCameraState(pipeline)
|
||||
|
||||
await pipeline.comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getModelFileWidgetValue(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
),
|
||||
{ timeout: 90_000 }
|
||||
)
|
||||
.not.toBe('')
|
||||
|
||||
await pipeline.preview3d.waitForModelLoaded()
|
||||
|
||||
const savedPath = await pipeline.getModelFileWidgetValue(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
)
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getCameraStateFromProperties(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
),
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.not.toBeNull()
|
||||
|
||||
const savedCamera = await pipeline.getCameraStateFromProperties(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
)
|
||||
|
||||
const workflowName = `p3d-restore-${Date.now().toString(36)}`
|
||||
await pipeline.comfyPage.menu.workflowsTab.open()
|
||||
await pipeline.comfyPage.menu.topbar.saveWorkflow(workflowName)
|
||||
|
||||
await pipeline.comfyPage.page.reload({ waitUntil: 'networkidle' })
|
||||
await waitForAppBootstrapped(pipeline.comfyPage)
|
||||
|
||||
await alignPreview3dWorkflowUiSettings(pipeline)
|
||||
|
||||
const tab = pipeline.comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
await tab.getPersistedItem(workflowName).click()
|
||||
await pipeline.comfyPage.workflow.waitForWorkflowIdle(15_000)
|
||||
await pipeline.comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getModelFileWidgetValue(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
),
|
||||
{ timeout: 30_000 }
|
||||
)
|
||||
.toBe(savedPath)
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getLastTimeModelFile(Preview3DPipelineContext.previewNodeId),
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
.toBe(savedPath)
|
||||
|
||||
await pipeline.preview3d.waitForModelLoaded()
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const b = await pipeline.preview3d.canvas.boundingBox()
|
||||
return (b?.width ?? 0) > 0 && (b?.height ?? 0) > 0
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
await expect
|
||||
.poll(async () =>
|
||||
cameraStatesClose(
|
||||
await pipeline.getCameraStateFromProperties(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
),
|
||||
savedCamera,
|
||||
2e-2
|
||||
)
|
||||
)
|
||||
.toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -323,38 +323,6 @@ describe('useGLSLPreview', () => {
|
||||
expect(mockRendererFactory.compileFragment).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses custom resolution when size_mode is custom', async () => {
|
||||
const store = fromAny<WidgetValueStoreStub, unknown>(
|
||||
useWidgetValueStore()
|
||||
)
|
||||
store._widgetMap.set('size_mode', { value: 'custom' })
|
||||
store._widgetMap.set('size_mode.width', { value: 800 })
|
||||
store._widgetMap.set('size_mode.height', { value: 600 })
|
||||
|
||||
const node = createMockNode()
|
||||
await setupAndRender(node)
|
||||
|
||||
expect(mockRendererFactory.setResolution).toHaveBeenCalledWith(800, 600)
|
||||
|
||||
store._widgetMap.delete('size_mode')
|
||||
store._widgetMap.delete('size_mode.width')
|
||||
store._widgetMap.delete('size_mode.height')
|
||||
})
|
||||
|
||||
it('uses default resolution when size_mode is not custom', async () => {
|
||||
const store = fromAny<WidgetValueStoreStub, unknown>(
|
||||
useWidgetValueStore()
|
||||
)
|
||||
store._widgetMap.set('size_mode', { value: 'from_input' })
|
||||
|
||||
const node = createMockNode()
|
||||
await setupAndRender(node)
|
||||
|
||||
expect(mockRendererFactory.setResolution).toHaveBeenCalledWith(512, 512)
|
||||
|
||||
store._widgetMap.delete('size_mode')
|
||||
})
|
||||
|
||||
it('disposes renderer and cancels debounce on cleanup', async () => {
|
||||
const node = createMockNode()
|
||||
const { dispose } = await setupAndRender(node)
|
||||
|
||||
@@ -282,44 +282,7 @@ function createInnerPreview(
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomResolution(): [number, number] | null {
|
||||
const gId = graphId.value
|
||||
if (!gId) return null
|
||||
|
||||
const sizeModeNodeId = innerGLSLNode
|
||||
? (innerGLSLNode.id as NodeId)
|
||||
: nodeId.value
|
||||
if (sizeModeNodeId == null) return null
|
||||
|
||||
const sizeMode = widgetValueStore.getWidget(
|
||||
gId,
|
||||
sizeModeNodeId,
|
||||
'size_mode'
|
||||
)
|
||||
if (sizeMode?.value !== 'custom') return null
|
||||
|
||||
const widthWidget = widgetValueStore.getWidget(
|
||||
gId,
|
||||
sizeModeNodeId,
|
||||
'size_mode.width'
|
||||
)
|
||||
const heightWidget = widgetValueStore.getWidget(
|
||||
gId,
|
||||
sizeModeNodeId,
|
||||
'size_mode.height'
|
||||
)
|
||||
if (!widthWidget || !heightWidget) return null
|
||||
|
||||
return clampResolution(
|
||||
normalizeDimension(widthWidget.value),
|
||||
normalizeDimension(heightWidget.value)
|
||||
)
|
||||
}
|
||||
|
||||
function getResolution(): [number, number] {
|
||||
const custom = getCustomResolution()
|
||||
if (custom) return custom
|
||||
|
||||
const node = nodeRef.value
|
||||
if (!node?.inputs) return [DEFAULT_SIZE, DEFAULT_SIZE]
|
||||
|
||||
@@ -362,6 +325,27 @@ function createInnerPreview(
|
||||
}
|
||||
}
|
||||
|
||||
const gId = graphId.value
|
||||
const nId = nodeId.value
|
||||
if (gId && nId != null) {
|
||||
const widthWidget = widgetValueStore.getWidget(
|
||||
gId,
|
||||
nId,
|
||||
'size_mode.width'
|
||||
)
|
||||
const heightWidget = widgetValueStore.getWidget(
|
||||
gId,
|
||||
nId,
|
||||
'size_mode.height'
|
||||
)
|
||||
if (widthWidget && heightWidget) {
|
||||
return clampResolution(
|
||||
normalizeDimension(widthWidget.value),
|
||||
normalizeDimension(heightWidget.value)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return [DEFAULT_SIZE, DEFAULT_SIZE]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { fromAny } from '@total-typescript/shoehorn'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
|
||||
import {
|
||||
extractUniformSources,
|
||||
toNumber
|
||||
} from '@/renderer/glsl/useGLSLUniforms'
|
||||
|
||||
function createMockSubgraph(
|
||||
links: Record<number, { origin_id: number; origin_slot: number }>,
|
||||
nodes: Record<
|
||||
number,
|
||||
{ id: number; widgets: Array<{ name: string; value: unknown }> }
|
||||
>
|
||||
) {
|
||||
return fromAny<Subgraph, unknown>({
|
||||
getLink: (id: number) => links[id] ?? null,
|
||||
getNodeById: (id: number) => nodes[id] ?? null
|
||||
})
|
||||
}
|
||||
|
||||
describe('extractUniformSources', () => {
|
||||
it('uses origin_slot to select the correct widget from source node', () => {
|
||||
const glslNode = fromAny<LGraphNode, unknown>({
|
||||
inputs: [
|
||||
{ name: 'ints.u_int0', link: 1 },
|
||||
{ name: 'ints.u_int1', link: 2 }
|
||||
]
|
||||
})
|
||||
|
||||
const subgraph = createMockSubgraph(
|
||||
{
|
||||
1: { origin_id: 10, origin_slot: 1 },
|
||||
2: { origin_id: 20, origin_slot: 0 }
|
||||
},
|
||||
{
|
||||
10: {
|
||||
id: 10,
|
||||
widgets: [
|
||||
{ name: 'choice', value: 'Master' },
|
||||
{ name: 'index', value: 0 }
|
||||
]
|
||||
},
|
||||
20: {
|
||||
id: 20,
|
||||
widgets: [{ name: 'value', value: 42 }]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const result = extractUniformSources(glslNode, subgraph)
|
||||
|
||||
expect(result.ints[0].widgetName).toBe('index')
|
||||
expect(result.ints[1].widgetName).toBe('value')
|
||||
})
|
||||
|
||||
it('skips source when origin_slot exceeds widget count', () => {
|
||||
const glslNode = fromAny<LGraphNode, unknown>({
|
||||
inputs: [{ name: 'floats.u_float0', link: 1 }]
|
||||
})
|
||||
|
||||
const subgraph = createMockSubgraph(
|
||||
{ 1: { origin_id: 10, origin_slot: 5 } },
|
||||
{ 10: { id: 10, widgets: [{ name: 'value', value: 3.14 }] } }
|
||||
)
|
||||
|
||||
const result = extractUniformSources(glslNode, subgraph)
|
||||
|
||||
expect(result.floats).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('provides directValue getter that reads from the widget', () => {
|
||||
const indexWidget = {
|
||||
name: 'index',
|
||||
get value() {
|
||||
return choiceWidget.value === 'Reds' ? 1 : 0
|
||||
}
|
||||
}
|
||||
const choiceWidget = { name: 'choice', value: 'Master' }
|
||||
|
||||
const glslNode = fromAny<LGraphNode, unknown>({
|
||||
inputs: [{ name: 'ints.u_int0', link: 1 }]
|
||||
})
|
||||
|
||||
const subgraph = createMockSubgraph(
|
||||
{ 1: { origin_id: 10, origin_slot: 1 } },
|
||||
{ 10: { id: 10, widgets: [choiceWidget, indexWidget] } }
|
||||
)
|
||||
|
||||
const result = extractUniformSources(glslNode, subgraph)
|
||||
|
||||
expect(result.ints[0].directValue()).toBe(0)
|
||||
|
||||
choiceWidget.value = 'Reds'
|
||||
expect(result.ints[0].directValue()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toNumber', () => {
|
||||
it('coerces hex color strings via hexToInt', () => {
|
||||
expect(toNumber('#45edf5')).toBe(0x45edf5)
|
||||
})
|
||||
|
||||
it('coerces plain numeric values', () => {
|
||||
expect(toNumber(42)).toBe(42)
|
||||
expect(toNumber('10')).toBe(10)
|
||||
})
|
||||
|
||||
it('returns 0 for non-numeric strings', () => {
|
||||
expect(toNumber('Master')).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -10,7 +10,6 @@ import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { isCurveData } from '@/components/curve/curveUtils'
|
||||
import type { CurveData } from '@/components/curve/types'
|
||||
import type { GLSLRendererConfig } from '@/renderer/glsl/useGLSLRenderer'
|
||||
import { hexToInt } from '@/utils/colorUtil'
|
||||
|
||||
interface AutogrowGroup {
|
||||
max: number
|
||||
@@ -21,8 +20,6 @@ interface AutogrowGroup {
|
||||
interface UniformSource {
|
||||
nodeId: NodeId
|
||||
widgetName: string
|
||||
/** Fallback getter for widgets not registered in widgetValueStore (e.g. hidden computed widgets). */
|
||||
directValue: () => unknown
|
||||
}
|
||||
|
||||
interface UniformSources {
|
||||
@@ -81,19 +78,16 @@ export function extractUniformSources(
|
||||
if (!link || link.origin_id === SUBGRAPH_INPUT_ID) continue
|
||||
|
||||
const sourceNode = subgraph.getNodeById(link.origin_id)
|
||||
if (!sourceNode?.widgets?.length) continue
|
||||
if (!sourceNode?.widgets?.[0]) continue
|
||||
|
||||
const inputName = input.name ?? ''
|
||||
const dotIndex = inputName.indexOf('.')
|
||||
if (dotIndex === -1) continue
|
||||
|
||||
const prefix = inputName.slice(0, dotIndex)
|
||||
if (link.origin_slot >= sourceNode.widgets.length) continue
|
||||
const widget = sourceNode.widgets[link.origin_slot]
|
||||
const source: UniformSource = {
|
||||
nodeId: sourceNode.id as NodeId,
|
||||
widgetName: widget.name,
|
||||
directValue: () => widget.value
|
||||
widgetName: sourceNode.widgets[0].name
|
||||
}
|
||||
|
||||
if (prefix === 'floats') floats.push(source)
|
||||
@@ -105,11 +99,6 @@ export function extractUniformSources(
|
||||
return { floats, ints, bools, curves }
|
||||
}
|
||||
|
||||
export function toNumber(v: unknown): number {
|
||||
if (typeof v === 'string' && v.startsWith('#')) return hexToInt(v)
|
||||
return Number(v) || 0
|
||||
}
|
||||
|
||||
export function useGLSLUniforms(
|
||||
graphId: ComputedRef<UUID | undefined>,
|
||||
nodeId: ComputedRef<NodeId | undefined>,
|
||||
@@ -131,9 +120,9 @@ export function useGLSLUniforms(
|
||||
if (!gId) return []
|
||||
|
||||
if (subgraphSources) {
|
||||
return subgraphSources.map(({ nodeId: nId, widgetName, directValue }) => {
|
||||
return subgraphSources.map(({ nodeId: nId, widgetName }) => {
|
||||
const widget = widgetValueStore.getWidget(gId, nId, widgetName)
|
||||
return coerce(widget?.value ?? directValue() ?? defaultValue)
|
||||
return coerce(widget?.value ?? defaultValue)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,24 +142,19 @@ export function useGLSLUniforms(
|
||||
const slot = node.inputs?.findIndex((inp) => inp.name === inputName)
|
||||
if (slot == null || slot < 0) break
|
||||
|
||||
const link = node.getInputLink(slot)
|
||||
if (!link) break
|
||||
const upstreamNode = node.getInputNode(slot)
|
||||
if (!upstreamNode) break
|
||||
const upstreamWidgets = widgetValueStore.getNodeWidgets(
|
||||
gId,
|
||||
upstreamNode.id as NodeId
|
||||
)
|
||||
if (
|
||||
upstreamWidgets.length === 0 ||
|
||||
link.origin_slot >= upstreamWidgets.length
|
||||
)
|
||||
break
|
||||
values.push(coerce(upstreamWidgets[link.origin_slot].value))
|
||||
if (upstreamWidgets.length === 0) break
|
||||
values.push(coerce(upstreamWidgets[0].value))
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
const toNumber = (v: unknown): number => Number(v) || 0
|
||||
const toBool = (v: unknown): boolean => Boolean(v)
|
||||
|
||||
const floatValues = computed(() =>
|
||||
@@ -213,10 +197,11 @@ export function useGLSLUniforms(
|
||||
const sources = uniformSources.value?.curves
|
||||
if (sources && sources.length > 0) {
|
||||
return sources
|
||||
.map(({ nodeId: nId, widgetName, directValue }) => {
|
||||
.map(({ nodeId: nId, widgetName }) => {
|
||||
const widget = widgetValueStore.getWidget(gId, nId, widgetName)
|
||||
const value = widget?.value ?? directValue()
|
||||
return isCurveData(value) ? (value as CurveData) : null
|
||||
return widget && isCurveData(widget.value)
|
||||
? (widget.value as CurveData)
|
||||
: null
|
||||
})
|
||||
.filter((v): v is CurveData => v !== null)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { ColorAdjustOptions } from '@/utils/colorUtil'
|
||||
import {
|
||||
adjustColor,
|
||||
hexToHsva,
|
||||
hexToInt,
|
||||
hexToRgb,
|
||||
hsbToRgb,
|
||||
hsvaToHex,
|
||||
@@ -96,20 +95,6 @@ describe('colorUtil conversions', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('hexToInt', () => {
|
||||
it('converts 6-digit hex to packed integer', () => {
|
||||
expect(hexToInt('#ff0000')).toBe(0xff0000)
|
||||
expect(hexToInt('#00ff00')).toBe(0x00ff00)
|
||||
expect(hexToInt('#45edf5')).toBe(0x45edf5)
|
||||
expect(hexToInt('#000000')).toBe(0)
|
||||
})
|
||||
|
||||
it('converts 3-digit hex to packed integer', () => {
|
||||
expect(hexToInt('#fff')).toBe(0xffffff)
|
||||
expect(hexToInt('#f00')).toBe(0xff0000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseToRgb', () => {
|
||||
it('parses #hex', () => {
|
||||
expect(parseToRgb('#ff0000')).toEqual({ r: 255, g: 0, b: 0 })
|
||||
|
||||
@@ -82,11 +82,6 @@ export function hexToRgb(hex: string): RGB {
|
||||
return { r, g, b }
|
||||
}
|
||||
|
||||
export function hexToInt(hex: string): number {
|
||||
const { r, g, b } = hexToRgb(hex)
|
||||
return (r << 16) | (g << 8) | b
|
||||
}
|
||||
|
||||
export function rgbToHex({ r, g, b }: RGB): string {
|
||||
const toHex = (n: number) =>
|
||||
Math.max(0, Math.min(255, Math.round(n)))
|
||||
|
||||
Reference in New Issue
Block a user