Files
ComfyUI_frontend/src/utils/mathUtil.ts
Terry Jia 63eab15c4f Range editor (#10936)
BE change https://github.com/Comfy-Org/ComfyUI/pull/13322

## Summary
Add RANGE widget for image levels adjustment       
- Add RangeEditor widget with three display modes: plain, gradient, and
histogram
- Support optional midpoint (gamma) control for non-linear midtone
adjustment
- Integrate histogram display from upstream node outputs

## Screenshots (if applicable)
<img width="1450" height="715" alt="image"
src="https://github.com/user-attachments/assets/864976af-9eb7-4dd0-9ce1-2f5d7f003117"
/>
<img width="1431" height="701" alt="image"
src="https://github.com/user-attachments/assets/7ee2af65-f87a-407b-8bf2-6ec59a1dff59"
/>
<img width="705" height="822" alt="image"
src="https://github.com/user-attachments/assets/7bcb8f17-795f-498a-9f8a-076ed6c05a98"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10936-Range-editor-33b6d73d365081089e8be040b40f6c8a)
by [Unito](https://www.unito.io)
2026-04-09 18:37:40 -07:00

141 lines
3.7 KiB
TypeScript

import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
import type { Bounds } from '@/renderer/core/layout/types'
/**
* Linearly maps a value from [min, max] to [0, 1].
* Returns 0 when min equals max to avoid division by zero.
*/
export function normalize(value: number, min: number, max: number): number {
return max === min ? 0 : (value - min) / (max - min)
}
/**
* Linearly maps a normalized value from [0, 1] back to [min, max].
*/
export function denormalize(
normalized: number,
min: number,
max: number
): number {
return min + normalized * (max - min)
}
/** Simple 2D point or size as [x, y] or [width, height] */
type Vec2 = readonly [number, number]
/**
* Finds the greatest common divisor (GCD) for two numbers using iterative
* Euclidean algorithm. Uses iteration instead of recursion to avoid stack
* overflow with large inputs or small floating-point step values.
*
* For floating-point numbers, uses a tolerance-based approach to handle
* precision issues and limits iterations to prevent hangs.
*
* @param a - The first number.
* @param b - The second number.
* @returns The GCD of the two numbers.
*/
export const gcd = (a: number, b: number): number => {
// Use absolute values to handle negative numbers
let x = Math.abs(a)
let y = Math.abs(b)
// Handle edge cases
if (x === 0) return y
if (y === 0) return x
// For floating-point numbers, use tolerance-based comparison
// This prevents infinite loops due to floating-point precision issues
const epsilon = 1e-10
const maxIterations = 100
let iterations = 0
while (y > epsilon && iterations < maxIterations) {
;[x, y] = [y, x % y]
iterations++
}
return x
}
/**
* Finds the least common multiple (LCM) for two numbers.
*
* @param a - The first number.
* @param b - The second number.
* @returns The LCM of the two numbers.
*/
export const lcm = (a: number, b: number): number => {
return Math.abs(a * b) / gcd(a, b)
}
/**
* Computes the union (bounding box) of multiple rectangles using a single-pass algorithm.
*
* Finds the minimum and maximum x/y coordinates across all rectangles to create
* a single bounding rectangle that contains all input rectangles. Optimized for
* performance with V8-friendly tuple access patterns.
*
* @param rectangles - Array of rectangle tuples in [x, y, width, height] format
* @returns Bounds object with union rectangle, or null if no rectangles provided
*/
export function computeUnionBounds(
rectangles: readonly ReadOnlyRect[]
): Bounds | null {
const n = rectangles.length
if (n === 0) {
return null
}
const r0 = rectangles[0]
let minX = r0[0]
let minY = r0[1]
let maxX = minX + r0[2]
let maxY = minY + r0[3]
for (let i = 1; i < n; i++) {
const r = rectangles[i]
const x1 = r[0]
const y1 = r[1]
const x2 = x1 + r[2]
const y2 = y1 + r[3]
if (x1 < minX) minX = x1
if (y1 < minY) minY = y1
if (x2 > maxX) maxX = x2
if (y2 > maxY) maxY = y2
}
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
}
}
/**
* Checks if any item with pos/size overlaps a rectangle (AABB test).
* @param items Items with pos [x, y] and size [width, height]
* @param rect Rectangle as [x, y, width, height]
* @returns `true` if any item overlaps the rect
*/
export function anyItemOverlapsRect(
items: Iterable<{ pos: Vec2; size: Vec2 }>,
rect: ReadOnlyRect
): boolean {
const rectRight = rect[0] + rect[2]
const rectBottom = rect[1] + rect[3]
for (const item of items) {
const overlaps =
item.pos[0] < rectRight &&
item.pos[0] + item.size[0] > rect[0] &&
item.pos[1] < rectBottom &&
item.pos[1] + item.size[1] > rect[1]
if (overlaps) return true
}
return false
}