frontend almost work

This commit is contained in:
itten
2026-07-09 21:27:52 +03:00
parent 0ea48bc9a2
commit 1a7eb136f5
6 changed files with 159 additions and 46 deletions
+15 -4
View File
@@ -10,7 +10,7 @@ import NodePalette from './components/NodePalette.vue'
import StudioNode from './components/nodes/StudioNode.vue' import StudioNode from './components/nodes/StudioNode.vue'
import { useStudioStore } from './stores/studio' import { useStudioStore } from './stores/studio'
import { buildGraph } from './graphBuilder' import { buildGraph } from './graphBuilder'
import { defaultParams } from './nodeTypes' import { defaultParams, NODE_TYPES } from './nodeTypes'
import type { StudioNodeData } from './types' import type { StudioNodeData } from './types'
const FLOW_ID = 'dmf-studio' const FLOW_ID = 'dmf-studio'
@@ -23,6 +23,16 @@ const nodes = ref<Node[]>([])
const edges = ref<Edge[]>([]) const edges = ref<Edge[]>([])
const flowWrapperRef = ref<HTMLElement>() const flowWrapperRef = ref<HTMLElement>()
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 let nodeCounter = 0
watch( watch(
@@ -85,12 +95,13 @@ function addNode(nodeType: string) {
function onConnect(connection: Connection) { function onConnect(connection: Connection) {
const srcHandle = connection.sourceHandle ?? '' const srcHandle = connection.sourceHandle ?? ''
const tgtHandle = connection.targetHandle ?? '' const tgtHandle = connection.targetHandle ?? ''
if (srcHandle.startsWith('video') !== tgtHandle.startsWith('video')) return
if (!srcHandle.includes('-out') || !tgtHandle.includes('-in')) 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 = [
...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,
] ]
} }
+3 -1
View File
@@ -12,7 +12,7 @@ interface PaletteGroup {
const groups: PaletteGroup[] = [ const groups: PaletteGroup[] = [
{ label: 'SOURCES', types: ['testpattern', 'ndiin', 'decklinkin', 'videoin'] }, { label: 'SOURCES', types: ['testpattern', 'ndiin', 'decklinkin', 'videoin'] },
{ label: 'PROCESS', types: ['mix'] }, { label: 'PROCESS', types: ['mix', 'pip', 'gaindb'] },
{ label: 'SINKS', types: ['ndiout', 'decklinkout', 'fakesink'] }, { label: 'SINKS', types: ['ndiout', 'decklinkout', 'fakesink'] },
] ]
@@ -25,6 +25,8 @@ const iconMap: Record<string, string> = {
decklinkout: '◧', decklinkout: '◧',
fakesink: '◯', fakesink: '◯',
mix: '⊕', mix: '⊕',
pip: '⊞',
gaindb: '◎',
} }
function onDragStart(event: DragEvent, nodeType: string) { function onDragStart(event: DragEvent, nodeType: string) {
+37 -1
View File
@@ -26,6 +26,8 @@ const iconMap: Record<string, string> = {
decklinkout: '◧', decklinkout: '◧',
fakesink: '◯', fakesink: '◯',
mix: '⊕', mix: '⊕',
pip: '⊞',
gaindb: '◎',
} }
const nodeIcon = computed(() => iconMap[props.data.nodeType] ?? '◯') const nodeIcon = computed(() => iconMap[props.data.nodeType] ?? '◯')
@@ -107,7 +109,15 @@ function onDeleteNode() {
<!-- Params --> <!-- Params -->
<div v-if="typeDef && typeDef.params.length" class="node-body"> <div v-if="typeDef && typeDef.params.length" class="node-body">
<div v-for="param in typeDef.params" :key="param.key" class="param-row"> <template v-for="(param, i) in typeDef.params" :key="param.key">
<div
v-if="param.group && (i === 0 || typeDef.params[i - 1].group !== param.group)"
class="param-group"
>
<span class="param-group-label">{{ param.group }}</span>
<span class="param-group-line"></span>
</div>
<div class="param-row">
<label class="param-label">{{ param.label.toUpperCase() }}</label> <label class="param-label">{{ param.label.toUpperCase() }}</label>
<select <select
v-if="param.type === 'select'" v-if="param.type === 'select'"
@@ -135,6 +145,7 @@ function onDeleteNode() {
@change="handleFileChange(param.key, $event)" @change="handleFileChange(param.key, $event)"
/> />
</div> </div>
</template>
</div> </div>
<!-- Footer --> <!-- Footer -->
@@ -285,6 +296,31 @@ function onDeleteNode() {
gap: 6px; 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 { .param-label {
color: var(--text-muted); color: var(--text-muted);
min-width: 68px; min-width: 68px;
+28 -6
View File
@@ -23,11 +23,26 @@ function parseRes(res: string): { width: number; height: number } {
return { width: w || 1920, height: h || 1080 } 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 // Converts a Vue Flow handle id to the NODE_CONFIG port key used in graph JSON
function handleToPort(handle: string): string { function handleToPort(handle: string): string {
return handle.replace(/-(?:in|out)(?:-\d+)?$/, '_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 { 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 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 srcData = nodeMap.get(e.source)
const srcHandle = e.sourceHandle ?? 'video-out' const srcHandle = e.sourceHandle ?? 'video-out'
const tgtHandle = e.targetHandle ?? 'video-in' const tgtHandle = e.targetHandle ?? 'video-in'
const kind = srcHandle.startsWith('video') ? 'video' : 'audio' const kind = portKindFor(srcNode, srcHandle) ?? 'video'
let format: Record<string, unknown> = { kind } let format: Record<string, unknown> = { kind }
if (kind === 'video') { 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')) const { fps_num, fps_den } = parseFps(String(srcData?.params.fps ?? '25'))
format = { kind, width, height, fps_num, fps_den } format = { kind, width, height, fps_num, fps_den }
} else { } 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 { return {
from: e.source, from_port: handleToPort(srcHandle), from: e.source, from_port: portKeyFor(srcNode, srcHandle),
to: e.target, to_port: handleToPort(tgtHandle), to: e.target, to_port: portKeyFor(tgtNode, tgtHandle),
format, format,
} }
}) })
+47 -7
View File
@@ -45,10 +45,22 @@ export const TRANSITION_OPTIONS = [
{ label: 'Push', value: 'push' }, { 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) // Format params shown on source nodes; values go into edge format (not node params)
const FORMAT_PARAMS: ParamDef[] = [ export const FORMAT_OPTIONS_VIDEO: ParamDef[] = [
{ key: 'res', label: 'Resolution', type: 'select', options: RESOLUTION_OPTIONS, default: '1920x1080', isFormat: true }, { 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 }, { 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<string, NodeTypeDef> = { export const NODE_TYPES: Record<string, NodeTypeDef> = {
@@ -60,9 +72,9 @@ export const NODE_TYPES: Record<string, NodeTypeDef> = {
{ id: 'audio-out', kind: 'audio', direction: 'out' }, { id: 'audio-out', kind: 'audio', direction: 'out' },
], ],
params: [ params: [
...FORMAT_PARAMS, ...FORMAT_OPTIONS_VIDEO,
...FORMAT_OPTIONS_AUDIO,
{ key: 'pattern', 'label': 'Pattern', type: 'select', options: PATTERN_OPTIONS, default: 'bars' }, { key: 'pattern', 'label': 'Pattern', type: 'select', options: PATTERN_OPTIONS, default: 'bars' },
{ key: 'channels', label: 'Channels', type: 'number', default: 0, min: 0, max: 16 }
], ],
}, },
ndiin: { ndiin: {
@@ -74,7 +86,8 @@ export const NODE_TYPES: Record<string, NodeTypeDef> = {
], ],
params: [ params: [
{ key: 'source_num', label: 'NDI Source #', type: 'number', default: 0, min: 0, max: 15 }, { key: 'source_num', label: 'NDI Source #', type: 'number', default: 0, min: 0, max: 15 },
...FORMAT_PARAMS, ...FORMAT_OPTIONS_VIDEO,
...FORMAT_OPTIONS_AUDIO,
], ],
}, },
decklinkin: { decklinkin: {
@@ -86,7 +99,8 @@ export const NODE_TYPES: Record<string, NodeTypeDef> = {
], ],
params: [ params: [
{ key: 'device_index', label: 'Port', type: 'select', options: DEVICE_OPTIONS, default: 0 }, { key: 'device_index', label: 'Port', type: 'select', options: DEVICE_OPTIONS, default: 0 },
...FORMAT_PARAMS, ...FORMAT_OPTIONS_VIDEO,
...FORMAT_OPTIONS_AUDIO,
], ],
}, },
ndiout: { ndiout: {
@@ -145,6 +159,32 @@ export const NODE_TYPES: Record<string, NodeTypeDef> = {
{ key: 'duration', label: 'Duration', type: 'number', default: 500, min: 0, max: 10000 }, { 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 // Default params for a new node of the given type
+3 -1
View File
@@ -1,9 +1,10 @@
export type PortKind = 'video' | 'audio' export type PortKind = 'video' | 'audio'
export interface PortDef { export interface PortDef {
id: string // 'video-out', 'audio-in', etc. id: string // 'video-out', 'audio-in', etc. (UI handle id)
kind: PortKind kind: PortKind
direction: 'in' | 'out' direction: 'in' | 'out'
portKey?: string // backend graph JSON key; defaults to handleToPort(id)
} }
export interface ParamDef { export interface ParamDef {
@@ -15,6 +16,7 @@ export interface ParamDef {
min?: number min?: number
max?: number max?: number
isFormat?: boolean // true → goes into edge format (not node params) 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 { export interface NodeTypeDef {