Allow node resize from any corner or edge (#1063)

This commit is contained in:
filtered
2025-05-26 16:36:03 +10:00
committed by GitHub
parent 942758e3a5
commit 71928af112
9 changed files with 571 additions and 59 deletions

View File

@@ -0,0 +1,164 @@
import { beforeEach, describe, expect } from "vitest"
import { LGraphNode } from "@/LGraphNode"
import { LiteGraph } from "@/litegraph"
import { test } from "./testExtensions"
describe("LGraphNode resize functionality", () => {
let node: LGraphNode
beforeEach(() => {
// Set up LiteGraph constants needed for measure
LiteGraph.NODE_TITLE_HEIGHT = 20
node = new LGraphNode("Test Node")
node.pos = [100, 100]
node.size = [200, 150]
// Create a mock canvas context for updateArea
const mockCtx = {} as CanvasRenderingContext2D
// Call updateArea to populate boundingRect
node.updateArea(mockCtx)
})
describe("findResizeDirection", () => {
describe("corners", () => {
test("should detect NW (top-left) corner", () => {
// With title bar, top is at y=80 (100 - 20)
// Corner is from (100, 80) to (100 + 15, 80 + 15)
expect(node.findResizeDirection(100, 80)).toBe("NW")
expect(node.findResizeDirection(110, 90)).toBe("NW")
expect(node.findResizeDirection(114, 94)).toBe("NW")
})
test("should detect NE (top-right) corner", () => {
// Corner is from (300 - 15, 80) to (300, 80 + 15)
expect(node.findResizeDirection(285, 80)).toBe("NE")
expect(node.findResizeDirection(290, 90)).toBe("NE")
expect(node.findResizeDirection(299, 94)).toBe("NE")
})
test("should detect SW (bottom-left) corner", () => {
// Bottom is at y=250 (100 + 150)
// Corner is from (100, 250 - 15) to (100 + 15, 250)
expect(node.findResizeDirection(100, 235)).toBe("SW")
expect(node.findResizeDirection(110, 240)).toBe("SW")
expect(node.findResizeDirection(114, 249)).toBe("SW")
})
test("should detect SE (bottom-right) corner", () => {
// Corner is from (300 - 15, 250 - 15) to (300, 250)
expect(node.findResizeDirection(285, 235)).toBe("SE")
expect(node.findResizeDirection(290, 240)).toBe("SE")
expect(node.findResizeDirection(299, 249)).toBe("SE")
})
})
describe("edges", () => {
test("should detect N (top) edge", () => {
// Top edge at y=80, but not in corners
expect(node.findResizeDirection(150, 80)).toBe("N")
expect(node.findResizeDirection(150, 84)).toBe("N")
expect(node.findResizeDirection(200, 80)).toBe("N")
})
test("should detect S (bottom) edge", () => {
// Bottom edge at y=250, but need to check within the 5px threshold
expect(node.findResizeDirection(150, 249)).toBe("S")
expect(node.findResizeDirection(150, 246)).toBe("S")
expect(node.findResizeDirection(200, 247)).toBe("S")
})
test("should detect W (left) edge", () => {
// Left edge at x=100, but not in corners
expect(node.findResizeDirection(100, 150)).toBe("W")
expect(node.findResizeDirection(104, 150)).toBe("W")
expect(node.findResizeDirection(100, 200)).toBe("W")
})
test("should detect E (right) edge", () => {
// Right edge at x=300, but need to check within the 5px threshold
expect(node.findResizeDirection(299, 150)).toBe("E")
expect(node.findResizeDirection(296, 150)).toBe("E")
expect(node.findResizeDirection(298, 200)).toBe("E")
})
})
describe("priority", () => {
test("corners should have priority over edges", () => {
// These points are technically on both corner and edge
// Corner should win
expect(node.findResizeDirection(100, 84)).toBe("NW") // Not "W"
expect(node.findResizeDirection(104, 80)).toBe("NW") // Not "N"
})
})
describe("negative cases", () => {
test("should return undefined when outside node bounds", () => {
expect(node.findResizeDirection(50, 50)).toBeUndefined()
expect(node.findResizeDirection(350, 300)).toBeUndefined()
expect(node.findResizeDirection(99, 150)).toBeUndefined()
expect(node.findResizeDirection(301, 150)).toBeUndefined()
})
test("should return undefined when inside node but not on resize areas", () => {
// Center of node (accounting for title bar offset)
expect(node.findResizeDirection(200, 165)).toBeUndefined()
// Just inside the edge threshold
expect(node.findResizeDirection(106, 150)).toBeUndefined()
expect(node.findResizeDirection(294, 150)).toBeUndefined()
expect(node.findResizeDirection(150, 86)).toBeUndefined() // 80 + 6
expect(node.findResizeDirection(150, 244)).toBeUndefined()
})
test("should return undefined when node is not resizable", () => {
node.resizable = false
expect(node.findResizeDirection(100, 100)).toBeUndefined()
expect(node.findResizeDirection(300, 250)).toBeUndefined()
expect(node.findResizeDirection(150, 100)).toBeUndefined()
})
})
describe("edge cases", () => {
test("should handle nodes at origin", () => {
node.pos = [0, 0]
node.size = [100, 100]
// Update boundingRect with new position/size
const mockCtx = {} as CanvasRenderingContext2D
node.updateArea(mockCtx)
expect(node.findResizeDirection(0, -20)).toBe("NW") // Account for title bar
expect(node.findResizeDirection(99, 99)).toBe("SE") // Bottom-right corner (100-1, 100-1)
expect(node.findResizeDirection(50, -20)).toBe("N")
expect(node.findResizeDirection(0, 50)).toBe("W")
})
test("should handle very small nodes", () => {
node.size = [20, 20] // Smaller than corner size
// Update boundingRect with new size
const mockCtx = {} as CanvasRenderingContext2D
node.updateArea(mockCtx)
// Corners still work (accounting for title bar offset)
expect(node.findResizeDirection(100, 80)).toBe("NW")
expect(node.findResizeDirection(119, 119)).toBe("SE")
})
})
})
describe("resizeEdgeSize static property", () => {
test("should have default value of 5", () => {
expect(LGraphNode.resizeEdgeSize).toBe(5)
})
})
describe("resizeHandleSize static property", () => {
test("should have default value of 15", () => {
expect(LGraphNode.resizeHandleSize).toBe(15)
})
})
})

View File

@@ -0,0 +1,144 @@
import { beforeEach, describe, expect, test } from "vitest"
import { Rectangle } from "@/infrastructure/Rectangle"
describe("Rectangle resize functionality", () => {
let rect: Rectangle
beforeEach(() => {
rect = new Rectangle(100, 200, 300, 400) // x, y, width, height
// So: left=100, top=200, right=400, bottom=600
})
describe("findContainingCorner", () => {
const cornerSize = 15
test("should detect NW (top-left) corner", () => {
expect(rect.findContainingCorner(100, 200, cornerSize)).toBe("NW")
expect(rect.findContainingCorner(110, 210, cornerSize)).toBe("NW")
expect(rect.findContainingCorner(114, 214, cornerSize)).toBe("NW")
})
test("should detect NE (top-right) corner", () => {
// Top-right corner starts at (right - cornerSize, top) = (385, 200)
expect(rect.findContainingCorner(385, 200, cornerSize)).toBe("NE")
expect(rect.findContainingCorner(390, 210, cornerSize)).toBe("NE")
expect(rect.findContainingCorner(399, 214, cornerSize)).toBe("NE")
})
test("should detect SW (bottom-left) corner", () => {
// Bottom-left corner starts at (left, bottom - cornerSize) = (100, 585)
expect(rect.findContainingCorner(100, 585, cornerSize)).toBe("SW")
expect(rect.findContainingCorner(110, 590, cornerSize)).toBe("SW")
expect(rect.findContainingCorner(114, 599, cornerSize)).toBe("SW")
})
test("should detect SE (bottom-right) corner", () => {
// Bottom-right corner starts at (right - cornerSize, bottom - cornerSize) = (385, 585)
expect(rect.findContainingCorner(385, 585, cornerSize)).toBe("SE")
expect(rect.findContainingCorner(390, 590, cornerSize)).toBe("SE")
expect(rect.findContainingCorner(399, 599, cornerSize)).toBe("SE")
})
test("should return undefined when not in any corner", () => {
// Middle of rectangle
expect(rect.findContainingCorner(250, 400, cornerSize)).toBeUndefined()
// On edge but not in corner
expect(rect.findContainingCorner(200, 200, cornerSize)).toBeUndefined()
expect(rect.findContainingCorner(100, 400, cornerSize)).toBeUndefined()
// Outside rectangle
expect(rect.findContainingCorner(50, 150, cornerSize)).toBeUndefined()
})
})
describe("corner detection methods", () => {
const cornerSize = 20
describe("isInTopLeftCorner", () => {
test("should return true when point is in top-left corner", () => {
expect(rect.isInTopLeftCorner(100, 200, cornerSize)).toBe(true)
expect(rect.isInTopLeftCorner(110, 210, cornerSize)).toBe(true)
expect(rect.isInTopLeftCorner(119, 219, cornerSize)).toBe(true)
})
test("should return false when point is outside top-left corner", () => {
expect(rect.isInTopLeftCorner(120, 200, cornerSize)).toBe(false)
expect(rect.isInTopLeftCorner(100, 220, cornerSize)).toBe(false)
expect(rect.isInTopLeftCorner(99, 200, cornerSize)).toBe(false)
expect(rect.isInTopLeftCorner(100, 199, cornerSize)).toBe(false)
})
})
describe("isInTopRightCorner", () => {
test("should return true when point is in top-right corner", () => {
// Top-right corner area is from (right - cornerSize, top) to (right, top + cornerSize)
// That's (380, 200) to (400, 220)
expect(rect.isInTopRightCorner(380, 200, cornerSize)).toBe(true)
expect(rect.isInTopRightCorner(390, 210, cornerSize)).toBe(true)
expect(rect.isInTopRightCorner(399, 219, cornerSize)).toBe(true)
})
test("should return false when point is outside top-right corner", () => {
expect(rect.isInTopRightCorner(379, 200, cornerSize)).toBe(false)
expect(rect.isInTopRightCorner(400, 220, cornerSize)).toBe(false)
expect(rect.isInTopRightCorner(401, 200, cornerSize)).toBe(false)
expect(rect.isInTopRightCorner(400, 199, cornerSize)).toBe(false)
})
})
describe("isInBottomLeftCorner", () => {
test("should return true when point is in bottom-left corner", () => {
// Bottom-left corner area is from (left, bottom - cornerSize) to (left + cornerSize, bottom)
// That's (100, 580) to (120, 600)
expect(rect.isInBottomLeftCorner(100, 580, cornerSize)).toBe(true)
expect(rect.isInBottomLeftCorner(110, 590, cornerSize)).toBe(true)
expect(rect.isInBottomLeftCorner(119, 599, cornerSize)).toBe(true)
})
test("should return false when point is outside bottom-left corner", () => {
expect(rect.isInBottomLeftCorner(120, 600, cornerSize)).toBe(false)
expect(rect.isInBottomLeftCorner(100, 579, cornerSize)).toBe(false)
expect(rect.isInBottomLeftCorner(99, 600, cornerSize)).toBe(false)
expect(rect.isInBottomLeftCorner(100, 601, cornerSize)).toBe(false)
})
})
describe("isInBottomRightCorner", () => {
test("should return true when point is in bottom-right corner", () => {
// Bottom-right corner area is from (right - cornerSize, bottom - cornerSize) to (right, bottom)
// That's (380, 580) to (400, 600)
expect(rect.isInBottomRightCorner(380, 580, cornerSize)).toBe(true)
expect(rect.isInBottomRightCorner(390, 590, cornerSize)).toBe(true)
expect(rect.isInBottomRightCorner(399, 599, cornerSize)).toBe(true)
})
test("should return false when point is outside bottom-right corner", () => {
expect(rect.isInBottomRightCorner(379, 600, cornerSize)).toBe(false)
expect(rect.isInBottomRightCorner(400, 579, cornerSize)).toBe(false)
expect(rect.isInBottomRightCorner(401, 600, cornerSize)).toBe(false)
expect(rect.isInBottomRightCorner(400, 601, cornerSize)).toBe(false)
})
})
})
describe("edge cases", () => {
test("should handle zero-sized corner areas", () => {
expect(rect.findContainingCorner(100, 200, 0)).toBeUndefined()
expect(rect.isInTopLeftCorner(100, 200, 0)).toBe(false)
})
test("should handle rectangles at origin", () => {
const originRect = new Rectangle(0, 0, 100, 100)
expect(originRect.findContainingCorner(0, 0, 10)).toBe("NW")
// Bottom-right corner is at (90, 90) to (100, 100)
expect(originRect.findContainingCorner(90, 90, 10)).toBe("SE")
})
test("should handle negative coordinates", () => {
const negRect = new Rectangle(-50, -50, 100, 100)
expect(negRect.findContainingCorner(-50, -50, 10)).toBe("NW")
// Bottom-right corner is at (40, 40) to (50, 50)
expect(negRect.findContainingCorner(40, 40, 10)).toBe("SE")
})
})
})