mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-28 10:37:16 +00:00
Compare commits
7 Commits
v1.41.13
...
fix/codera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d35bd7e5b9 | ||
|
|
55b8236c8d | ||
|
|
5e17bbbf85 | ||
|
|
7cb07f9b2d | ||
|
|
a0abe3e36f | ||
|
|
96fd25de5c | ||
|
|
e2cb3560cc |
@@ -29,11 +29,23 @@ test.describe(
|
||||
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
|
||||
// 'workflow.avif'
|
||||
]
|
||||
const filesWithUpload = new Set(['no_workflow.webp'])
|
||||
|
||||
fileNames.forEach(async (fileName) => {
|
||||
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.dragDrop.dragAndDropFile(`workflowInMedia/${fileName}`)
|
||||
const waitForUpload = filesWithUpload.has(fileName)
|
||||
await comfyPage.dragDrop.dragAndDropFile(
|
||||
`workflowInMedia/${fileName}`,
|
||||
{ waitForUpload }
|
||||
)
|
||||
if (waitForUpload) {
|
||||
await comfyPage.page.waitForResponse(
|
||||
(resp) => resp.url().includes('/view') && resp.status() !== 0,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
}
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
|
||||
})
|
||||
})
|
||||
|
||||
14
src/App.vue
14
src/App.vue
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<router-view />
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<Loader size="lg" class="text-white" />
|
||||
</div>
|
||||
<GlobalDialog />
|
||||
<BlockUI full-screen :blocked="isLoading" />
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="pointer-events-none fixed inset-0 z-1200 flex items-center justify-center"
|
||||
>
|
||||
<LogoComfyWaveLoader size="xl" color="yellow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -15,7 +15,7 @@ import { captureException } from '@sentry/vue'
|
||||
import BlockUI from 'primevue/blockui'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import Loader from '@/components/common/Loader.vue'
|
||||
import LogoComfyWaveLoader from '@/components/loader/LogoComfyWaveLoader.vue'
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
@@ -9,133 +9,686 @@ import {
|
||||
} from './curveUtils'
|
||||
|
||||
describe('createMonotoneInterpolator', () => {
|
||||
it('returns 0 for empty points', () => {
|
||||
const interpolate = createMonotoneInterpolator([])
|
||||
expect(interpolate(0.5)).toBe(0)
|
||||
describe('degenerate inputs', () => {
|
||||
it('returns 0 for empty points', () => {
|
||||
const interpolate = createMonotoneInterpolator([])
|
||||
expect(interpolate(0)).toBe(0)
|
||||
expect(interpolate(0.5)).toBe(0)
|
||||
expect(interpolate(1)).toBe(0)
|
||||
})
|
||||
|
||||
it('returns constant for single point', () => {
|
||||
const interpolate = createMonotoneInterpolator([[0.5, 0.7]])
|
||||
expect(interpolate(0)).toBe(0.7)
|
||||
expect(interpolate(0.5)).toBe(0.7)
|
||||
expect(interpolate(1)).toBe(0.7)
|
||||
})
|
||||
|
||||
it('handles two points as linear segment', () => {
|
||||
const interpolate = createMonotoneInterpolator([
|
||||
[0, 0],
|
||||
[1, 1]
|
||||
])
|
||||
expect(interpolate(0)).toBeCloseTo(0, 5)
|
||||
expect(interpolate(0.25)).toBeCloseTo(0.25, 5)
|
||||
expect(interpolate(0.5)).toBeCloseTo(0.5, 5)
|
||||
expect(interpolate(0.75)).toBeCloseTo(0.75, 5)
|
||||
expect(interpolate(1)).toBeCloseTo(1, 5)
|
||||
})
|
||||
|
||||
it('handles duplicate x values', () => {
|
||||
const interpolate = createMonotoneInterpolator([
|
||||
[0.5, 0.2],
|
||||
[0.5, 0.8]
|
||||
])
|
||||
// Should not throw or produce NaN
|
||||
const y = interpolate(0.5)
|
||||
expect(Number.isFinite(y)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('returns constant for single point', () => {
|
||||
const interpolate = createMonotoneInterpolator([[0.5, 0.7]])
|
||||
expect(interpolate(0)).toBe(0.7)
|
||||
expect(interpolate(1)).toBe(0.7)
|
||||
describe('pass-through and interpolation', () => {
|
||||
it('passes through control points exactly', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.5, 0.8],
|
||||
[1, 1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
expect(interpolate(0)).toBeCloseTo(0, 5)
|
||||
expect(interpolate(0.5)).toBeCloseTo(0.8, 5)
|
||||
expect(interpolate(1)).toBeCloseTo(1, 5)
|
||||
})
|
||||
|
||||
it('passes through many control points exactly', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.1, 0.05],
|
||||
[0.3, 0.2],
|
||||
[0.5, 0.5],
|
||||
[0.7, 0.8],
|
||||
[0.9, 0.95],
|
||||
[1, 1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
for (const [x, y] of points) {
|
||||
expect(interpolate(x)).toBeCloseTo(y, 5)
|
||||
}
|
||||
})
|
||||
|
||||
it('interpolated values are between neighboring control points for monotone input', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.25, 0.1],
|
||||
[0.5, 0.5],
|
||||
[0.75, 0.9],
|
||||
[1, 1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
// Test midpoints between each pair
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const midX = (points[i][0] + points[i + 1][0]) / 2
|
||||
const y = interpolate(midX)
|
||||
const minY = Math.min(points[i][1], points[i + 1][1])
|
||||
const maxY = Math.max(points[i][1], points[i + 1][1])
|
||||
expect(y).toBeGreaterThanOrEqual(minY - 1e-10)
|
||||
expect(y).toBeLessThanOrEqual(maxY + 1e-10)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('passes through control points exactly', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.5, 0.8],
|
||||
[1, 1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
expect(interpolate(0)).toBeCloseTo(0, 5)
|
||||
expect(interpolate(0.5)).toBeCloseTo(0.8, 5)
|
||||
expect(interpolate(1)).toBeCloseTo(1, 5)
|
||||
describe('clamping and boundary behavior', () => {
|
||||
it('clamps to endpoint values outside range', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0.2, 0.3],
|
||||
[0.8, 0.9]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
expect(interpolate(0)).toBe(0.3)
|
||||
expect(interpolate(0.1)).toBe(0.3)
|
||||
expect(interpolate(1)).toBe(0.9)
|
||||
expect(interpolate(1.5)).toBe(0.9)
|
||||
})
|
||||
|
||||
it('clamps for negative x values', () => {
|
||||
const interpolate = createMonotoneInterpolator([
|
||||
[0, 0.5],
|
||||
[1, 1]
|
||||
])
|
||||
expect(interpolate(-0.5)).toBe(0.5)
|
||||
expect(interpolate(-100)).toBe(0.5)
|
||||
})
|
||||
|
||||
it('clamps for x values far beyond range', () => {
|
||||
const interpolate = createMonotoneInterpolator([
|
||||
[0, 0],
|
||||
[1, 1]
|
||||
])
|
||||
expect(interpolate(1000)).toBe(1)
|
||||
})
|
||||
|
||||
it('returns correct value at exact boundary x values', () => {
|
||||
const interpolate = createMonotoneInterpolator([
|
||||
[0, 0.2],
|
||||
[1, 0.8]
|
||||
])
|
||||
expect(interpolate(0)).toBeCloseTo(0.2, 5)
|
||||
expect(interpolate(1)).toBeCloseTo(0.8, 5)
|
||||
})
|
||||
})
|
||||
|
||||
it('clamps to endpoint values outside range', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0.2, 0.3],
|
||||
[0.8, 0.9]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
expect(interpolate(0)).toBe(0.3)
|
||||
expect(interpolate(1)).toBe(0.9)
|
||||
describe('monotonicity', () => {
|
||||
it('produces monotone increasing output for monotone increasing input', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.25, 0.2],
|
||||
[0.5, 0.5],
|
||||
[0.75, 0.8],
|
||||
[1, 1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
|
||||
let prev = -Infinity
|
||||
for (let x = 0; x <= 1; x += 0.001) {
|
||||
const y = interpolate(x)
|
||||
expect(y).toBeGreaterThanOrEqual(prev - 1e-10)
|
||||
prev = y
|
||||
}
|
||||
})
|
||||
|
||||
it('produces monotone decreasing output for monotone decreasing input', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 1],
|
||||
[0.25, 0.8],
|
||||
[0.5, 0.5],
|
||||
[0.75, 0.2],
|
||||
[1, 0]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
|
||||
let prev = Infinity
|
||||
for (let x = 0; x <= 1; x += 0.001) {
|
||||
const y = interpolate(x)
|
||||
expect(y).toBeLessThanOrEqual(prev + 1e-10)
|
||||
prev = y
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves monotonicity with steep transitions', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.1, 0.01],
|
||||
[0.5, 0.5],
|
||||
[0.9, 0.99],
|
||||
[1, 1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
|
||||
let prev = -Infinity
|
||||
for (let x = 0; x <= 1; x += 0.001) {
|
||||
const y = interpolate(x)
|
||||
expect(y).toBeGreaterThanOrEqual(prev - 1e-10)
|
||||
prev = y
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('produces monotone output for monotone input', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.25, 0.2],
|
||||
[0.5, 0.5],
|
||||
[0.75, 0.8],
|
||||
[1, 1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
describe('sorting and ordering', () => {
|
||||
it('handles unsorted input points', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[1, 1],
|
||||
[0, 0],
|
||||
[0.5, 0.5]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
expect(interpolate(0)).toBeCloseTo(0, 5)
|
||||
expect(interpolate(0.5)).toBeCloseTo(0.5, 5)
|
||||
expect(interpolate(1)).toBeCloseTo(1, 5)
|
||||
})
|
||||
|
||||
let prev = -Infinity
|
||||
for (let x = 0; x <= 1; x += 0.01) {
|
||||
const y = interpolate(x)
|
||||
expect(y).toBeGreaterThanOrEqual(prev)
|
||||
prev = y
|
||||
}
|
||||
it('produces same result regardless of input order', () => {
|
||||
const sorted: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.3, 0.4],
|
||||
[0.6, 0.7],
|
||||
[1, 1]
|
||||
]
|
||||
const reversed: CurvePoint[] = [...sorted].reverse() as CurvePoint[]
|
||||
const shuffled: CurvePoint[] = [
|
||||
sorted[2],
|
||||
sorted[0],
|
||||
sorted[3],
|
||||
sorted[1]
|
||||
]
|
||||
|
||||
const f1 = createMonotoneInterpolator(sorted)
|
||||
const f2 = createMonotoneInterpolator(reversed)
|
||||
const f3 = createMonotoneInterpolator(shuffled)
|
||||
|
||||
for (let x = 0; x <= 1; x += 0.05) {
|
||||
expect(f1(x)).toBeCloseTo(f2(x), 10)
|
||||
expect(f1(x)).toBeCloseTo(f3(x), 10)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('handles unsorted input points', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[1, 1],
|
||||
[0, 0],
|
||||
[0.5, 0.5]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
expect(interpolate(0)).toBeCloseTo(0, 5)
|
||||
expect(interpolate(0.5)).toBeCloseTo(0.5, 5)
|
||||
expect(interpolate(1)).toBeCloseTo(1, 5)
|
||||
})
|
||||
})
|
||||
describe('non-monotone and special curves', () => {
|
||||
it('handles flat segment (constant y)', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0.5],
|
||||
[0.5, 0.5],
|
||||
[1, 0.5]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
for (let x = 0; x <= 1; x += 0.1) {
|
||||
expect(interpolate(x)).toBeCloseTo(0.5, 5)
|
||||
}
|
||||
})
|
||||
|
||||
describe('curvesToLUT', () => {
|
||||
it('returns a 256-entry Uint8Array', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0],
|
||||
[1, 1]
|
||||
])
|
||||
expect(lut).toBeInstanceOf(Uint8Array)
|
||||
expect(lut.length).toBe(256)
|
||||
it('handles step-like curve with flat regions', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.4, 0],
|
||||
[0.6, 1],
|
||||
[1, 1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
// Flat regions should stay flat
|
||||
expect(interpolate(0)).toBeCloseTo(0, 5)
|
||||
expect(interpolate(0.2)).toBeCloseTo(0, 5)
|
||||
expect(interpolate(0.8)).toBeCloseTo(1, 5)
|
||||
expect(interpolate(1)).toBeCloseTo(1, 5)
|
||||
})
|
||||
|
||||
it('handles non-monotone (wave-like) input without NaN', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0.5],
|
||||
[0.25, 1],
|
||||
[0.5, 0.5],
|
||||
[0.75, 0],
|
||||
[1, 0.5]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
for (let x = 0; x <= 1; x += 0.01) {
|
||||
const y = interpolate(x)
|
||||
expect(Number.isFinite(y)).toBe(true)
|
||||
}
|
||||
// Should pass through control points
|
||||
expect(interpolate(0)).toBeCloseTo(0.5, 5)
|
||||
expect(interpolate(0.25)).toBeCloseTo(1, 5)
|
||||
expect(interpolate(0.5)).toBeCloseTo(0.5, 5)
|
||||
expect(interpolate(0.75)).toBeCloseTo(0, 5)
|
||||
expect(interpolate(1)).toBeCloseTo(0.5, 5)
|
||||
})
|
||||
|
||||
it('handles y values outside [0, 1]', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, -0.5],
|
||||
[0.5, 2],
|
||||
[1, -1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
expect(interpolate(0)).toBeCloseTo(-0.5, 5)
|
||||
expect(interpolate(0.5)).toBeCloseTo(2, 5)
|
||||
expect(interpolate(1)).toBeCloseTo(-1, 5)
|
||||
})
|
||||
})
|
||||
|
||||
it('produces identity LUT for diagonal curve', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0],
|
||||
[1, 1]
|
||||
])
|
||||
for (let i = 0; i < 256; i++) {
|
||||
expect(lut[i]).toBeCloseTo(i, 0)
|
||||
}
|
||||
describe('numerical precision', () => {
|
||||
it('handles very closely spaced x values', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.0001, 0.5],
|
||||
[0.0002, 1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
expect(interpolate(0)).toBeCloseTo(0, 5)
|
||||
expect(interpolate(0.0001)).toBeCloseTo(0.5, 5)
|
||||
expect(interpolate(0.0002)).toBeCloseTo(1, 5)
|
||||
expect(Number.isFinite(interpolate(0.00015))).toBe(true)
|
||||
})
|
||||
|
||||
it('handles very large y values', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.5, 1e6],
|
||||
[1, 0]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
expect(interpolate(0.5)).toBeCloseTo(1e6, -1)
|
||||
expect(Number.isFinite(interpolate(0.25))).toBe(true)
|
||||
})
|
||||
|
||||
it('returns finite values for all inputs across dense sampling', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.2, 0.3],
|
||||
[0.4, 0.7],
|
||||
[0.6, 0.4],
|
||||
[0.8, 0.9],
|
||||
[1, 1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
for (let x = -0.1; x <= 1.1; x += 0.001) {
|
||||
expect(Number.isFinite(interpolate(x))).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('clamps output to [0, 255]', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0],
|
||||
[0.5, 1.5],
|
||||
[1, 1]
|
||||
])
|
||||
for (let i = 0; i < 256; i++) {
|
||||
expect(lut[i]).toBeGreaterThanOrEqual(0)
|
||||
expect(lut[i]).toBeLessThanOrEqual(255)
|
||||
}
|
||||
describe('slope limiting (Fritsch-Carlson condition)', () => {
|
||||
it('limits slopes when alpha^2 + beta^2 > 9', () => {
|
||||
// Create points where slopes would be excessively steep
|
||||
// without the Fritsch-Carlson limiting
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.1, 0.9],
|
||||
[0.2, 0.1],
|
||||
[0.3, 0.95],
|
||||
[1, 1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
// Should still produce finite, reasonable values
|
||||
for (let x = 0; x <= 1; x += 0.01) {
|
||||
const y = interpolate(x)
|
||||
expect(Number.isFinite(y)).toBe(true)
|
||||
}
|
||||
// Should pass through control points
|
||||
expect(interpolate(0)).toBeCloseTo(0, 5)
|
||||
expect(interpolate(0.1)).toBeCloseTo(0.9, 5)
|
||||
expect(interpolate(0.2)).toBeCloseTo(0.1, 5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('binary search correctness', () => {
|
||||
it('produces continuous output across segment boundaries', () => {
|
||||
const points: CurvePoint[] = [
|
||||
[0, 0],
|
||||
[0.25, 0.3],
|
||||
[0.5, 0.6],
|
||||
[0.75, 0.8],
|
||||
[1, 1]
|
||||
]
|
||||
const interpolate = createMonotoneInterpolator(points)
|
||||
// Test continuity at segment boundaries
|
||||
const eps = 1e-8
|
||||
for (const [x] of points.slice(1, -1)) {
|
||||
const left = interpolate(x - eps)
|
||||
const at = interpolate(x)
|
||||
const right = interpolate(x + eps)
|
||||
expect(Math.abs(left - at)).toBeLessThan(1e-4)
|
||||
expect(Math.abs(right - at)).toBeLessThan(1e-4)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('histogramToPath', () => {
|
||||
it('returns empty string for empty histogram', () => {
|
||||
expect(histogramToPath(new Uint32Array(0))).toBe('')
|
||||
describe('empty and zero inputs', () => {
|
||||
it('returns empty string for empty histogram', () => {
|
||||
expect(histogramToPath(new Uint32Array(0))).toBe('')
|
||||
})
|
||||
|
||||
it('returns empty string when all bins are zero', () => {
|
||||
expect(histogramToPath(new Uint32Array(256))).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
it('returns empty string when all bins are zero', () => {
|
||||
expect(histogramToPath(new Uint32Array(256))).toBe('')
|
||||
describe('path structure', () => {
|
||||
it('returns a closed SVG path starting at M0,1 and ending at L1,1 Z', () => {
|
||||
const histogram = new Uint32Array(256)
|
||||
for (let i = 0; i < 256; i++) histogram[i] = i + 1
|
||||
const path = histogramToPath(histogram)
|
||||
expect(path).toMatch(/^M0,1/)
|
||||
expect(path).toMatch(/L1,1 Z$/)
|
||||
})
|
||||
|
||||
it('generates exactly 258 segments (M + 256 L + L1,1 Z)', () => {
|
||||
const histogram = new Uint32Array(256)
|
||||
histogram.fill(50)
|
||||
const path = histogramToPath(histogram)
|
||||
const parts = path.split(' ')
|
||||
// M0,1 + 256 L segments + "L1,1" + "Z"
|
||||
expect(parts.length).toBe(259) // M0,1, L0,y, L..., L1,y, L1,1, Z
|
||||
})
|
||||
|
||||
it('x values span from 0 to 1 in 256 steps', () => {
|
||||
const histogram = new Uint32Array(256)
|
||||
histogram.fill(100)
|
||||
const path = histogramToPath(histogram)
|
||||
const segments = path.split(' ').filter((s) => s.startsWith('L'))
|
||||
// Remove the closing L1,1
|
||||
const dataSegments = segments.slice(0, -1)
|
||||
expect(dataSegments.length).toBe(256)
|
||||
|
||||
const firstX = parseFloat(dataSegments[0].substring(1).split(',')[0])
|
||||
const lastX = parseFloat(
|
||||
dataSegments[dataSegments.length - 1].substring(1).split(',')[0]
|
||||
)
|
||||
expect(firstX).toBeCloseTo(0, 5)
|
||||
expect(lastX).toBeCloseTo(1, 5)
|
||||
})
|
||||
})
|
||||
|
||||
it('returns a closed SVG path for valid histogram', () => {
|
||||
const histogram = new Uint32Array(256)
|
||||
for (let i = 0; i < 256; i++) histogram[i] = i + 1
|
||||
const path = histogramToPath(histogram)
|
||||
expect(path).toMatch(/^M0,1/)
|
||||
expect(path).toMatch(/L1,1 Z$/)
|
||||
describe('normalization', () => {
|
||||
it('normalizes using 99.5th percentile to suppress outliers', () => {
|
||||
const histogram = new Uint32Array(256)
|
||||
for (let i = 0; i < 256; i++) histogram[i] = 100
|
||||
histogram[255] = 100000
|
||||
const path = histogramToPath(histogram)
|
||||
// Most bins should map to y=0 (1 - 100/100 = 0) since
|
||||
// the 99.5th percentile is 100, not the outlier 100000
|
||||
const yValues = path
|
||||
.split(/[ML]/)
|
||||
.filter(Boolean)
|
||||
.map((s) => parseFloat(s.split(',')[1]))
|
||||
.filter((y) => !isNaN(y))
|
||||
const nearZero = yValues.filter((y) => Math.abs(y) < 0.01)
|
||||
expect(nearZero.length).toBeGreaterThan(200)
|
||||
})
|
||||
|
||||
it('clamps values exceeding the percentile max to y=0', () => {
|
||||
const histogram = new Uint32Array(256)
|
||||
histogram.fill(50)
|
||||
// Set last two bins to extreme values
|
||||
histogram[254] = 500
|
||||
histogram[255] = 10000
|
||||
const path = histogramToPath(histogram)
|
||||
// The outlier bins should have their y values clamped to 0
|
||||
// (1 - min(1, val/max) where val > max -> y = 0)
|
||||
const segments = path.split(' ').filter((s) => s.startsWith('L'))
|
||||
const lastDataSegment = segments[segments.length - 2] // second to last L (before L1,1)
|
||||
const y = parseFloat(lastDataSegment.split(',')[1])
|
||||
expect(y).toBe(0)
|
||||
})
|
||||
|
||||
it('uniform histogram produces uniform y values', () => {
|
||||
const histogram = new Uint32Array(256)
|
||||
histogram.fill(200)
|
||||
const path = histogramToPath(histogram)
|
||||
const yValues = path
|
||||
.split(/[ML]/)
|
||||
.filter(Boolean)
|
||||
.map((s) => parseFloat(s.split(',')[1]))
|
||||
.filter((y) => !isNaN(y))
|
||||
// Skip first (M0,1) and last (L1,1 Z) entries
|
||||
const dataYValues = yValues.slice(1, -1)
|
||||
// All should be near 0 (1 - 200/200 = 0)
|
||||
for (const y of dataYValues) {
|
||||
expect(y).toBeCloseTo(0, 5)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('normalizes using 99.5th percentile to suppress outliers', () => {
|
||||
const histogram = new Uint32Array(256)
|
||||
for (let i = 0; i < 256; i++) histogram[i] = 100
|
||||
histogram[255] = 100000
|
||||
const path = histogramToPath(histogram)
|
||||
// Most bins should map to y=0 (1 - 100/100 = 0) since
|
||||
// the 99.5th percentile is 100, not the outlier 100000
|
||||
const yValues = path
|
||||
.split(/[ML]/)
|
||||
.filter(Boolean)
|
||||
.map((s) => parseFloat(s.split(',')[1]))
|
||||
.filter((y) => !isNaN(y))
|
||||
const nearZero = yValues.filter((y) => Math.abs(y) < 0.01)
|
||||
expect(nearZero.length).toBeGreaterThan(200)
|
||||
describe('single-bin histograms', () => {
|
||||
it('handles histogram with only one non-zero bin', () => {
|
||||
const histogram = new Uint32Array(256)
|
||||
histogram[128] = 1000
|
||||
const path = histogramToPath(histogram)
|
||||
// The 99.5th percentile of a mostly-zero array may be 0,
|
||||
// but since max is sorted[floor(255*0.995)] = sorted[253],
|
||||
// and we have 255 zeros and 1 non-zero, sorted[253] = 0
|
||||
// So max=0, function returns ''
|
||||
expect(path).toBe('')
|
||||
})
|
||||
|
||||
it('handles histogram with a few non-zero bins', () => {
|
||||
const histogram = new Uint32Array(256)
|
||||
for (let i = 0; i < 10; i++) histogram[i] = 100
|
||||
const path = histogramToPath(histogram)
|
||||
// sorted[253] = 100 (non-zero bins sort to end), so path is generated
|
||||
// The non-zero bins should produce y < 1, zero bins produce y = 1
|
||||
expect(path).not.toBe('')
|
||||
expect(path).toMatch(/^M0,1/)
|
||||
})
|
||||
|
||||
it('returns path when enough bins are non-zero', () => {
|
||||
const histogram = new Uint32Array(256)
|
||||
// Fill all bins to ensure percentile > 0
|
||||
histogram.fill(1)
|
||||
histogram[0] = 100
|
||||
const path = histogramToPath(histogram)
|
||||
expect(path).not.toBe('')
|
||||
expect(path).toMatch(/^M0,1/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('y value range', () => {
|
||||
it('all y values are in [0, 1]', () => {
|
||||
const histogram = new Uint32Array(256)
|
||||
for (let i = 0; i < 256; i++)
|
||||
histogram[i] = Math.floor(Math.random() * 1000) + 1
|
||||
const path = histogramToPath(histogram)
|
||||
const yValues = path
|
||||
.split(/[ML]/)
|
||||
.filter(Boolean)
|
||||
.map((s) => parseFloat(s.split(',')[1]))
|
||||
.filter((y) => !isNaN(y))
|
||||
for (const y of yValues) {
|
||||
expect(y).toBeGreaterThanOrEqual(0)
|
||||
expect(y).toBeLessThanOrEqual(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('curvesToLUT', () => {
|
||||
describe('basic properties', () => {
|
||||
it('returns a 256-entry Uint8Array', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0],
|
||||
[1, 1]
|
||||
])
|
||||
expect(lut).toBeInstanceOf(Uint8Array)
|
||||
expect(lut.length).toBe(256)
|
||||
})
|
||||
|
||||
it('all values are in [0, 255]', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0],
|
||||
[0.5, 0.8],
|
||||
[1, 0.2]
|
||||
])
|
||||
for (let i = 0; i < 256; i++) {
|
||||
expect(lut[i]).toBeGreaterThanOrEqual(0)
|
||||
expect(lut[i]).toBeLessThanOrEqual(255)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('identity and constant curves', () => {
|
||||
it('produces identity LUT for diagonal curve', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0],
|
||||
[1, 1]
|
||||
])
|
||||
for (let i = 0; i < 256; i++) {
|
||||
expect(lut[i]).toBeCloseTo(i, 0)
|
||||
}
|
||||
})
|
||||
|
||||
it('produces constant LUT for flat curve', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0.5],
|
||||
[1, 0.5]
|
||||
])
|
||||
const expected = Math.round(0.5 * 255) // 128
|
||||
for (let i = 0; i < 256; i++) {
|
||||
expect(lut[i]).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
it('produces all-zero LUT for y=0 curve', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0],
|
||||
[1, 0]
|
||||
])
|
||||
for (let i = 0; i < 256; i++) {
|
||||
expect(lut[i]).toBe(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('produces all-255 LUT for y=1 curve', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 1],
|
||||
[1, 1]
|
||||
])
|
||||
for (let i = 0; i < 256; i++) {
|
||||
expect(lut[i]).toBe(255)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('clamping', () => {
|
||||
it('clamps output when interpolation exceeds 1', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0],
|
||||
[0.5, 1.5],
|
||||
[1, 1]
|
||||
])
|
||||
for (let i = 0; i < 256; i++) {
|
||||
expect(lut[i]).toBeLessThanOrEqual(255)
|
||||
}
|
||||
})
|
||||
|
||||
it('clamps output when interpolation goes below 0', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0],
|
||||
[0.5, -0.5],
|
||||
[1, 0]
|
||||
])
|
||||
for (let i = 0; i < 256; i++) {
|
||||
expect(lut[i]).toBeGreaterThanOrEqual(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('endpoint values', () => {
|
||||
it('first LUT entry matches curve y at x=0', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0.3],
|
||||
[1, 0.8]
|
||||
])
|
||||
expect(lut[0]).toBe(Math.round(0.3 * 255))
|
||||
})
|
||||
|
||||
it('last LUT entry matches curve y at x=1', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0.3],
|
||||
[1, 0.8]
|
||||
])
|
||||
expect(lut[255]).toBe(Math.round(0.8 * 255))
|
||||
})
|
||||
})
|
||||
|
||||
describe('monotonicity in LUT', () => {
|
||||
it('produces monotone increasing LUT for monotone increasing curve', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0],
|
||||
[0.25, 0.2],
|
||||
[0.5, 0.5],
|
||||
[0.75, 0.8],
|
||||
[1, 1]
|
||||
])
|
||||
for (let i = 1; i < 256; i++) {
|
||||
expect(lut[i]).toBeGreaterThanOrEqual(lut[i - 1])
|
||||
}
|
||||
})
|
||||
|
||||
it('produces monotone decreasing LUT for inverted curve', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 1],
|
||||
[0.5, 0.5],
|
||||
[1, 0]
|
||||
])
|
||||
for (let i = 1; i < 256; i++) {
|
||||
expect(lut[i]).toBeLessThanOrEqual(lut[i - 1])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles empty points (all zeros)', () => {
|
||||
const lut = curvesToLUT([])
|
||||
for (let i = 0; i < 256; i++) {
|
||||
expect(lut[i]).toBe(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('handles single point', () => {
|
||||
const lut = curvesToLUT([[0.5, 0.7]])
|
||||
const expected = Math.round(0.7 * 255) // 179
|
||||
for (let i = 0; i < 256; i++) {
|
||||
expect(lut[i]).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
it('values are rounded to nearest integer', () => {
|
||||
const lut = curvesToLUT([
|
||||
[0, 0.5],
|
||||
[1, 0.5]
|
||||
])
|
||||
// 0.5 * 255 = 127.5 -> rounds to 128
|
||||
expect(lut[0]).toBe(128)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import Loader from './Loader.vue'
|
||||
|
||||
const meta: Meta<typeof Loader> = {
|
||||
title: 'Components/Common/Loader',
|
||||
title: 'Components/Loader/Loader',
|
||||
component: Loader,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
96
src/components/loader/LogoCFillLoader.stories.ts
Normal file
96
src/components/loader/LogoCFillLoader.stories.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import LogoCFillLoader from './LogoCFillLoader.vue'
|
||||
|
||||
const meta: Meta<typeof LogoCFillLoader> = {
|
||||
title: 'Components/Loader/LogoCFillLoader',
|
||||
component: LogoCFillLoader,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
backgrounds: { default: 'dark' }
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['sm', 'md', 'lg', 'xl']
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: ['yellow', 'blue', 'white', 'black']
|
||||
},
|
||||
bordered: {
|
||||
control: 'boolean'
|
||||
},
|
||||
disableAnimation: {
|
||||
control: 'boolean'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const Small: Story = {
|
||||
args: { size: 'sm' }
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: { size: 'lg' }
|
||||
}
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
args: { size: 'xl' }
|
||||
}
|
||||
|
||||
export const NoBorder: Story = {
|
||||
args: { bordered: false }
|
||||
}
|
||||
|
||||
export const Static: Story = {
|
||||
args: { disableAnimation: true }
|
||||
}
|
||||
|
||||
export const BrandColors: Story = {
|
||||
render: () => ({
|
||||
components: { LogoCFillLoader },
|
||||
template: `
|
||||
<div class="flex items-end gap-12">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-400">Yellow</span>
|
||||
<LogoCFillLoader size="lg" color="yellow" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-400">Blue</span>
|
||||
<LogoCFillLoader size="lg" color="blue" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-400">White</span>
|
||||
<LogoCFillLoader size="lg" color="white" />
|
||||
</div>
|
||||
<div class="p-4 bg-white rounded" style="background: white">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-600">Black</span>
|
||||
<LogoCFillLoader size="lg" color="black" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const AllSizes: Story = {
|
||||
render: () => ({
|
||||
components: { LogoCFillLoader },
|
||||
template: `
|
||||
<div class="flex items-end gap-8">
|
||||
<LogoCFillLoader size="sm" color="yellow" />
|
||||
<LogoCFillLoader size="md" color="yellow" />
|
||||
<LogoCFillLoader size="lg" color="yellow" />
|
||||
<LogoCFillLoader size="xl" color="yellow" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
100
src/components/loader/LogoCFillLoader.vue
Normal file
100
src/components/loader/LogoCFillLoader.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<span role="status" :class="cn('inline-flex', colorClass)">
|
||||
<svg
|
||||
:width="Math.round(heightMap[size] * (VB_W / VB_H))"
|
||||
:height="heightMap[size]"
|
||||
:viewBox="`0 0 ${VB_W} ${VB_H}`"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<mask :id="maskId">
|
||||
<path :d="C_PATH" fill="white" />
|
||||
</mask>
|
||||
</defs>
|
||||
<path
|
||||
v-if="bordered"
|
||||
:d="C_PATH"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
opacity="0.4"
|
||||
/>
|
||||
<g :mask="`url(#${maskId})`">
|
||||
<rect
|
||||
:class="disableAnimation ? undefined : 'c-fill-rect'"
|
||||
:x="-BLEED"
|
||||
:y="-BLEED"
|
||||
:width="VB_W + BLEED * 2"
|
||||
:height="VB_H + BLEED * 2"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<span class="sr-only">{{ t('g.loading') }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useId, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const {
|
||||
size = 'md',
|
||||
color = 'black',
|
||||
bordered = true,
|
||||
disableAnimation = false
|
||||
} = defineProps<{
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||
color?: 'yellow' | 'blue' | 'white' | 'black'
|
||||
bordered?: boolean
|
||||
disableAnimation?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const maskId = `c-mask-${useId()}`
|
||||
|
||||
const VB_W = 185
|
||||
const VB_H = 201
|
||||
const BLEED = 1
|
||||
|
||||
// Larger than LogoComfyWaveLoader because the C logo is near-square (185×201)
|
||||
// while the COMFY wordmark is wide (879×284), so larger heights are needed
|
||||
// for visually comparable perceived size.
|
||||
const heightMap = { sm: 48, md: 80, lg: 120, xl: 200 } as const
|
||||
const colorMap = {
|
||||
yellow: 'text-brand-yellow',
|
||||
blue: 'text-brand-blue',
|
||||
white: 'text-white',
|
||||
black: 'text-black'
|
||||
} as const
|
||||
|
||||
const colorClass = computed(() => colorMap[color])
|
||||
|
||||
const C_PATH =
|
||||
'M42.1217 200.812C37.367 200.812 33.5304 199.045 31.0285 195.703C28.4569 192.27 27.7864 187.477 29.1882 182.557L34.8172 162.791C35.2661 161.217 34.9537 159.523 33.9747 158.214C32.9958 156.908 31.464 156.139 29.8371 156.139L13.6525 156.139C8.89521 156.139 5.05862 154.374 2.55797 151.032C-0.0136533 147.597-0.684085 142.804 0.71869 137.883L20.0565 70.289L22.1916 62.8625C25.0617 52.7847 35.5288 44.5943 45.528 44.5943L64.8938 44.5943C67.2048 44.5943 69.2376 43.0535 69.8738 40.8175L76.2782 18.3344C79.1454 8.26681 89.6127 0.0763962 99.6117 0.0763945L141.029 0.00258328L171.349-2.99253e-05C176.104-3.0756e-05 179.941 1.765 182.442 5.10626C185.013 8.53932 185.684 13.3324 184.282 18.2528L175.612 48.6947C172.746 58.7597 162.279 66.9475 152.28 66.9475L110.771 67.0265L91.4113 67.0265C89.1029 67.0265 87.0727 68.5647 86.4326 70.7983L70.2909 127.179C69.8394 128.756 70.1518 130.454 71.1334 131.763C72.1123 133.07 73.6441 133.839 75.2697 133.839C75.2736 133.839 102.699 133.785 102.699 133.785L132.929 133.785C137.685 133.785 141.522 135.55 144.023 138.892C146.594 142.327 147.265 147.12 145.862 152.041L137.192 182.478C134.326 192.545 123.859 200.733 113.86 200.733L72.3517 200.812L42.1217 200.812Z'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.c-fill-rect {
|
||||
animation: c-fill-up 2.5s cubic-bezier(0.25, 0, 0.3, 1) forwards;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@keyframes c-fill-up {
|
||||
0% {
|
||||
transform: translateY(calc(v-bind(VB_H) * 1px + v-bind(BLEED) * 1px));
|
||||
}
|
||||
100% {
|
||||
transform: translateY(calc(v-bind(BLEED) * -1px));
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.c-fill-rect {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
96
src/components/loader/LogoComfyWaveLoader.stories.ts
Normal file
96
src/components/loader/LogoComfyWaveLoader.stories.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import LogoComfyWaveLoader from './LogoComfyWaveLoader.vue'
|
||||
|
||||
const meta: Meta<typeof LogoComfyWaveLoader> = {
|
||||
title: 'Components/Loader/LogoComfyWaveLoader',
|
||||
component: LogoComfyWaveLoader,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
backgrounds: { default: 'dark' }
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['sm', 'md', 'lg', 'xl']
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: ['yellow', 'blue', 'white', 'black']
|
||||
},
|
||||
bordered: {
|
||||
control: 'boolean'
|
||||
},
|
||||
disableAnimation: {
|
||||
control: 'boolean'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const Small: Story = {
|
||||
args: { size: 'sm' }
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: { size: 'lg' }
|
||||
}
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
args: { size: 'xl' }
|
||||
}
|
||||
|
||||
export const NoBorder: Story = {
|
||||
args: { bordered: false }
|
||||
}
|
||||
|
||||
export const Static: Story = {
|
||||
args: { disableAnimation: true }
|
||||
}
|
||||
|
||||
export const BrandColors: Story = {
|
||||
render: () => ({
|
||||
components: { LogoComfyWaveLoader },
|
||||
template: `
|
||||
<div class="flex flex-col items-center gap-12">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-400">#F0FF41 (Yellow)</span>
|
||||
<LogoComfyWaveLoader size="lg" color="yellow" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-400">#172DD7 (Blue)</span>
|
||||
<LogoComfyWaveLoader size="lg" color="blue" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-400">White</span>
|
||||
<LogoComfyWaveLoader size="lg" color="white" />
|
||||
</div>
|
||||
<div class="p-4 bg-white rounded" style="background: white">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-600">Black</span>
|
||||
<LogoComfyWaveLoader size="lg" color="black" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const AllSizes: Story = {
|
||||
render: () => ({
|
||||
components: { LogoComfyWaveLoader },
|
||||
template: `
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<LogoComfyWaveLoader size="sm" color="yellow" />
|
||||
<LogoComfyWaveLoader size="md" color="yellow" />
|
||||
<LogoComfyWaveLoader size="lg" color="yellow" />
|
||||
<LogoComfyWaveLoader size="xl" color="yellow" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
111
src/components/loader/LogoComfyWaveLoader.vue
Normal file
111
src/components/loader/LogoComfyWaveLoader.vue
Normal file
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Loader from '@/components/common/Loader.vue'
|
||||
import Loader from '@/components/loader/Loader.vue'
|
||||
import StatusBadge from '@/components/common/StatusBadge.vue'
|
||||
import type { AssetDownload } from '@/stores/assetDownloadStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -905,6 +905,14 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
app.canvas.pasteFromClipboard()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.PasteFromClipboardWithConnect',
|
||||
icon: 'icon-[lucide--clipboard-paste]',
|
||||
label: () => t('Paste with Connect'),
|
||||
function: () => {
|
||||
app.canvas.pasteFromClipboard({ connectInputs: true })
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.SelectAll',
|
||||
icon: 'icon-[lucide--lasso-select]',
|
||||
@@ -919,6 +927,12 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Delete Selected Items',
|
||||
versionAdded: '1.10.5',
|
||||
function: () => {
|
||||
if (app.canvas.selectedItems.size === 0) {
|
||||
app.canvas.canvas.dispatchEvent(
|
||||
new CustomEvent('litegraph:no-items-selected', { bubbles: true })
|
||||
)
|
||||
return
|
||||
}
|
||||
app.canvas.deleteSelected()
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
|
||||
@@ -189,11 +189,10 @@ export function useWorkflowActionsMenu(
|
||||
|
||||
addItem({
|
||||
id: 'share',
|
||||
label: t('menuLabels.Share'),
|
||||
label: t('breadcrumbsMenu.share'),
|
||||
icon: 'icon-[comfy--send]',
|
||||
command: async () => {},
|
||||
disabled: true,
|
||||
visible: isRoot
|
||||
visible: false
|
||||
})
|
||||
|
||||
addItem({
|
||||
|
||||
@@ -3791,13 +3791,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
return
|
||||
}
|
||||
|
||||
private _noItemsSelected(): void {
|
||||
const event = new CustomEvent('litegraph:no-items-selected', {
|
||||
bubbles: true
|
||||
})
|
||||
this.canvas.dispatchEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* process a key event
|
||||
*/
|
||||
@@ -3842,31 +3835,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.node_panel?.close()
|
||||
this.options_panel?.close()
|
||||
if (this.node_panel || this.options_panel) block_default = true
|
||||
} else if (e.keyCode === 65 && e.ctrlKey) {
|
||||
// select all Control A
|
||||
this.selectItems()
|
||||
block_default = true
|
||||
} else if (e.keyCode === 67 && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
||||
// copy
|
||||
if (this.selected_nodes) {
|
||||
this.copyToClipboard()
|
||||
block_default = true
|
||||
}
|
||||
} else if (e.keyCode === 86 && (e.metaKey || e.ctrlKey)) {
|
||||
// paste
|
||||
this.pasteFromClipboard({ connectInputs: e.shiftKey })
|
||||
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
// delete or backspace
|
||||
// @ts-expect-error EventTarget.localName is not in standard types
|
||||
if (e.target.localName != 'input' && e.target.localName != 'textarea') {
|
||||
if (this.selectedItems.size === 0) {
|
||||
this._noItemsSelected()
|
||||
return
|
||||
}
|
||||
|
||||
this.deleteSelected()
|
||||
block_default = true
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
"uploadAlreadyInProgress": "Upload already in progress",
|
||||
"capture": "capture",
|
||||
"nodes": "Nodes",
|
||||
"nodesCount": "{count} nodes | {count} node | {count} nodes",
|
||||
"nodesCount": "{count} node | {count} nodes",
|
||||
"addNode": "Add a node...",
|
||||
"filterBy": "Filter by:",
|
||||
"filterByType": "Filter by {type}...",
|
||||
@@ -222,7 +222,7 @@
|
||||
"failed": "Failed",
|
||||
"cancelled": "Cancelled",
|
||||
"job": "Job",
|
||||
"asset": "{count} assets | {count} asset | {count} assets",
|
||||
"asset": "{count} asset | {count} assets",
|
||||
"untitled": "Untitled",
|
||||
"emDash": "—",
|
||||
"enabling": "Enabling {id}",
|
||||
@@ -1262,6 +1262,7 @@
|
||||
"Move Selected Nodes Right": "Move Selected Nodes Right",
|
||||
"Move Selected Nodes Up": "Move Selected Nodes Up",
|
||||
"Paste": "Paste",
|
||||
"Paste with Connect": "Paste with Connect",
|
||||
"Reset View": "Reset View",
|
||||
"Resize Selected Nodes": "Resize Selected Nodes",
|
||||
"Select All": "Select All",
|
||||
@@ -2603,7 +2604,8 @@
|
||||
"deleteWorkflow": "Delete Workflow",
|
||||
"deleteBlueprint": "Delete Blueprint",
|
||||
"enterNewName": "Enter new name",
|
||||
"missingNodesWarning": "Workflow contains unsupported nodes (highlighted red)."
|
||||
"missingNodesWarning": "Workflow contains unsupported nodes (highlighted red).",
|
||||
"share": "Share"
|
||||
},
|
||||
"shortcuts": {
|
||||
"shortcuts": "Shortcuts",
|
||||
@@ -3347,7 +3349,7 @@
|
||||
}
|
||||
},
|
||||
"errorOverlay": {
|
||||
"errorCount": "{count} ERRORS | {count} ERROR | {count} ERRORS",
|
||||
"errorCount": "{count} ERROR | {count} ERRORS",
|
||||
"seeErrors": "See Errors"
|
||||
},
|
||||
"help": {
|
||||
@@ -3357,7 +3359,7 @@
|
||||
"progressToast": {
|
||||
"importingModels": "Importing Models",
|
||||
"downloadingModel": "Downloading model...",
|
||||
"downloadsFailed": "{count} downloads failed | {count} download failed | {count} downloads failed",
|
||||
"downloadsFailed": "{count} download failed | {count} downloads failed",
|
||||
"allDownloadsCompleted": "All downloads completed",
|
||||
"noImportsInQueue": "No {filter} in queue",
|
||||
"failed": "Failed",
|
||||
@@ -3374,7 +3376,7 @@
|
||||
"exportingAssets": "Exporting Assets",
|
||||
"preparingExport": "Preparing export...",
|
||||
"exportError": "Export failed",
|
||||
"exportFailed": "{count} export failed | {count} export failed | {count} exports failed",
|
||||
"exportFailed": "{count} export failed | {count} exports failed",
|
||||
"allExportsCompleted": "All exports completed",
|
||||
"noExportsInQueue": "No {filter} exports in queue",
|
||||
"exportStarted": "Preparing ZIP download...",
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Loader from '@/components/common/Loader.vue'
|
||||
import Loader from '@/components/loader/Loader.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useJobActions } from '@/composables/queue/useJobActions'
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Loader from '@/components/common/Loader.vue'
|
||||
import Loader from '@/components/loader/Loader.vue'
|
||||
import HoneyToast from '@/components/honeyToast/HoneyToast.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type { AssetExport } from '@/stores/assetExportStore'
|
||||
|
||||
@@ -4,7 +4,7 @@ import Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Loader from '@/components/common/Loader.vue'
|
||||
import Loader from '@/components/loader/Loader.vue'
|
||||
import HoneyToast from '@/components/honeyToast/HoneyToast.vue'
|
||||
import ProgressToastItem from '@/components/toast/ProgressToastItem.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Loader from '@/components/common/Loader.vue'
|
||||
import Loader from '@/components/loader/Loader.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import VideoHelpDialog from '@/platform/assets/components/VideoHelpDialog.vue'
|
||||
|
||||
|
||||
@@ -208,5 +208,52 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
key: 'Escape'
|
||||
},
|
||||
commandId: 'Comfy.Graph.ExitSubgraph'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
key: 'a'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.SelectAll',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
key: 'c'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.CopySelected',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
key: 'v'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.PasteFromClipboard',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
key: 'v'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.PasteFromClipboardWithConnect',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'Delete'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.DeleteSelectedItems',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'Backspace'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.DeleteSelectedItems',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
vi.mock('@/scripts/app', () => {
|
||||
return {
|
||||
app: {
|
||||
canvas: {
|
||||
processKey: vi.fn()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn(() => [])
|
||||
@@ -36,13 +25,15 @@ function createTestKeyboardEvent(
|
||||
ctrlKey?: boolean
|
||||
altKey?: boolean
|
||||
metaKey?: boolean
|
||||
shiftKey?: boolean
|
||||
} = {}
|
||||
): KeyboardEvent {
|
||||
const {
|
||||
target = document.body,
|
||||
ctrlKey = false,
|
||||
altKey = false,
|
||||
metaKey = false
|
||||
metaKey = false,
|
||||
shiftKey = false
|
||||
} = options
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
@@ -50,6 +41,7 @@ function createTestKeyboardEvent(
|
||||
ctrlKey,
|
||||
altKey,
|
||||
metaKey,
|
||||
shiftKey,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
@@ -60,8 +52,10 @@ function createTestKeyboardEvent(
|
||||
return event
|
||||
}
|
||||
|
||||
describe('keybindingService - Event Forwarding', () => {
|
||||
describe('keybindingService - Canvas Keybindings', () => {
|
||||
let keybindingService: ReturnType<typeof useKeybindingService>
|
||||
let canvasContainer: HTMLDivElement
|
||||
let canvasChild: HTMLCanvasElement
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -76,94 +70,156 @@ describe('keybindingService - Event Forwarding', () => {
|
||||
typeof useDialogStore
|
||||
>)
|
||||
|
||||
canvasContainer = document.createElement('div')
|
||||
canvasContainer.id = 'graph-canvas-container'
|
||||
canvasChild = document.createElement('canvas')
|
||||
canvasContainer.appendChild(canvasChild)
|
||||
document.body.appendChild(canvasContainer)
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
keybindingService.registerCoreKeybindings()
|
||||
})
|
||||
|
||||
it('should forward Delete key to canvas when no keybinding exists', async () => {
|
||||
const event = createTestKeyboardEvent('Delete')
|
||||
afterEach(() => {
|
||||
canvasContainer.remove()
|
||||
})
|
||||
|
||||
it('should execute DeleteSelectedItems for Delete key on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('Delete', {
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).toHaveBeenCalledWith(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.DeleteSelectedItems'
|
||||
)
|
||||
})
|
||||
|
||||
it('should forward Backspace key to canvas when no keybinding exists', async () => {
|
||||
const event = createTestKeyboardEvent('Backspace')
|
||||
it('should execute DeleteSelectedItems for Backspace key on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('Backspace', {
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).toHaveBeenCalledWith(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.DeleteSelectedItems'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not forward Delete key when typing in input field', async () => {
|
||||
it('should not execute DeleteSelectedItems when typing in input field', async () => {
|
||||
const inputElement = document.createElement('input')
|
||||
const event = createTestKeyboardEvent('Delete', { target: inputElement })
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not forward Delete key when typing in textarea', async () => {
|
||||
it('should not execute DeleteSelectedItems when typing in textarea', async () => {
|
||||
const textareaElement = document.createElement('textarea')
|
||||
const event = createTestKeyboardEvent('Delete', { target: textareaElement })
|
||||
const event = createTestKeyboardEvent('Delete', {
|
||||
target: textareaElement
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not forward Delete key when canvas processKey is not available', async () => {
|
||||
// Temporarily replace processKey with undefined - testing edge case
|
||||
const originalProcessKey = vi.mocked(app.canvas).processKey
|
||||
vi.mocked(app.canvas).processKey = undefined!
|
||||
|
||||
const event = createTestKeyboardEvent('Delete')
|
||||
|
||||
try {
|
||||
await keybindingService.keybindHandler(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
// Restore processKey for other tests
|
||||
vi.mocked(app.canvas).processKey = originalProcessKey
|
||||
}
|
||||
})
|
||||
|
||||
it('should not forward Delete key when canvas is not available', async () => {
|
||||
const originalCanvas = vi.mocked(app).canvas
|
||||
vi.mocked(app).canvas = null!
|
||||
|
||||
const event = createTestKeyboardEvent('Delete')
|
||||
|
||||
try {
|
||||
await keybindingService.keybindHandler(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
// Restore canvas for other tests
|
||||
vi.mocked(app).canvas = originalCanvas
|
||||
}
|
||||
})
|
||||
|
||||
it('should not forward non-canvas keys', async () => {
|
||||
const event = createTestKeyboardEvent('Enter')
|
||||
it('should execute SelectAll for Ctrl+A on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('a', {
|
||||
ctrlKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.SelectAll'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not forward when modifier keys are pressed', async () => {
|
||||
const event = createTestKeyboardEvent('Delete', { ctrlKey: true })
|
||||
it('should execute CopySelected for Ctrl+C on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('c', {
|
||||
ctrlKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.CopySelected'
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute PasteFromClipboard for Ctrl+V on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('v', {
|
||||
ctrlKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.PasteFromClipboard'
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute PasteFromClipboardWithConnect for Ctrl+Shift+V on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('v', {
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.PasteFromClipboardWithConnect'
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute graph-canvas bindings by normalizing to graph-canvas-container', async () => {
|
||||
const event = createTestKeyboardEvent('=', {
|
||||
altKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.ZoomIn'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not execute graph-canvas bindings when target is outside canvas', async () => {
|
||||
const outsideDiv = document.createElement('div')
|
||||
document.body.appendChild(outsideDiv)
|
||||
|
||||
const event = createTestKeyboardEvent('=', {
|
||||
altKey: true,
|
||||
target: outsideDiv
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
outsideDiv.remove()
|
||||
})
|
||||
|
||||
it('should not execute canvas commands when target is outside canvas container', async () => {
|
||||
const outsideDiv = document.createElement('div')
|
||||
document.body.appendChild(outsideDiv)
|
||||
|
||||
const event = createTestKeyboardEvent('Delete', {
|
||||
target: outsideDiv
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
outsideDiv.remove()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
@@ -15,16 +14,6 @@ export function useKeybindingService() {
|
||||
const settingStore = useSettingStore()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function shouldForwardToCanvas(event: KeyboardEvent): boolean {
|
||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
const canvasKeys = ['Delete', 'Backspace']
|
||||
|
||||
return canvasKeys.includes(event.key)
|
||||
}
|
||||
|
||||
async function keybindHandler(event: KeyboardEvent) {
|
||||
const keyCombo = KeyComboImpl.fromEvent(event)
|
||||
if (keyCombo.isModifier) {
|
||||
@@ -44,7 +33,17 @@ export function useKeybindingService() {
|
||||
}
|
||||
|
||||
const keybinding = keybindingStore.getKeybinding(keyCombo)
|
||||
if (keybinding && keybinding.targetElementId !== 'graph-canvas') {
|
||||
if (keybinding) {
|
||||
const targetElementId =
|
||||
keybinding.targetElementId === 'graph-canvas'
|
||||
? 'graph-canvas-container'
|
||||
: keybinding.targetElementId
|
||||
if (targetElementId) {
|
||||
const container = document.getElementById(targetElementId)
|
||||
if (!container?.contains(target)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (
|
||||
event.key === 'Escape' &&
|
||||
!event.ctrlKey &&
|
||||
@@ -74,18 +73,6 @@ export function useKeybindingService() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!keybinding && shouldForwardToCanvas(event)) {
|
||||
const canvas = app.canvas
|
||||
if (
|
||||
canvas &&
|
||||
canvas.processKey &&
|
||||
typeof canvas.processKey === 'function'
|
||||
) {
|
||||
canvas.processKey(event)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
v-else
|
||||
class="fixed inset-0 z-1100 flex items-center justify-center bg-(--p-mask-background)"
|
||||
>
|
||||
<Loader size="lg" class="text-white" />
|
||||
<LogoComfyWaveLoader size="xl" color="yellow" disable-animation />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
* instead of workspace tokens when the workspace feature is enabled.
|
||||
*/
|
||||
import { promiseTimeout, until } from '@vueuse/core'
|
||||
import Loader from '@/components/common/Loader.vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
@@ -31,6 +30,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { refreshRemoteConfig } from '@/platform/remoteConfig/refreshRemoteConfig'
|
||||
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import LogoComfyWaveLoader from '@/components/loader/LogoComfyWaveLoader.vue'
|
||||
|
||||
const FIREBASE_INIT_TIMEOUT_MS = 16_000
|
||||
const CONFIG_REFRESH_TIMEOUT_MS = 10_000
|
||||
|
||||
@@ -5,7 +5,7 @@ import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Loader from '@/components/common/Loader.vue'
|
||||
import Loader from '@/components/loader/Loader.vue'
|
||||
import ScrubableNumberInput from '@/components/common/ScrubableNumberInput.vue'
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Loader from '@/components/common/Loader.vue'
|
||||
import Loader from '@/components/loader/Loader.vue'
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Loader from '@/components/common/Loader.vue'
|
||||
import Loader from '@/components/loader/Loader.vue'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import {
|
||||
|
||||
@@ -676,20 +676,6 @@ export class ComfyApp {
|
||||
e.stopImmediatePropagation()
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl+C Copy
|
||||
if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl+V Paste
|
||||
if (
|
||||
(e.key === 'v' || e.key == 'V') &&
|
||||
(e.metaKey || e.ctrlKey) &&
|
||||
!e.shiftKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fall through to Litegraph defaults
|
||||
|
||||
@@ -245,6 +245,13 @@ class ComfyList {
|
||||
this._reverse = reverse || false
|
||||
this.element = $el('div.comfy-list') as HTMLDivElement
|
||||
this.element.style.display = 'none'
|
||||
|
||||
console.warn(
|
||||
'[ComfyUI] The legacy queue/history menu is deprecated. ' +
|
||||
'Core functionality in this menu may break at any time. ' +
|
||||
'Issues and feature requests related to the legacy menu will not be addressed. ' +
|
||||
'To switch to the new menu: Settings → search "Use new menu" → change from "Disabled" to "Top".'
|
||||
)
|
||||
}
|
||||
|
||||
get visible() {
|
||||
|
||||
Reference in New Issue
Block a user