From 0ea48bc9a2b181b08a5163b5d149daaadae1ac95 Mon Sep 17 00:00:00 2001 From: itten Date: Thu, 9 Jul 2026 11:38:11 +0300 Subject: [PATCH] minor fixes & mix node sample --- src/App.vue | 1 + src/components/NodePalette.vue | 2 ++ src/components/nodes/StudioNode.vue | 10 +++++++--- src/graphBuilder.ts | 3 ++- src/nodeTypes.ts | 31 +++++++++++++++++++++++++++++ 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/App.vue b/src/App.vue index 6d5cf54..947b039 100644 --- a/src/App.vue +++ b/src/App.vue @@ -86,6 +86,7 @@ function onConnect(connection: Connection) { const srcHandle = connection.sourceHandle ?? '' const tgtHandle = connection.targetHandle ?? '' if (srcHandle.startsWith('video') !== tgtHandle.startsWith('video')) return + if (!srcHandle.includes('-out') || !tgtHandle.includes('-in')) return const kind = srcHandle.startsWith('video') ? 'video' : 'audio' edges.value = [ ...edges.value, diff --git a/src/components/NodePalette.vue b/src/components/NodePalette.vue index 053088e..a18796c 100644 --- a/src/components/NodePalette.vue +++ b/src/components/NodePalette.vue @@ -12,6 +12,7 @@ interface PaletteGroup { const groups: PaletteGroup[] = [ { label: 'SOURCES', types: ['testpattern', 'ndiin', 'decklinkin', 'videoin'] }, + { label: 'PROCESS', types: ['mix'] }, { label: 'SINKS', types: ['ndiout', 'decklinkout', 'fakesink'] }, ] @@ -23,6 +24,7 @@ const iconMap: Record = { ndiout: '◬', decklinkout: '◧', fakesink: '◯', + mix: '⊕', } function onDragStart(event: DragEvent, nodeType: string) { diff --git a/src/components/nodes/StudioNode.vue b/src/components/nodes/StudioNode.vue index 5c57e33..026f18f 100644 --- a/src/components/nodes/StudioNode.vue +++ b/src/components/nodes/StudioNode.vue @@ -12,7 +12,10 @@ const typeDef = computed(() => NODE_TYPES[props.data.nodeType] ?? null) const inputs = computed(() => typeDef.value?.ports.filter(p => p.direction === 'in') ?? []) const outputs = computed(() => typeDef.value?.ports.filter(p => p.direction === 'out') ?? []) -const isSource = computed(() => outputs.value.length > 0) +function portLabel(portId: string): string { + const parts = portId.split('-') + return parts.slice(0, -1).join(' ').toUpperCase() +} const iconMap: Record = { testpattern: '▦', @@ -22,6 +25,7 @@ const iconMap: Record = { ndiout: '◬', decklinkout: '◧', fakesink: '◯', + mix: '⊕', } const nodeIcon = computed(() => iconMap[props.data.nodeType] ?? '◯') @@ -91,12 +95,12 @@ function onDeleteNode() {
- {{ port.kind }} + {{ portLabel(port.id) }}
- {{ port.kind }} + {{ portLabel(port.id) }}
diff --git a/src/graphBuilder.ts b/src/graphBuilder.ts index 56ed2ec..9448b80 100644 --- a/src/graphBuilder.ts +++ b/src/graphBuilder.ts @@ -25,8 +25,9 @@ function parseRes(res: string): { width: number; height: number } { // Converts a Vue Flow handle id to the NODE_CONFIG port key used in graph JSON function handleToPort(handle: string): string { - return handle.replace(/-(?:in|out)$/, '_flow_id') + return handle.replace(/-(?:in|out)(?:-\d+)?$/, '_flow_id') // 'video-out' → 'video_flow_id', 'audio-in' → 'audio_flow_id' + // 'video-in-1' → 'video_flow_id', 'audio-in-2' → 'audio_flow_id' } export function buildGraph(vfNodes: Node[], vfEdges: Edge[]): GraphJson { diff --git a/src/nodeTypes.ts b/src/nodeTypes.ts index f3fabef..692c49e 100644 --- a/src/nodeTypes.ts +++ b/src/nodeTypes.ts @@ -31,6 +31,20 @@ export const PATTERN_OPTIONS = [ { label: 'White solid', value: 'white' }, ] +export const MIX_MODE_OPTIONS = [ + { label: 'Fade', value: 'fade' }, + { label: 'Dissolve', value: 'dissolve' }, + { label: 'Wipe', value: 'wipe' }, + { label: 'A/B', value: 'ab' }, +] + +export const TRANSITION_OPTIONS = [ + { label: 'Cut', value: 'cut' }, + { label: 'Cross', value: 'cross' }, + { label: 'Slide', value: 'slide' }, + { label: 'Push', value: 'push' }, +] + // Format params shown on source nodes; values go into edge format (not node params) const FORMAT_PARAMS: ParamDef[] = [ { key: 'res', label: 'Resolution', type: 'select', options: RESOLUTION_OPTIONS, default: '1920x1080', isFormat: true }, @@ -114,6 +128,23 @@ export const NODE_TYPES: Record = { { key: 'file', label: 'File', type: 'input', default: '0.ts' } ], }, + mix: { + type: 'mix', + label: 'Mix', + ports: [ + { id: 'video-in-1', kind: 'video', direction: 'in' }, + { id: 'video-in-2', kind: 'video', direction: 'in' }, + { id: 'audio-in-1', kind: 'audio', direction: 'in' }, + { id: 'audio-in-2', kind: 'audio', direction: 'in' }, + { id: 'video-out', kind: 'video', direction: 'out' }, + { id: 'audio-out', kind: 'audio', direction: 'out' }, + ], + params: [ + { key: 'mode', label: 'Mode', type: 'select', options: MIX_MODE_OPTIONS, default: 'fade' }, + { key: 'transition', label: 'Transition', type: 'select', options: TRANSITION_OPTIONS, default: 'cut' }, + { key: 'duration', label: 'Duration', type: 'number', default: 500, min: 0, max: 10000 }, + ], + }, } // Default params for a new node of the given type