Improve GraphFX UI editing and image previews

This commit is contained in:
2026-06-19 10:22:04 +02:00
parent 7927b34c5f
commit 8c58aaae2b
13 changed files with 1151 additions and 659 deletions

View File

@@ -2,6 +2,7 @@
import { Handle, Position } from '@vue-flow/core'
import type { GraphFxNodeData, GraphFxPort } from '../lib/graphfxToVueFlow'
import { getInputHandleId, getOutputHandleId } from '../lib/graphfxToVueFlow'
import { displayIoValue } from '../lib/editorHelpers'
const props = defineProps<{
data: GraphFxNodeData
@@ -11,25 +12,94 @@ function typeClass(port: GraphFxPort) {
return `io-type-${port.type.toLowerCase()}`
}
function valuePreview(port: GraphFxPort) {
if (port.output) return ''
if (port.value === null || port.value === undefined) return ''
if (typeof port.value === 'string') return port.value.length > 24 ? `${port.value.slice(0, 24)}` : port.value
if (typeof port.value === 'number' || typeof port.value === 'boolean') return String(port.value)
if (typeof port.value === 'object') return 'object'
return String(port.value)
}
function hasCustomLabel(port: GraphFxPort) {
return port.label && port.label !== port.name
}
function labelValue(port: GraphFxPort) {
return hasCustomLabel(port) ? port.label : ''
}
function previewUrl(direction: 'in' | 'out', port: GraphFxPort) {
return props.data.imagePreviews?.[`${direction}:${port.name}`] || port.previewUrl || ''
}
function hasSelectOptions(port: GraphFxPort) {
return Boolean(port.options?.length)
}
function updateNodeId(event: Event) {
const target = event.target as HTMLInputElement
props.data.actions?.renameNodeId(props.data.id, target.value)
}
function updateLabel(direction: 'in' | 'out', port: GraphFxPort, event: Event) {
const target = event.target as HTMLInputElement
props.data.actions?.updateIoLabel(props.data.id, direction, port.name, target.value)
}
function updateInput(port: GraphFxPort, event: Event) {
const target = event.target as HTMLInputElement | HTMLSelectElement
const value = port.type === 'Boolean' ? (target as HTMLInputElement).checked : target.value
props.data.actions?.updateInputValue(props.data.id, port.name, value)
}
function selectImage(port: GraphFxPort, event: Event) {
const target = event.target as HTMLSelectElement
if (!target.value) return
props.data.actions?.selectInputImage(props.data.id, port.name, target.value)
}
function selectedImage(port: GraphFxPort) {
return props.data.selectedImages?.[port.name] || ''
}
function deleteNode() {
props.data.actions?.deleteNode(props.data.id)
}
function duplicateNode() {
props.data.actions?.duplicateNode(props.data.id)
}
function disconnectInput(port: GraphFxPort) {
props.data.actions?.disconnectInput(props.data.id, port.name)
}
function inputType(port: GraphFxPort) {
if (port.type === 'Number') return 'number'
if (port.type === 'Color') return 'color'
return 'text'
}
function connectedText(port: GraphFxPort) {
return port.connectedDisplay || port.output || 'connected'
}
</script>
<template>
<article class="graphfx-node">
<header class="graphfx-node__header">
<h3>{{ data.name }}</h3>
<small>{{ data.id }}</small>
<div>
<h3>{{ data.name }}</h3>
<input
class="graphfx-node__id nodrag"
:value="data.id"
aria-label="Node id"
title="Node id"
@change="updateNodeId"
@pointerdown.stop
@keydown.stop
/>
</div>
<details class="graphfx-node__menu nodrag" @pointerdown.stop @click.stop>
<summary title="Node actions"></summary>
<div class="graphfx-node__menu-popover">
<button type="button" class="secondary" @click="duplicateNode">Duplicate</button>
<button type="button" class="danger" @click="deleteNode">Delete</button>
</div>
</details>
</header>
<section class="graphfx-node__ports" :class="{ 'graphfx-node__ports--empty': !data.inputs.length && !data.outputs.length }">
@@ -48,9 +118,75 @@ function hasCustomLabel(port: GraphFxPort) {
:position="Position.Left"
class="graphfx-node__handle graphfx-node__handle--input"
/>
<span class="graphfx-node__port-name">{{ port.name }}</span>
<span v-if="hasCustomLabel(port)" class="graphfx-node__port-label">{{ port.label }}</span>
<span v-if="valuePreview(port)" class="graphfx-node__port-value">{{ valuePreview(port) }}</span>
<div class="graphfx-node__port-head">
<span class="graphfx-node__port-name">{{ port.name }}</span>
<span v-if="hasCustomLabel(port)" class="graphfx-node__label-chip">{{ port.label }}</span>
<span class="graphfx-node__port-type">{{ port.type }}</span>
</div>
<img v-if="port.type === 'Image' && previewUrl('in', port)" class="graphfx-node__image-preview" :src="previewUrl('in', port)" :alt="`${port.name} preview`" />
<div v-if="port.output" class="graphfx-node__connected-value nodrag" @pointerdown.stop>
<span :title="port.output"> {{ connectedText(port) }}</span>
<button type="button" class="ghost danger" title="Disconnect input" @click="disconnectInput(port)">×</button>
</div>
<template v-else>
<select
v-if="port.type === 'Image'"
class="graphfx-node__value-input nodrag"
:value="selectedImage(port)"
:disabled="!data.imageOptions?.length"
title="Use uploaded image as this input"
@change="selectImage(port, $event)"
@pointerdown.stop
@keydown.stop
>
<option value="">{{ data.imageOptions?.length ? 'choose uploaded image' : 'upload images first' }}</option>
<option v-for="image in data.imageOptions" :key="image.id" :value="image.id">{{ image.name }}</option>
</select>
<select
v-else-if="hasSelectOptions(port)"
class="graphfx-node__value-input nodrag"
:value="displayIoValue(port.value)"
title="Input value"
@change="updateInput(port, $event)"
@pointerdown.stop
@keydown.stop
>
<option v-for="option in port.options" :key="option" :value="option">{{ option }}</option>
</select>
<label v-else-if="port.type === 'Boolean'" class="graphfx-node__checkbox nodrag" @pointerdown.stop>
<input :checked="Boolean(port.value)" type="checkbox" @change="updateInput(port, $event)" />
{{ Boolean(port.value) ? 'true' : 'false' }}
</label>
<input
v-else
class="graphfx-node__value-input nodrag"
:type="inputType(port)"
:value="displayIoValue(port.value)"
title="Input value"
@change="updateInput(port, $event)"
@pointerdown.stop
@keydown.stop
/>
</template>
<details class="graphfx-node__label-editor nodrag" @pointerdown.stop @click.stop>
<summary title="Edit input label">label</summary>
<input
class="graphfx-node__label-input"
:value="labelValue(port)"
placeholder="optional label"
title="Input label"
@change="updateLabel('in', port, $event)"
@keydown.stop
/>
</details>
</div>
</div>
@@ -63,8 +199,26 @@ function hasCustomLabel(port: GraphFxPort) {
:class="[typeClass(port), { 'graphfx-node__port--labeled': hasCustomLabel(port) }]"
:title="`${port.name} · ${port.type}`"
>
<span class="graphfx-node__port-name">{{ port.name }}</span>
<span v-if="hasCustomLabel(port)" class="graphfx-node__port-label">{{ port.label }}</span>
<div class="graphfx-node__port-head graphfx-node__port-head--right">
<span class="graphfx-node__port-type">{{ port.type }}</span>
<span v-if="hasCustomLabel(port)" class="graphfx-node__label-chip">{{ port.label }}</span>
<span class="graphfx-node__port-name">{{ port.name }}</span>
</div>
<img v-if="port.type === 'Image' && previewUrl('out', port)" class="graphfx-node__image-preview" :src="previewUrl('out', port)" :alt="`${port.name} preview`" />
<details class="graphfx-node__label-editor nodrag" @pointerdown.stop @click.stop>
<summary title="Edit output label">label</summary>
<input
class="graphfx-node__label-input"
:value="labelValue(port)"
placeholder="optional label"
title="Output label"
@change="updateLabel('out', port, $event)"
@keydown.stop
/>
</details>
<Handle
type="source"
:id="getOutputHandleId(data.id, port.name)"