mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
## GPU accelerated brush engine for the mask editor - Full GPU acceleration using TypeGPU and type-safe shaders - Catmull-Rom Spline Smoothing - arc-length equidistant resampling - much improved performance, even for huge images - photoshop like opacity clamping for brush strokes - much improved soft brushes - fallback to CPU fully implemented, much improved CPU rendering features as well ### Tested Browsers - Chrome (fully supported) - Safari 26 (fully supported, prev versions CPU fallback) - Firefox (CPU fallback, flags needed for full support) https://github.com/user-attachments/assets/b7b5cb8a-2290-4a95-ae7d-180e11fccdb0 https://github.com/user-attachments/assets/4297aaa5-f249-499a-9b74-869677f1c73b https://github.com/user-attachments/assets/602b4783-3e2b-489e-bcb9-70534bcaac5e ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6767-GPU-accelerated-maskeditor-rendering-2b16d73d3650818cb294e1fca03f6169) by [Unito](https://www.unito.io)
109 lines
4.0 KiB
TypeScript
109 lines
4.0 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { StrokeProcessor } from './StrokeProcessor'
|
|
import type { Point } from '@/extensions/core/maskeditor/types'
|
|
|
|
describe('StrokeProcessor', () => {
|
|
it('should generate equidistant points from irregular input', () => {
|
|
const spacing = 10
|
|
const processor = new StrokeProcessor(spacing)
|
|
const outputPoints: Point[] = []
|
|
|
|
// Simulate a horizontal line drawn with irregular speed
|
|
// Points: (0,0) -> (5,0) -> (25,0) -> (30,0) -> (100,0)
|
|
const inputPoints: Point[] = [
|
|
{ x: 0, y: 0 },
|
|
{ x: 5, y: 0 }, // dist 5
|
|
{ x: 25, y: 0 }, // dist 20
|
|
{ x: 30, y: 0 }, // dist 5
|
|
{ x: 100, y: 0 } // dist 70
|
|
]
|
|
|
|
for (const p of inputPoints) {
|
|
outputPoints.push(...processor.addPoint(p))
|
|
}
|
|
outputPoints.push(...processor.endStroke())
|
|
|
|
// Verify we have points
|
|
expect(outputPoints.length).toBeGreaterThan(0)
|
|
|
|
// Verify spacing
|
|
// Note: The first few points might be affected by the start condition,
|
|
// but the middle section should be perfectly spaced.
|
|
// Also, Catmull-Rom splines don't necessarily pass through control points in a straight line
|
|
// if the points are collinear, they should be straight.
|
|
|
|
// Let's check distances between consecutive points
|
|
const distances: number[] = []
|
|
for (let i = 1; i < outputPoints.length; i++) {
|
|
const dx = outputPoints[i].x - outputPoints[i - 1].x
|
|
const dy = outputPoints[i].y - outputPoints[i - 1].y
|
|
distances.push(Math.hypot(dx, dy))
|
|
}
|
|
|
|
// Check that distances are close to spacing
|
|
// We allow a small epsilon because of floating point and spline approximation
|
|
// Filter out the very last segment which might be shorter (remainder)
|
|
// But wait, our logic doesn't output the last point if it's not a full spacing step?
|
|
// resampleSegment outputs points at [start + spacing, start + 2*spacing, ...]
|
|
// It does NOT output the end point of the segment.
|
|
// So all distances between output points should be exactly `spacing`.
|
|
// EXCEPT possibly if the spline curvature makes the straight-line distance slightly different
|
|
// from the arc length. But for a straight line input, it should be exact.
|
|
|
|
// However, catmull-rom with collinear points IS a straight line.
|
|
|
|
// Let's log the distances for debugging if test fails
|
|
// console.log('Distances:', distances)
|
|
|
|
// All distances should be approximately equal to spacing
|
|
// We might have a gap between segments if the logic isn't perfect,
|
|
// but within a segment it's guaranteed by resampleSegment.
|
|
// The critical part is the transition between segments.
|
|
|
|
for (let i = 0; i < distances.length; i++) {
|
|
const d = distances[i]
|
|
if (Math.abs(d - spacing) > 0.5) {
|
|
console.log(
|
|
`Distance mismatch at index ${i}: ${d} (expected ${spacing})`
|
|
)
|
|
console.log(`Point ${i}:`, outputPoints[i])
|
|
console.log(`Point ${i + 1}:`, outputPoints[i + 1])
|
|
}
|
|
expect(d).toBeCloseTo(spacing, 1)
|
|
}
|
|
})
|
|
|
|
it('should handle a simple 3-point stroke', () => {
|
|
const spacing = 5
|
|
const processor = new StrokeProcessor(spacing)
|
|
const points: Point[] = []
|
|
|
|
points.push(...processor.addPoint({ x: 0, y: 0 }))
|
|
points.push(...processor.addPoint({ x: 10, y: 0 }))
|
|
points.push(...processor.addPoint({ x: 20, y: 0 }))
|
|
points.push(...processor.endStroke())
|
|
|
|
expect(points.length).toBeGreaterThan(0)
|
|
|
|
// Check distances
|
|
for (let i = 1; i < points.length; i++) {
|
|
const dx = points[i].x - points[i - 1].x
|
|
const dy = points[i].y - points[i - 1].y
|
|
const d = Math.hypot(dx, dy)
|
|
expect(d).toBeCloseTo(spacing, 1)
|
|
}
|
|
})
|
|
|
|
it('should handle a single point click', () => {
|
|
const spacing = 5
|
|
const processor = new StrokeProcessor(spacing)
|
|
const points: Point[] = []
|
|
|
|
points.push(...processor.addPoint({ x: 100, y: 100 }))
|
|
points.push(...processor.endStroke())
|
|
|
|
expect(points.length).toBe(1)
|
|
expect(points[0]).toEqual({ x: 100, y: 100 })
|
|
})
|
|
})
|