mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
[backport core/1.42] feat: support histogram display in curve widget (#10561)
Backport of #10365 to `core/1.42` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10561-backport-core-1-42-feat-support-histogram-display-in-curve-widget-32f6d73d365081bfa21fd1d0ed01aa7a) by [Unito](https://www.unito.io) Co-authored-by: Terry Jia <terryjia88@gmail.com>
This commit is contained in:
@@ -22,19 +22,21 @@
|
||||
:model-value="effectiveCurve.points"
|
||||
:disabled="isDisabled"
|
||||
:interpolation="effectiveCurve.interpolation"
|
||||
:histogram="histogram"
|
||||
@update:model-value="onPointsChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import {
|
||||
singleValueExtractor,
|
||||
useUpstreamValue
|
||||
} from '@/composables/useUpstreamValue'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
|
||||
import Select from '@/components/ui/select/Select.vue'
|
||||
import SelectContent from '@/components/ui/select/SelectContent.vue'
|
||||
@@ -63,11 +65,27 @@ const modelValue = defineModel<CurveData>({
|
||||
|
||||
const isDisabled = computed(() => !!widget.options?.disabled)
|
||||
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
const histogram = computed(() => {
|
||||
const locatorId = widget.nodeLocatorId
|
||||
if (!locatorId) return null
|
||||
const output = nodeOutputStore.nodeOutputs[locatorId]
|
||||
const data = output?.histogram
|
||||
if (!Array.isArray(data) || data.length === 0) return null
|
||||
return new Uint32Array(data)
|
||||
})
|
||||
|
||||
const upstreamValue = useUpstreamValue(
|
||||
() => widget.linkedUpstream,
|
||||
singleValueExtractor(isCurveData)
|
||||
)
|
||||
|
||||
watch(upstreamValue, (upstream) => {
|
||||
if (isDisabled.value && upstream) {
|
||||
modelValue.value = upstream
|
||||
}
|
||||
})
|
||||
|
||||
const effectiveCurve = computed(() =>
|
||||
isDisabled.value && upstreamValue.value
|
||||
? upstreamValue.value
|
||||
|
||||
@@ -150,21 +150,27 @@ export function createMonotoneInterpolator(
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a 256-bin histogram into an SVG path string.
|
||||
* Normalizes using the 99.5th percentile to avoid outlier spikes.
|
||||
* Convert a histogram (arbitrary number of bins) into an SVG path string.
|
||||
* Applies square-root scaling and normalizes using the 99.5th percentile
|
||||
* to avoid outlier spikes.
|
||||
*/
|
||||
export function histogramToPath(histogram: Uint32Array): string {
|
||||
if (!histogram.length) return ''
|
||||
const len = histogram.length
|
||||
if (len === 0) return ''
|
||||
|
||||
const sorted = Array.from(histogram).sort((a, b) => a - b)
|
||||
const max = sorted[Math.floor(255 * 0.995)]
|
||||
const sqrtValues = new Float32Array(len)
|
||||
for (let i = 0; i < len; i++) sqrtValues[i] = Math.sqrt(histogram[i])
|
||||
|
||||
const sorted = Array.from(sqrtValues).sort((a, b) => a - b)
|
||||
const max = sorted[Math.floor((len - 1) * 0.995)]
|
||||
if (max === 0) return ''
|
||||
|
||||
const invMax = 1 / max
|
||||
const lastIdx = len - 1
|
||||
const parts: string[] = ['M0,1']
|
||||
for (let i = 0; i < 256; i++) {
|
||||
const x = i / 255
|
||||
const y = 1 - Math.min(1, histogram[i] * invMax)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const x = lastIdx === 0 ? 0.5 : i / lastIdx
|
||||
const y = 1 - Math.min(1, sqrtValues[i] * invMax)
|
||||
parts.push(`L${x},${y}`)
|
||||
}
|
||||
parts.push('L1,1 Z')
|
||||
|
||||
@@ -125,7 +125,10 @@ import type {
|
||||
WidgetValue
|
||||
} from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { getExecutionIdFromNodeData } from '@/utils/graphTraversalUtil'
|
||||
import {
|
||||
getExecutionIdFromNodeData,
|
||||
getLocatorIdFromNodeData
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
import InputSlot from './InputSlot.vue'
|
||||
@@ -407,6 +410,12 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
}
|
||||
: undefined
|
||||
|
||||
const nodeLocatorId = widget.nodeId
|
||||
? widget.nodeId
|
||||
: nodeData
|
||||
? getLocatorIdFromNodeData(nodeData)
|
||||
: undefined
|
||||
|
||||
const simplified: SimplifiedWidget = {
|
||||
name: widget.name,
|
||||
type: widget.type,
|
||||
@@ -416,6 +425,7 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
controlWidget: widget.controlWidget,
|
||||
label: widget.promotedLabel ?? widgetState?.label,
|
||||
linkedUpstream,
|
||||
nodeLocatorId,
|
||||
options: widgetOptions,
|
||||
spec: widget.spec
|
||||
}
|
||||
|
||||
@@ -76,6 +76,9 @@ export interface SimplifiedWidget<
|
||||
/** Optional serialization method for custom value handling */
|
||||
serializeValue?: () => unknown
|
||||
|
||||
/** NodeLocatorId for the node that owns this widget's execution outputs */
|
||||
nodeLocatorId?: string
|
||||
|
||||
/** Optional input specification backing this widget */
|
||||
spec?: InputSpecV2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user