diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css
index eee0c06589..c516f24e7c 100644
--- a/packages/design-system/src/css/style.css
+++ b/packages/design-system/src/css/style.css
@@ -200,7 +200,7 @@
--node-stroke-executing: var(--color-blue-100);
--text-secondary: var(--color-stone-100);
--text-primary: var(--color-charcoal-700);
- --input-surface: rgba(0, 0, 0, 0.15);
+ --input-surface: rgb(0 0 0 / 0.15);
}
.dark-theme {
@@ -247,7 +247,7 @@
--node-stroke-executing: var(--color-blue-100);
--text-secondary: var(--color-slate-100);
--text-primary: var(--color-pure-white);
- --input-surface: rgba(130, 130, 130, 0.1);
+ --input-surface: rgb(130 130 130 / 0.1);
}
@theme inline {
@@ -1139,7 +1139,7 @@ audio.comfy-audio.empty-audio-widget {
}
.isLOD .lg-node-header {
- border-radius: 0px;
+ border-radius: 0;
pointer-events: none;
}
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 210468449a..40b951b926 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -62,6 +62,10 @@
"icon": "Icon",
"color": "Color",
"error": "Error",
+ "resizeFromBottomRight": "Resize from bottom-right corner",
+ "resizeFromTopRight": "Resize from top-right corner",
+ "resizeFromBottomLeft": "Resize from bottom-left corner",
+ "resizeFromTopLeft": "Resize from top-left corner",
"info": "Node Info",
"bookmark": "Save to Library",
"moreOptions": "More Options",
diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue
index 7ec7be2964..7ed7934cf0 100644
--- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue
+++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue
@@ -9,7 +9,7 @@
:class="
cn(
'bg-node-component-surface',
- 'lg-node absolute rounded-2xl touch-none flex flex-col',
+ 'lg-node absolute rounded-2xl touch-none flex flex-col group',
'border-1 border-solid border-node-component-border',
// hover (only when node should handle events)
shouldHandleNodePointerEvents &&
@@ -108,12 +108,17 @@
-
-
+
+
+
+
@@ -121,6 +126,7 @@
import { whenever } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, inject, onErrorCaptured, onMounted, ref } from 'vue'
+import { useI18n } from 'vue-i18n'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { toggleNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
@@ -147,7 +153,8 @@ import {
} from '@/utils/graphTraversalUtil'
import { cn } from '@/utils/tailwindUtil'
-import { useNodeResize } from '../composables/useNodeResize'
+import type { ResizeHandleDirection } from '../interactions/resize/resizeMath'
+import { useNodeResize } from '../interactions/resize/useNodeResize'
import { calculateIntrinsicSize } from '../utils/calculateIntrinsicSize'
import LivePreview from './LivePreview.vue'
import NodeContent from './NodeContent.vue'
@@ -165,6 +172,8 @@ interface LGraphNodeProps {
const { nodeData, error = null } = defineProps()
+const { t } = useI18n()
+
const {
handleNodeCollapse,
handleNodeTitleUpdate,
@@ -243,8 +252,7 @@ onErrorCaptured((error) => {
return false // Prevent error propagation
})
-// Use layout system for node position and dragging
-const { position, size, zIndex } = useNodeLayout(() => nodeData.id)
+const { position, size, zIndex, moveNodeTo } = useNodeLayout(() => nodeData.id)
const { pointerHandlers, isDragging, dragStyle } = useNodePointerInteractions(
() => nodeData,
handleNodeSelect
@@ -282,19 +290,73 @@ onMounted(() => {
}
})
+const baseResizeHandleClasses =
+ 'absolute h-3 w-3 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'
+const POSITION_EPSILON = 0.01
+
+type CornerResizeHandle = {
+ id: string
+ direction: ResizeHandleDirection
+ classes: string
+ ariaLabel: string
+}
+
+const cornerResizeHandles: CornerResizeHandle[] = [
+ {
+ id: 'se',
+ direction: { horizontal: 'right', vertical: 'bottom' },
+ classes: 'right-0 bottom-0 cursor-se-resize',
+ ariaLabel: t('g.resizeFromBottomRight')
+ },
+ {
+ id: 'ne',
+ direction: { horizontal: 'right', vertical: 'top' },
+ classes: 'right-0 top-0 cursor-ne-resize',
+ ariaLabel: t('g.resizeFromTopRight')
+ },
+ {
+ id: 'sw',
+ direction: { horizontal: 'left', vertical: 'bottom' },
+ classes: 'left-0 bottom-0 cursor-sw-resize',
+ ariaLabel: t('g.resizeFromBottomLeft')
+ },
+ {
+ id: 'nw',
+ direction: { horizontal: 'left', vertical: 'top' },
+ classes: 'left-0 top-0 cursor-nw-resize',
+ ariaLabel: t('g.resizeFromTopLeft')
+ }
+]
+
const { startResize } = useNodeResize(
- (newSize, element) => {
- // Apply size directly to DOM element - ResizeObserver will pick this up
+ (result, element) => {
if (isCollapsed.value) return
- element.style.width = `${newSize.width}px`
- element.style.height = `${newSize.height}px`
+ // Apply size directly to DOM element - ResizeObserver will pick this up
+ element.style.width = `${result.size.width}px`
+ element.style.height = `${result.size.height}px`
+
+ const currentPosition = position.value
+ const deltaX = Math.abs(result.position.x - currentPosition.x)
+ const deltaY = Math.abs(result.position.y - currentPosition.y)
+
+ if (deltaX > POSITION_EPSILON || deltaY > POSITION_EPSILON) {
+ moveNodeTo(result.position)
+ }
},
{
transformState
}
)
+const handleResizePointerDown = (direction: ResizeHandleDirection) => {
+ return (event: PointerEvent) => {
+ if (nodeData.flags?.pinned) return
+
+ startResize(event, direction, { ...position.value })
+ }
+}
+
whenever(isCollapsed, () => {
const element = nodeContainerRef.value
if (!element) return
diff --git a/src/renderer/extensions/vueNodes/interactions/resize/resizeMath.ts b/src/renderer/extensions/vueNodes/interactions/resize/resizeMath.ts
new file mode 100644
index 0000000000..c58b0db6f8
--- /dev/null
+++ b/src/renderer/extensions/vueNodes/interactions/resize/resizeMath.ts
@@ -0,0 +1,130 @@
+import type { Point, Size } from '@/renderer/core/layout/types'
+
+export type ResizeHandleDirection = {
+ horizontal: 'left' | 'right'
+ vertical: 'top' | 'bottom'
+}
+
+function applyHandleDelta(
+ startSize: Size,
+ delta: Point,
+ handle: ResizeHandleDirection
+): Size {
+ const horizontalMultiplier = handle.horizontal === 'right' ? 1 : -1
+ const verticalMultiplier = handle.vertical === 'bottom' ? 1 : -1
+
+ return {
+ width: startSize.width + delta.x * horizontalMultiplier,
+ height: startSize.height + delta.y * verticalMultiplier
+ }
+}
+
+function clampToMinSize(size: Size, minSize: Size): Size {
+ return {
+ width: Math.max(size.width, minSize.width),
+ height: Math.max(size.height, minSize.height)
+ }
+}
+
+function snapSize(
+ size: Size,
+ minSize: Size,
+ snapFn?: (size: Size) => Size
+): Size {
+ if (!snapFn) return size
+ const snapped = snapFn(size)
+ return {
+ width: Math.max(minSize.width, snapped.width),
+ height: Math.max(minSize.height, snapped.height)
+ }
+}
+
+function computeAdjustedPosition(
+ startPosition: Point,
+ startSize: Size,
+ nextSize: Size,
+ handle: ResizeHandleDirection
+): Point {
+ const widthDelta = startSize.width - nextSize.width
+ const heightDelta = startSize.height - nextSize.height
+
+ return {
+ x:
+ handle.horizontal === 'left'
+ ? startPosition.x + widthDelta
+ : startPosition.x,
+ y:
+ handle.vertical === 'top'
+ ? startPosition.y + heightDelta
+ : startPosition.y
+ }
+}
+
+/**
+ * Computes the resulting size and position of a node given pointer movement
+ * and handle orientation.
+ */
+export function computeResizeOutcome({
+ startSize,
+ startPosition,
+ delta,
+ minSize,
+ handle,
+ snapFn
+}: {
+ startSize: Size
+ startPosition: Point
+ delta: Point
+ minSize: Size
+ handle: ResizeHandleDirection
+ snapFn?: (size: Size) => Size
+}): { size: Size; position: Point } {
+ const resized = applyHandleDelta(startSize, delta, handle)
+ const clamped = clampToMinSize(resized, minSize)
+ const snapped = snapSize(clamped, minSize, snapFn)
+ const position = computeAdjustedPosition(
+ startPosition,
+ startSize,
+ snapped,
+ handle
+ )
+
+ return {
+ size: snapped,
+ position
+ }
+}
+
+export function createResizeSession(config: {
+ startSize: Size
+ startPosition: Point
+ minSize: Size
+ handle: ResizeHandleDirection
+}) {
+ const startSize = { ...config.startSize }
+ const startPosition = { ...config.startPosition }
+ const minSize = { ...config.minSize }
+ const handle = config.handle
+
+ return (delta: Point, snapFn?: (size: Size) => Size) =>
+ computeResizeOutcome({
+ startSize,
+ startPosition,
+ minSize,
+ handle,
+ delta,
+ snapFn
+ })
+}
+
+export function toCanvasDelta(
+ startPointer: Point,
+ currentPointer: Point,
+ scale: number
+): Point {
+ const safeScale = scale === 0 ? 1 : scale
+ return {
+ x: (currentPointer.x - startPointer.x) / safeScale,
+ y: (currentPointer.y - startPointer.y) / safeScale
+ }
+}
diff --git a/src/renderer/extensions/vueNodes/composables/useNodeResize.ts b/src/renderer/extensions/vueNodes/interactions/resize/useNodeResize.ts
similarity index 57%
rename from src/renderer/extensions/vueNodes/composables/useNodeResize.ts
rename to src/renderer/extensions/vueNodes/interactions/resize/useNodeResize.ts
index bf3fcc02fc..2566fe41b6 100644
--- a/src/renderer/extensions/vueNodes/composables/useNodeResize.ts
+++ b/src/renderer/extensions/vueNodes/interactions/resize/useNodeResize.ts
@@ -2,25 +2,24 @@ import { useEventListener } from '@vueuse/core'
import { ref } from 'vue'
import type { TransformState } from '@/renderer/core/layout/injectionKeys'
+import type { Point, Size } from '@/renderer/core/layout/types'
import { useNodeSnap } from '@/renderer/extensions/vueNodes/composables/useNodeSnap'
import { useShiftKeySync } from '@/renderer/extensions/vueNodes/composables/useShiftKeySync'
import { calculateIntrinsicSize } from '@/renderer/extensions/vueNodes/utils/calculateIntrinsicSize'
-interface Size {
- width: number
- height: number
-}
-
-interface Position {
- x: number
- y: number
-}
+import type { ResizeHandleDirection } from './resizeMath'
+import { createResizeSession, toCanvasDelta } from './resizeMath'
interface UseNodeResizeOptions {
/** Transform state for coordinate conversion */
transformState: TransformState
}
+interface ResizeCallbackPayload {
+ size: Size
+ position: Point
+}
+
/**
* Composable for node resizing functionality
*
@@ -28,15 +27,26 @@ interface UseNodeResizeOptions {
* Handles pointer capture, coordinate calculations, and size constraints.
*/
export function useNodeResize(
- resizeCallback: (size: Size, element: HTMLElement) => void,
+ resizeCallback: (
+ payload: ResizeCallbackPayload,
+ element: HTMLElement
+ ) => void,
options: UseNodeResizeOptions
) {
const { transformState } = options
const isResizing = ref(false)
- const resizeStartPos = ref(null)
- const resizeStartSize = ref(null)
- const intrinsicMinSize = ref(null)
+ const resizeStartPointer = ref(null)
+ const resizeSession = ref<
+ | ((
+ delta: Point,
+ snapFn?: (size: Size) => Size
+ ) => {
+ size: Size
+ position: Point
+ })
+ | null
+ >(null)
// Snap-to-grid functionality
const { shouldSnap, applySnapToSize } = useNodeSnap()
@@ -44,85 +54,78 @@ export function useNodeResize(
// Shift key sync for LiteGraph canvas preview
const { trackShiftKey } = useShiftKeySync()
- const startResize = (event: PointerEvent) => {
+ const startResize = (
+ event: PointerEvent,
+ handle: ResizeHandleDirection,
+ startPosition: Point
+ ) => {
event.preventDefault()
event.stopPropagation()
const target = event.currentTarget
if (!(target instanceof HTMLElement)) return
- // Track shift key state and sync to canvas for snap preview
- const stopShiftSync = trackShiftKey(event)
-
- // Capture pointer to ensure we get all move/up events
- target.setPointerCapture(event.pointerId)
-
- isResizing.value = true
- resizeStartPos.value = { x: event.clientX, y: event.clientY }
-
- // Get current node size from the DOM and calculate intrinsic min size
const nodeElement = target.closest('[data-node-id]')
if (!(nodeElement instanceof HTMLElement)) return
const rect = nodeElement.getBoundingClientRect()
const scale = transformState.camera.z
- // Calculate current size in canvas coordinates
- resizeStartSize.value = {
+ const startSize: Size = {
width: rect.width / scale,
height: rect.height / scale
}
- // Calculate intrinsic content size (minimum based on content)
- intrinsicMinSize.value = calculateIntrinsicSize(nodeElement, scale)
+ const minSize = calculateIntrinsicSize(nodeElement, scale)
+
+ // Track shift key state and sync to canvas for snap preview
+ const stopShiftSync = trackShiftKey(event)
+
+ // Capture pointer to ensure we get all move/up events
+ target.setPointerCapture(event.pointerId)
+
+ isResizing.value = true
+ resizeStartPointer.value = { x: event.clientX, y: event.clientY }
+ resizeSession.value = createResizeSession({
+ startSize,
+ startPosition: { ...startPosition },
+ minSize,
+ handle
+ })
const handlePointerMove = (moveEvent: PointerEvent) => {
if (
!isResizing.value ||
- !resizeStartPos.value ||
- !resizeStartSize.value ||
- !intrinsicMinSize.value
+ !resizeStartPointer.value ||
+ !resizeSession.value
)
return
- const dx = moveEvent.clientX - resizeStartPos.value.x
- const dy = moveEvent.clientY - resizeStartPos.value.y
-
- // Apply scale factor from transform state
- const scale = transformState.camera.z
- const scaledDx = dx / scale
- const scaledDy = dy / scale
-
- // Apply constraints: only minimum size based on content, no maximum
- const constrainedSize = {
- width: Math.max(
- intrinsicMinSize.value.width,
- resizeStartSize.value.width + scaledDx
- ),
- height: Math.max(
- intrinsicMinSize.value.height,
- resizeStartSize.value.height + scaledDy
- )
- }
+ const startPointer = resizeStartPointer.value
+ const session = resizeSession.value
- // Apply snap-to-grid if shift is held or always snap is enabled
- const finalSize = shouldSnap(moveEvent)
- ? applySnapToSize(constrainedSize)
- : constrainedSize
+ const delta = toCanvasDelta(
+ startPointer,
+ { x: moveEvent.clientX, y: moveEvent.clientY },
+ transformState.camera.z
+ )
- // Get the node element to apply size directly
const nodeElement = target.closest('[data-node-id]')
if (nodeElement instanceof HTMLElement) {
- resizeCallback(finalSize, nodeElement)
+ const outcome = session(
+ delta,
+ shouldSnap(moveEvent) ? applySnapToSize : undefined
+ )
+
+ resizeCallback(outcome, nodeElement)
}
}
const handlePointerUp = (upEvent: PointerEvent) => {
if (isResizing.value) {
isResizing.value = false
- resizeStartPos.value = null
- resizeStartSize.value = null
- intrinsicMinSize.value = null
+ resizeStartPointer.value = null
+ resizeSession.value = null
// Stop tracking shift key state
stopShiftSync()
diff --git a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts
index 03f0cbf038..e770c0aeab 100644
--- a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts
+++ b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts
@@ -253,7 +253,7 @@ export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter) {
/**
* Update node position directly (without drag)
*/
- function moveTo(position: Point) {
+ function moveNodeTo(position: Point) {
mutations.setSource(LayoutSource.Vue)
mutations.moveNode(nodeId, position)
}
@@ -269,7 +269,7 @@ export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter) {
isDragging,
// Mutations
- moveTo,
+ moveNodeTo,
// Drag handlers
startDrag,
diff --git a/src/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue b/src/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue
index 59ed07bde0..ccdf6e47a6 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue
+++ b/src/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue
@@ -388,7 +388,7 @@ onUnmounted(() => {
diff --git a/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.test.ts b/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.test.ts
index fbeb9dd123..e41608e731 100644
--- a/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.test.ts
+++ b/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.test.ts
@@ -51,9 +51,11 @@ vi.mock('@/renderer/extensions/vueNodes/layout/useNodeLayout', () => ({
useNodeLayout: () => ({
position: { x: 100, y: 50 },
size: { width: 200, height: 100 },
+ zIndex: 0,
startDrag: vi.fn(),
handleDrag: vi.fn(),
- endDrag: vi.fn()
+ endDrag: vi.fn(),
+ moveTo: vi.fn()
})
}))
@@ -77,7 +79,7 @@ vi.mock('@/renderer/extensions/vueNodes/preview/useNodePreviewState', () => ({
}))
}))
-vi.mock('../composables/useNodeResize', () => ({
+vi.mock('../interactions/resize/useNodeResize', () => ({
useNodeResize: vi.fn(() => ({
startResize: vi.fn(),
isResizing: computed(() => false)
diff --git a/tests-ui/tests/renderer/extensions/vueNodes/interactions/resize/resizeMath.test.ts b/tests-ui/tests/renderer/extensions/vueNodes/interactions/resize/resizeMath.test.ts
new file mode 100644
index 0000000000..dd59534f16
--- /dev/null
+++ b/tests-ui/tests/renderer/extensions/vueNodes/interactions/resize/resizeMath.test.ts
@@ -0,0 +1,160 @@
+import { describe, expect, it, vi } from 'vitest'
+
+import {
+ computeResizeOutcome,
+ createResizeSession,
+ toCanvasDelta
+} from '@/renderer/extensions/vueNodes/interactions/resize/resizeMath'
+
+describe('nodeResizeMath', () => {
+ const startSize = { width: 200, height: 120 }
+ const startPosition = { x: 80, y: 160 }
+ const minSize = { width: 120, height: 80 }
+
+ it('computes resize from bottom-right corner without moving position', () => {
+ const outcome = computeResizeOutcome({
+ startSize,
+ startPosition,
+ delta: { x: 40, y: 20 },
+ minSize,
+ handle: { horizontal: 'right', vertical: 'bottom' }
+ })
+
+ expect(outcome.size).toEqual({ width: 240, height: 140 })
+ expect(outcome.position).toEqual(startPosition)
+ })
+
+ it('computes resize from top-left corner adjusting position', () => {
+ const outcome = computeResizeOutcome({
+ startSize,
+ startPosition,
+ delta: { x: -30, y: -20 },
+ minSize,
+ handle: { horizontal: 'left', vertical: 'top' }
+ })
+
+ expect(outcome.size).toEqual({ width: 230, height: 140 })
+ expect(outcome.position).toEqual({ x: 50, y: 140 })
+ })
+
+ it('clamps to minimum size when shrinking below intrinsic size', () => {
+ const outcome = computeResizeOutcome({
+ startSize,
+ startPosition,
+ delta: { x: 500, y: 500 },
+ minSize,
+ handle: { horizontal: 'left', vertical: 'top' }
+ })
+
+ expect(outcome.size).toEqual(minSize)
+ expect(outcome.position).toEqual({
+ x: startPosition.x + (startSize.width - minSize.width),
+ y: startPosition.y + (startSize.height - minSize.height)
+ })
+ })
+
+ it('supports reusable resize sessions with snapping', () => {
+ const session = createResizeSession({
+ startSize,
+ startPosition,
+ minSize,
+ handle: { horizontal: 'right', vertical: 'bottom' }
+ })
+
+ const snapFn = vi.fn((size: typeof startSize) => ({
+ width: Math.round(size.width / 25) * 25,
+ height: Math.round(size.height / 25) * 25
+ }))
+
+ const applied = session({ x: 13, y: 17 }, snapFn)
+
+ expect(applied.size).toEqual({ width: 225, height: 125 })
+ expect(applied.position).toEqual(startPosition)
+ expect(snapFn).toHaveBeenCalled()
+ })
+
+ it('converts screen delta to canvas delta using scale', () => {
+ const delta = toCanvasDelta({ x: 50, y: 75 }, { x: 150, y: 135 }, 2)
+
+ expect(delta).toEqual({ x: 50, y: 30 })
+ })
+
+ describe('edge cases', () => {
+ it('handles zero scale by using fallback scale of 1', () => {
+ const delta = toCanvasDelta({ x: 50, y: 75 }, { x: 150, y: 135 }, 0)
+
+ expect(delta).toEqual({ x: 100, y: 60 })
+ })
+
+ it('handles negative deltas when resizing from right/bottom', () => {
+ const outcome = computeResizeOutcome({
+ startSize,
+ startPosition,
+ delta: { x: -50, y: -30 },
+ minSize,
+ handle: { horizontal: 'right', vertical: 'bottom' }
+ })
+
+ expect(outcome.size).toEqual({ width: 150, height: 90 })
+ expect(outcome.position).toEqual(startPosition)
+ })
+
+ it('handles very large deltas without overflow', () => {
+ const outcome = computeResizeOutcome({
+ startSize,
+ startPosition,
+ delta: { x: 10000, y: 10000 },
+ minSize,
+ handle: { horizontal: 'right', vertical: 'bottom' }
+ })
+
+ expect(outcome.size.width).toBe(10200)
+ expect(outcome.size.height).toBe(10120)
+ expect(outcome.position).toEqual(startPosition)
+ })
+
+ it('respects minimum size even with extreme negative deltas', () => {
+ const outcome = computeResizeOutcome({
+ startSize,
+ startPosition,
+ delta: { x: -1000, y: -1000 },
+ minSize,
+ handle: { horizontal: 'right', vertical: 'bottom' }
+ })
+
+ expect(outcome.size).toEqual(minSize)
+ expect(outcome.position).toEqual(startPosition)
+ })
+
+ it('handles minSize larger than startSize', () => {
+ const largeMinSize = { width: 300, height: 200 }
+ const outcome = computeResizeOutcome({
+ startSize,
+ startPosition,
+ delta: { x: 10, y: 10 },
+ minSize: largeMinSize,
+ handle: { horizontal: 'right', vertical: 'bottom' }
+ })
+
+ expect(outcome.size).toEqual(largeMinSize)
+ expect(outcome.position).toEqual(startPosition)
+ })
+
+ it('adjusts position correctly when minSize prevents shrinking from top-left', () => {
+ const largeMinSize = { width: 250, height: 150 }
+ const outcome = computeResizeOutcome({
+ startSize,
+ startPosition,
+ delta: { x: 100, y: 100 },
+ minSize: largeMinSize,
+ handle: { horizontal: 'left', vertical: 'top' }
+ })
+
+ expect(outcome.size).toEqual(largeMinSize)
+ expect(outcome.position).toEqual({
+ x: startPosition.x + (startSize.width - largeMinSize.width),
+ y: startPosition.y + (startSize.height - largeMinSize.height)
+ })
+ })
+ })
+})