diff --git a/src/App.vue b/src/App.vue index 947b039..5cb6fae 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,7 +10,7 @@ import NodePalette from './components/NodePalette.vue' import StudioNode from './components/nodes/StudioNode.vue' import { useStudioStore } from './stores/studio' import { buildGraph } from './graphBuilder' -import { defaultParams } from './nodeTypes' +import { defaultParams, NODE_TYPES } from './nodeTypes' import type { StudioNodeData } from './types' const FLOW_ID = 'dmf-studio' @@ -23,6 +23,16 @@ const nodes = ref([]) const edges = ref([]) const flowWrapperRef = ref() +function portKind(nodeId: string, handleId: string): string | undefined { + for (const n of nodes.value) { + if (n.id === nodeId) { + const def = NODE_TYPES[(n.data as StudioNodeData).nodeType] + return def?.ports.find(p => p.id === handleId)?.kind + } + } + return undefined +} + let nodeCounter = 0 watch( @@ -85,12 +95,13 @@ function addNode(nodeType: string) { 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' + const srcKind = portKind(connection.source, srcHandle) + const tgtKind = portKind(connection.target, tgtHandle) + if (!srcKind || !tgtKind || srcKind !== tgtKind) return edges.value = [ ...edges.value, - { ...connection, id: `e-${Date.now()}`, animated: true, class: `edge-${kind}` } as Edge, + { ...connection, id: `e-${Date.now()}`, animated: true, class: `edge-${srcKind}` } as Edge, ] } diff --git a/src/components/NodePalette.vue b/src/components/NodePalette.vue index a18796c..7f2980b 100644 --- a/src/components/NodePalette.vue +++ b/src/components/NodePalette.vue @@ -12,7 +12,7 @@ interface PaletteGroup { const groups: PaletteGroup[] = [ { label: 'SOURCES', types: ['testpattern', 'ndiin', 'decklinkin', 'videoin'] }, - { label: 'PROCESS', types: ['mix'] }, + { label: 'PROCESS', types: ['mix', 'pip', 'gaindb'] }, { label: 'SINKS', types: ['ndiout', 'decklinkout', 'fakesink'] }, ] @@ -25,6 +25,8 @@ const iconMap: Record = { decklinkout: '◧', fakesink: '◯', mix: '⊕', + pip: '⊞', + gaindb: '◎', } function onDragStart(event: DragEvent, nodeType: string) { diff --git a/src/components/nodes/StudioNode.vue b/src/components/nodes/StudioNode.vue index 026f18f..a63e565 100644 --- a/src/components/nodes/StudioNode.vue +++ b/src/components/nodes/StudioNode.vue @@ -26,6 +26,8 @@ const iconMap: Record = { decklinkout: '◧', fakesink: '◯', mix: '⊕', + pip: '⊞', + gaindb: '◎', } const nodeIcon = computed(() => iconMap[props.data.nodeType] ?? '◯') @@ -107,34 +109,43 @@ function onDeleteNode() {
-
- - - - -
+ {{ param.group }} + +
+
+ + + + +
+ @@ -285,6 +296,31 @@ function onDeleteNode() { gap: 6px; } +.param-group { + display: flex; + align-items: center; + gap: 8px; + margin-top: 2px; + margin-bottom: 2px; +} + +.param-group:first-child { + margin-top: 0; +} + +.param-group-label { + font-size: 8px; + letter-spacing: 1.5px; + color: var(--text-dim); + flex-shrink: 0; +} + +.param-group-line { + flex: 1; + height: 1px; + background: var(--border-thin); +} + .param-label { color: var(--text-muted); min-width: 68px; diff --git a/src/graphBuilder.ts b/src/graphBuilder.ts index 9448b80..8d01f7c 100644 --- a/src/graphBuilder.ts +++ b/src/graphBuilder.ts @@ -23,11 +23,26 @@ function parseRes(res: string): { width: number; height: number } { return { width: w || 1920, height: h || 1080 } } +// Looks up the port kind from the node type definition +function portKindFor(node: Node | undefined, handleId: string): string | undefined { + if (!node) return undefined + const def = NODE_TYPES[(node.data as StudioNodeData).nodeType] + return def?.ports.find(p => p.id === handleId)?.kind +} + +// Looks up the backend port key from the node type definition, falls back to handleToPort +function portKeyFor(node: Node | undefined, handleId: string): string { + if (node) { + const def = NODE_TYPES[(node.data as StudioNodeData).nodeType] + const portKey = def?.ports.find(p => p.id === handleId)?.portKey + if (portKey) return portKey + } + return handleToPort(handleId) +} + // 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)(?:-\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 { @@ -49,10 +64,12 @@ export function buildGraph(vfNodes: Node[], vfEdges: Edge[]): GraphJson { }) const edges = vfEdges.map((e) => { + const srcNode = vfNodes.find(n => n.id === e.source) + const tgtNode = vfNodes.find(n => n.id === e.target) const srcData = nodeMap.get(e.source) const srcHandle = e.sourceHandle ?? 'video-out' const tgtHandle = e.targetHandle ?? 'video-in' - const kind = srcHandle.startsWith('video') ? 'video' : 'audio' + const kind = portKindFor(srcNode, srcHandle) ?? 'video' let format: Record = { kind } if (kind === 'video') { @@ -60,12 +77,17 @@ export function buildGraph(vfNodes: Node[], vfEdges: Edge[]): GraphJson { const { fps_num, fps_den } = parseFps(String(srcData?.params.fps ?? '25')) format = { kind, width, height, fps_num, fps_den } } else { - format = { kind, sample_rate: 48000, channels: 2, bit_depth: 32 } + format = { + kind, + sample_rate: Number(srcData?.params.sample_rate ?? 48000), + channels: Number(srcData?.params.channels ?? 2), + bit_depth: 32, + } } return { - from: e.source, from_port: handleToPort(srcHandle), - to: e.target, to_port: handleToPort(tgtHandle), + from: e.source, from_port: portKeyFor(srcNode, srcHandle), + to: e.target, to_port: portKeyFor(tgtNode, tgtHandle), format, } }) diff --git a/src/nodeTypes.ts b/src/nodeTypes.ts index 692c49e..eab7fde 100644 --- a/src/nodeTypes.ts +++ b/src/nodeTypes.ts @@ -45,10 +45,22 @@ export const TRANSITION_OPTIONS = [ { label: 'Push', value: 'push' }, ] +export const SAMPLE_RATE_OPTIONS = [ + { label: '44100', value: 44100 }, + { label: '48000', value: 48000 }, + { label: '96000', value: 96000 }, +] + // 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 }, - { key: 'fps', label: 'FPS', type: 'select', options: FPS_OPTIONS, default: '25', isFormat: true }, +export const FORMAT_OPTIONS_VIDEO: ParamDef[] = [ + { key: 'res', label: 'Resolution', type: 'select', options: RESOLUTION_OPTIONS, default: '1920x1080', isFormat: true, group: 'VIDEO' }, + { key: 'fps', label: 'FPS', type: 'select', options: FPS_OPTIONS, default: '25', isFormat: true, group: 'VIDEO' }, +] + +export const FORMAT_OPTIONS_AUDIO: ParamDef[] = [ + { key: 'channels', label: 'Channels', type: 'number', default: 0, min: 0, max: 16, isFormat: true, group: 'AUDIO' }, + { key: 'sample_rate', label: 'Sample Rate', type: 'select', options: SAMPLE_RATE_OPTIONS, default: 48000, isFormat: true, group: 'AUDIO' }, + { key: 'amplitude', label: 'Amplitude (dB)', type: 'number', default: -18, min: -60, max: 0, isFormat: true }, ] export const NODE_TYPES: Record = { @@ -60,9 +72,9 @@ export const NODE_TYPES: Record = { { id: 'audio-out', kind: 'audio', direction: 'out' }, ], params: [ - ...FORMAT_PARAMS, + ...FORMAT_OPTIONS_VIDEO, + ...FORMAT_OPTIONS_AUDIO, { key: 'pattern', 'label': 'Pattern', type: 'select', options: PATTERN_OPTIONS, default: 'bars' }, - { key: 'channels', label: 'Channels', type: 'number', default: 0, min: 0, max: 16 } ], }, ndiin: { @@ -74,7 +86,8 @@ export const NODE_TYPES: Record = { ], params: [ { key: 'source_num', label: 'NDI Source #', type: 'number', default: 0, min: 0, max: 15 }, - ...FORMAT_PARAMS, + ...FORMAT_OPTIONS_VIDEO, + ...FORMAT_OPTIONS_AUDIO, ], }, decklinkin: { @@ -86,7 +99,8 @@ export const NODE_TYPES: Record = { ], params: [ { key: 'device_index', label: 'Port', type: 'select', options: DEVICE_OPTIONS, default: 0 }, - ...FORMAT_PARAMS, + ...FORMAT_OPTIONS_VIDEO, + ...FORMAT_OPTIONS_AUDIO, ], }, ndiout: { @@ -145,6 +159,32 @@ export const NODE_TYPES: Record = { { key: 'duration', label: 'Duration', type: 'number', default: 500, min: 0, max: 10000 }, ], }, + pip: { + type: 'pip', + label: 'PiP', + ports: [ + { id: 'background-in', kind: 'video', direction: 'in' }, + { id: 'inset-in', kind: 'video', direction: 'in' }, + { id: 'video-out', kind: 'video', direction: 'out' }, + ], + params: [ + { key: 'x', label: 'X', type: 'number', default: 0 }, + { key: 'y', label: 'Y', type: 'number', default: 0 }, + { key: 'width', label: 'Width', type: 'number', default: 480 }, + { key: 'height', label: 'Height', type: 'number', default: 270 }, + ], + }, + gaindb: { + type: 'gaindb', + label: 'Gain/dB', + ports: [ + { id: 'audio-in', kind: 'audio', direction: 'in', portKey: 'audio_in_flow_id' }, + { id: 'audio-out', kind: 'audio', direction: 'out', portKey: 'audio_out_flow_id' }, + ], + params: [ + { key: 'gain_db', label: 'Gain (dB)', type: 'number', default: 0, min: -60, max: 20 }, + ], + }, } // Default params for a new node of the given type diff --git a/src/types.ts b/src/types.ts index 43dea25..5b5c491 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,10 @@ export type PortKind = 'video' | 'audio' export interface PortDef { - id: string // 'video-out', 'audio-in', etc. + id: string // 'video-out', 'audio-in', etc. (UI handle id) kind: PortKind direction: 'in' | 'out' + portKey?: string // backend graph JSON key; defaults to handleToPort(id) } export interface ParamDef { @@ -15,6 +16,7 @@ export interface ParamDef { min?: number max?: number isFormat?: boolean // true → goes into edge format (not node params) + group?: string // section label e.g. 'VIDEO', 'AUDIO' — renders a divider header } export interface NodeTypeDef {