Compare commits

..

8 Commits

Author SHA1 Message Date
snomiao
5541edcec7 [fix] Correct checkout ref for fork PRs in lint-and-format workflow
The workflow was using `github.event.pull_request.head.ref` which only
contains the branch name (e.g., 'mpe/typoFixes'). For PRs from forks,
this causes GitHub Actions to incorrectly look for that branch in the
main repository instead of the fork.

Changed to use `refs/pull/${{ github.event.pull_request.number }}/head`
which correctly references the PR's merge ref that GitHub creates. This
ensures the workflow checks out the right code for both fork and non-fork PRs.

This issue was observed in PR #5880 where the workflow was fetching from
Comfy-Org/ComfyUI_frontend instead of the contributor's fork.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-01 23:17:47 +00:00
Benjamin Lu
d76b1abc46 Rename workflows to match workflow names (#5866)
## Summary
- rename each GitHub Actions workflow file so its filename matches the
workflow `name` value for easier discovery

------
https://chatgpt.com/codex/tasks/task_e_68dc213f0a808330869ed73c27858eb9

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5866-Rename-workflows-to-match-workflow-names-27e6d73d36508103bca7ea2746f73a3b)
by [Unito](https://www.unito.io)
2025-10-01 13:27:14 -07:00
Christian Byrne
fd757027a9 [ci] allow Claude review even when Playwright and Vitest checks have failed (#5882)
Currently the claude review action will be skipped if any tests have
failed. This is not really necessary, it will be more efficient to allow
claude to review while still waiting for tests.

This accounts for scenario where there is an expected visual baseline
change, but the PR author doesn't want to regenerate baselines until
everything is approved and ready to merge (as generating right away
before you know whether changes will be requested can be a hassle).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5882-ci-allow-Claude-review-even-when-Playwright-and-Vitest-checks-have-failed-27f6d73d365081ccbcdaff7104edc2fd)
by [Unito](https://www.unito.io)
2025-10-01 13:16:14 -07:00
Benjamin Lu
1efc2233c5 Fix Claude review workflow checkout ref (#5874)
By default, in this case the checkout action will checkout to github's
temporary merge base ref, which may include changes from the base branch
when the base branch moves.

This happened in this review:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/5866#pullrequestreview-3287366541

To prevent this, the PR's HEAD SHA was specified to be used
specifically, keeping claude's reviews only looking at that PR's branch.
2025-10-01 12:52:03 -07:00
Comfy Org PR Bot
557b2fdb0e 1.28.4 (#5875)
Patch version increment to 1.28.4

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5875-1-28-4-27f6d73d3650819f984ac83c971197b0)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <4284322+AustinMroz@users.noreply.github.com>
2025-09-30 23:40:53 -05:00
filtered
7fd2dc304a pnpm catalog for centralized dependency management (#5871)
## Summary

Adds pnpm catalog to centralize dependency versions across the monorepo.

## Changes

- **What**: Consolidates dependencies into single default catalog with
[`prefer` mode](https://pnpm.io/catalogs#catalog-mode)
- **Dependencies**: No new dependencies - reorganizes existing version
management

## Review Focus

The catalog uses `prefer` mode which automatically uses catalog versions
for packages already in the catalog, falling back to direct versions for
packages not yet cataloged.

### Example Usage

When adding a dependency already in the catalog:
```bash
pnpm add vue
```

This automatically uses `"vue": "catalog:"` in `package.json` instead of
a direct version.
2025-09-30 20:05:41 -07:00
Christian Byrne
e8de474d42 [docs] Change grammar in docs (#5868)
## Summary

Updates docs.

This PR is mostly for testing some n8n automation

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5868-docs-Change-grammar-in-docs-27e6d73d3650812691d2dcf71efe7bf4)
by [Unito](https://www.unito.io)
2025-09-30 19:10:20 -07:00
Christian Byrne
3f291672d4 fix progress state on Vue nodes in subgraphs (#5842)
## Summary

Fixes two errors with subgraph progress states:

1. Nodes inside subgraphs were not having progress state shown
2. Subgraph nodes (outer representation) themselves did not have a
visible progress state

1 is fixed by using locator IDs instead of local node IDs.

2 is fixed by ensuring the subgraph title button does not wrap to a
newline and thus block the progress bar under the node header.

## Changes

- **What**: Updated `useNodeExecutionState` composable to use
`nodeLocatorId` for tracking execution state across subgraph boundaries
- **What**: Modified NodeHeader layout to fix subgraph enter button
positioning with proper flexbox gap

## Review Focus

Execution state tracking accuracy for nested subgraph nodes and
NodeHeader layout consistency across different node types.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5842-fix-progress-state-on-Vue-nodes-in-subgraphs-27c6d73d365081cb8335c8bb5dbd74f7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-09-30 15:01:18 -07:00
81 changed files with 671 additions and 647 deletions

View File

@@ -29,11 +29,9 @@ jobs:
- name: Check if we should proceed
id: check-status
run: |
# Get all check runs for this commit
CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("lint-and-format|test|playwright-tests")) | {name, conclusion}')
# Check if any required checks failed
if echo "$CHECK_RUNS" | grep -q '"conclusion": "failure"'; then
CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("lint-and-format")) | {name, conclusion}')
if echo "$CHECK_RUNS" | grep -Eq '"conclusion": "(failure|cancelled|timed_out|action_required)"'; then
echo "Some CI checks failed - skipping Claude review"
echo "proceed=false" >> $GITHUB_OUTPUT
else
@@ -53,6 +51,7 @@ jobs:
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/head
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -86,4 +85,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
REPOSITORY: ${{ github.repository }}
REPOSITORY: ${{ github.repository }}

View File

@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.ref }}
ref: refs/pull/${{ github.event.pull_request.number }}/head
fetch-depth: 0
- name: Install pnpm

1
.npmrc
View File

@@ -1 +1,2 @@
ignore-workspace-root-check=true
catalog-mode=prefer

View File

@@ -16,7 +16,7 @@ Without this flag, parallel tests will conflict and fail randomly.
### ComfyUI devtools
ComfyUI_devtools is now included in this repository under `tools/devtools/`. During CI/CD, these files are automatically copied to the `custom_nodes` directory.
ComfyUI_devtools is included in this repository under `tools/devtools/`. During CI/CD, these files are automatically copied to the `custom_nodes` directory.
_ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._
For local development, copy the devtools files to your ComfyUI installation:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.28.3",
"version": "1.28.4",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",

View File

@@ -2,6 +2,114 @@ packages:
- apps/**
- packages/**
catalog:
# Core frameworks
typescript: ^5.9.2
vue: ^3.5.13
# Build tools
'@nx/eslint': 21.4.1
'@nx/playwright': 21.4.1
'@nx/storybook': 21.4.1
'@nx/vite': 21.4.1
nx: 21.4.1
tsx: ^4.15.6
vite: ^5.4.19
'@vitejs/plugin-vue': ^5.1.4
'vite-plugin-dts': ^4.5.4
vue-tsc: ^3.0.7
# Testing
'happy-dom': ^15.11.0
jsdom: ^26.1.0
'@pinia/testing': ^0.1.5
'@playwright/test': ^1.52.0
'@vitest/coverage-v8': ^3.2.4
'@vitest/ui': ^3.0.0
vitest: ^3.2.4
'@vue/test-utils': ^2.4.6
# Linting & Formatting
'@eslint/js': ^9.35.0
eslint: ^9.34.0
'eslint-config-prettier': ^10.1.8
'eslint-plugin-prettier': ^5.5.4
'eslint-plugin-storybook': ^9.1.6
'eslint-plugin-unused-imports': ^4.2.0
'eslint-plugin-vue': ^10.4.0
globals: ^15.9.0
'@intlify/eslint-plugin-vue-i18n': ^4.1.0
prettier: ^3.3.2
'typescript-eslint': ^8.44.0
'vue-eslint-parser': ^10.2.0
# Vue ecosystem
'@sentry/vue': ^8.48.0
'@vueuse/core': ^11.0.0
'@vueuse/integrations': ^13.9.0
'vite-plugin-html': ^3.2.2
'vite-plugin-vue-devtools': ^7.7.6
pinia: ^2.1.7
'vue-i18n': ^9.14.3
'vue-router': ^4.4.3
vuefire: ^3.2.1
# PrimeVue UI framework
'@primeuix/forms': 0.0.2
'@primeuix/styled': 0.3.2
'@primeuix/utils': ^0.3.2
'@primevue/core': ^4.2.5
'@primevue/forms': ^4.2.5
'@primevue/icons': 4.2.5
'@primevue/themes': ^4.2.5
primeicons: ^7.0.0
primevue: ^4.2.5
# Tailwind CSS and design
'@iconify/json': ^2.2.380
'@iconify-json/lucide': ^1.1.178
'@iconify/tailwind': ^1.1.3
'@tailwindcss/vite': ^4.1.12
tailwindcss: ^4.1.12
'tailwindcss-primeui': ^0.6.1
'tw-animate-css': ^1.3.8
'unplugin-icons': ^0.22.0
'unplugin-vue-components': ^0.28.0
# Storybook
'@storybook/addon-docs': ^9.1.1
storybook: ^9.1.6
'@storybook/vue3': ^9.1.1
'@storybook/vue3-vite': ^9.1.1
# Data and validation
algoliasearch: ^5.21.0
firebase: ^11.6.0
yjs: ^13.6.27
zod: ^3.23.8
'zod-validation-error': ^3.3.0
# Dev tools
dotenv: ^16.4.5
husky: ^9.0.11
jiti: 2.4.2
knip: ^5.62.0
'lint-staged': ^15.2.7
# Type definitions
'@types/fs-extra': ^11.0.4
'@types/jsdom': ^21.1.7
'@types/node': ^20.14.8
'@types/semver': ^7.7.0
'@types/three': ^0.169.0
'vue-component-type-helpers': ^3.0.7
'zod-to-json-schema': ^3.24.1
# i18n
'@alloc/quick-lru': ^5.2.0
'@lobehub/i18n-cli': ^1.25.1
'@trivago/prettier-plugin-sort-imports': ^5.2.0
ignoredBuiltDependencies:
- '@firebase/util'
- protobufjs

View File

@@ -4,7 +4,7 @@ import type { Ref } from 'vue'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import type { Rect } from '@/lib/litegraph/src/interfaces'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
@@ -71,7 +71,7 @@ export function useSelectionToolboxPosition(
visible.value = true
// Get bounds for all selected items
const allBounds: Rect[] = []
const allBounds: ReadOnlyRect[] = []
for (const item of selectableItems) {
// Skip items without valid IDs
if (item.id == null) continue

View File

@@ -1,4 +1,4 @@
import type { Point, Rect } from './interfaces'
import type { Point, ReadOnlyRect, Rect } from './interfaces'
import { EaseFunction, Rectangle } from './litegraph'
export interface DragAndScaleState {
@@ -188,7 +188,10 @@ export class DragAndScale {
* Fits the view to the specified bounds.
* @param bounds The bounds to fit the view to, defined by a rectangle.
*/
fitToBounds(bounds: Rect, { zoom = 0.75 }: { zoom?: number } = {}): void {
fitToBounds(
bounds: ReadOnlyRect,
{ zoom = 0.75 }: { zoom?: number } = {}
): void {
const cw = this.element.width / window.devicePixelRatio
const ch = this.element.height / window.devicePixelRatio
let targetScale = this.scale
@@ -220,7 +223,7 @@ export class DragAndScale {
* @param bounds The bounds to animate the view to, defined by a rectangle.
*/
animateToBounds(
bounds: Readonly<Rect | Rectangle>,
bounds: ReadOnlyRect,
setDirty: () => void,
{
duration = 350,

View File

@@ -4,7 +4,6 @@ import {
SUBGRAPH_INPUT_ID,
SUBGRAPH_OUTPUT_ID
} from '@/lib/litegraph/src/constants'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
import { createUuidv4, zeroUuid } from '@/lib/litegraph/src/utils/uuid'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
@@ -1708,12 +1707,7 @@ export class LGraph
...subgraphNode.subgraph.groups
].map((p: { pos: Point; size?: Size }): HasBoundingRect => {
return {
boundingRect: new Rectangle(
p.pos[0],
p.pos[1],
p.size?.[0] ?? 0,
p.size?.[1] ?? 0
)
boundingRect: [p.pos[0], p.pos[1], p.size?.[0] ?? 0, p.size?.[1] ?? 0]
}
})
const bounds = createBounds(positionables) ?? [0, 0, 0, 0]

View File

@@ -47,6 +47,8 @@ import type {
NullableProperties,
Point,
Positionable,
ReadOnlyPoint,
ReadOnlyRect,
Rect,
Size
} from './interfaces'
@@ -234,11 +236,11 @@ export class LGraphCanvas
implements CustomEventDispatcher<LGraphCanvasEventMap>
{
// Optimised buffers used during rendering
static #temp = [0, 0, 0, 0] satisfies Rect
static #temp_vec2 = [0, 0] satisfies Point
static #tmp_area = [0, 0, 0, 0] satisfies Rect
static #margin_area = [0, 0, 0, 0] satisfies Rect
static #link_bounding = [0, 0, 0, 0] satisfies Rect
static #temp = new Float32Array(4)
static #temp_vec2 = new Float32Array(2)
static #tmp_area = new Float32Array(4)
static #margin_area = new Float32Array(4)
static #link_bounding = new Float32Array(4)
static DEFAULT_BACKGROUND_IMAGE =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII='
@@ -626,7 +628,7 @@ export class LGraphCanvas
dirty_area?: Rect | null
/** @deprecated Unused */
node_in_panel?: LGraphNode | null
last_mouse: Point = [0, 0]
last_mouse: ReadOnlyPoint = [0, 0]
last_mouseclick: number = 0
graph: LGraph | Subgraph | null
get _graph(): LGraph | Subgraph {
@@ -2632,7 +2634,7 @@ export class LGraphCanvas
pointer: CanvasPointer,
node?: LGraphNode | undefined
): void {
const dragRect: [number, number, number, number] = [0, 0, 0, 0]
const dragRect = new Float32Array(4)
dragRect[0] = e.canvasX
dragRect[1] = e.canvasY
@@ -3172,7 +3174,7 @@ export class LGraphCanvas
LGraphCanvas.active_canvas = this
this.adjustMouseEvent(e)
const mouse: Point = [e.clientX, e.clientY]
const mouse: ReadOnlyPoint = [e.clientX, e.clientY]
this.mouse[0] = mouse[0]
this.mouse[1] = mouse[1]
const delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]]
@@ -4075,10 +4077,7 @@ export class LGraphCanvas
this.setDirty(true)
}
#handleMultiSelect(
e: CanvasPointerEvent,
dragRect: [number, number, number, number]
) {
#handleMultiSelect(e: CanvasPointerEvent, dragRect: Float32Array) {
// Process drag
// Convert Point pair (pos, offset) to Rect
const { graph, selectedItems, subgraph } = this
@@ -4733,47 +4732,32 @@ export class LGraphCanvas
for (const renderLink of renderLinks) {
const {
fromSlot,
fromPos: pos
// fromDirection,
// dragDirection
fromPos: pos,
fromDirection,
dragDirection
} = renderLink
const connShape = fromSlot.shape
const connType = fromSlot.type
const color = resolveConnectingLinkColor(connType)
const colour = resolveConnectingLinkColor(connType)
// the connection being dragged by the mouse
if (
this.linkRenderer &&
renderLink.fromSlotIndex !== undefined &&
renderLink.node !== undefined
) {
const { fromSlotIndex, node } = renderLink
if (
node instanceof LGraphNode &&
('link' in fromSlot || 'links' in fromSlot)
) {
this.linkRenderer.renderDraggingLink(
ctx,
node,
fromSlot,
fromSlotIndex,
highlightPos,
this.buildLinkRenderContext(),
{ fromInput: 'link' in fromSlot, color }
// pos,
// colour,
// fromDirection,
// dragDirection,
// {
// ...this.buildLinkRenderContext(),
// linkMarkerShape: LinkMarkerShape.None
// }
)
}
if (this.linkRenderer) {
this.linkRenderer.renderDraggingLink(
ctx,
pos,
highlightPos,
colour,
fromDirection,
dragDirection,
{
...this.buildLinkRenderContext(),
linkMarkerShape: LinkMarkerShape.None
}
)
}
ctx.fillStyle = color
ctx.fillStyle = colour
ctx.beginPath()
if (connType === LiteGraph.EVENT || connShape === RenderShape.BOX) {
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10)
@@ -4864,7 +4848,7 @@ export class LGraphCanvas
}
/** Get the target snap / highlight point in graph space */
#getHighlightPosition(): Point {
#getHighlightPosition(): ReadOnlyPoint {
return LiteGraph.snaps_for_comfy
? this.linkConnector.state.snapLinksPos ??
this._highlight_pos ??
@@ -4879,7 +4863,7 @@ export class LGraphCanvas
*/
#renderSnapHighlight(
ctx: CanvasRenderingContext2D,
highlightPos: Point
highlightPos: ReadOnlyPoint
): void {
const linkConnectorSnap = !!this.linkConnector.state.snapLinksPos
if (!this._highlight_pos && !linkConnectorSnap) return
@@ -5221,8 +5205,7 @@ export class LGraphCanvas
// clip if required (mask)
const shape = node._shape || RenderShape.BOX
const size = LGraphCanvas.#temp_vec2
size[0] = node.renderingSize[0]
size[1] = node.renderingSize[1]
size.set(node.renderingSize)
if (node.collapsed) {
ctx.font = this.inner_text_font
@@ -5417,10 +5400,7 @@ export class LGraphCanvas
// Normalised node dimensions
const area = LGraphCanvas.#tmp_area
area[0] = node.boundingRect[0]
area[1] = node.boundingRect[1]
area[2] = node.boundingRect[2]
area[3] = node.boundingRect[3]
area.set(node.boundingRect)
area[0] -= node.pos[0]
area[1] -= node.pos[1]
@@ -5522,10 +5502,7 @@ export class LGraphCanvas
shape = RenderShape.ROUND
) {
const snapGuide = LGraphCanvas.#temp
snapGuide[0] = item.boundingRect[0]
snapGuide[1] = item.boundingRect[1]
snapGuide[2] = item.boundingRect[2]
snapGuide[3] = item.boundingRect[3]
snapGuide.set(item.boundingRect)
// Not all items have pos equal to top-left of bounds
const { pos } = item
@@ -5965,8 +5942,8 @@ export class LGraphCanvas
*/
renderLink(
ctx: CanvasRenderingContext2D,
a: Point,
b: Point,
a: ReadOnlyPoint,
b: ReadOnlyPoint,
link: LLink | null,
skip_border: boolean,
flow: number | null,
@@ -5983,9 +5960,9 @@ export class LGraphCanvas
/** When defined, render data will be saved to this reroute instead of the {@link link}. */
reroute?: Reroute
/** Offset of the bezier curve control point from {@link a point a} (output side) */
startControl?: Point
startControl?: ReadOnlyPoint
/** Offset of the bezier curve control point from {@link b point b} (input side) */
endControl?: Point
endControl?: ReadOnlyPoint
/** Number of sublines (useful to represent vec3 or rgb) @todo If implemented, refactor calculations out of the loop */
num_sublines?: number
/** Whether this is a floating link segment */
@@ -8456,7 +8433,7 @@ export class LGraphCanvas
* Starts an animation to fit the view around the specified selection of nodes.
* @param bounds The bounds to animate the view to, defined by a rectangle.
*/
animateToBounds(bounds: Rect | Rectangle, options: AnimationOptions = {}) {
animateToBounds(bounds: ReadOnlyRect, options: AnimationOptions = {}) {
const setDirty = () => this.setDirty(true, true)
this.ds.animateToBounds(bounds, setDirty, options)
}

View File

@@ -1,5 +1,4 @@
import { NullGraphError } from '@/lib/litegraph/src/infrastructure/NullGraphError'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { LGraph } from './LGraph'
import { LGraphCanvas } from './LGraphCanvas'
@@ -41,15 +40,15 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
title: string
font?: string
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
_bounding: [number, number, number, number] = [
_bounding: Float32Array = new Float32Array([
10,
10,
LGraphGroup.minWidth,
LGraphGroup.minHeight
]
])
_pos: Point = [10, 10]
_size: Size = [LGraphGroup.minWidth, LGraphGroup.minHeight]
_pos: Point = this._bounding.subarray(0, 2)
_size: Size = this._bounding.subarray(2, 4)
/** @deprecated See {@link _children} */
_nodes: LGraphNode[] = []
_children: Set<Positionable> = new Set()
@@ -108,13 +107,8 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
this._size[1] = Math.max(LGraphGroup.minHeight, v[1])
}
get boundingRect(): Rectangle {
return Rectangle.from([
this._pos[0],
this._pos[1],
this._size[0],
this._size[1]
])
get boundingRect() {
return this._bounding
}
get nodes() {
@@ -151,17 +145,14 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
configure(o: ISerialisedGroup): void {
this.id = o.id
this.title = o.title
this._pos[0] = o.bounding[0]
this._pos[1] = o.bounding[1]
this._size[0] = o.bounding[2]
this._size[1] = o.bounding[3]
this._bounding.set(o.bounding)
this.color = o.color
this.flags = o.flags || this.flags
if (o.font_size) this.font_size = o.font_size
}
serialize(): ISerialisedGroup {
const b = [this._pos[0], this._pos[1], this._size[0], this._size[1]]
const b = this._bounding
return {
id: this.id,
title: this.title,
@@ -219,7 +210,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
)
if (LiteGraph.highlight_selected_group && this.selected) {
strokeShape(ctx, this.boundingRect, {
strokeShape(ctx, this._bounding, {
title_height: this.titleHeight,
padding
})
@@ -260,7 +251,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
// Move nodes we overlap the centre point of
for (const node of nodes) {
if (containsCentre(this.boundingRect, node.boundingRect)) {
if (containsCentre(this._bounding, node.boundingRect)) {
this._nodes.push(node)
children.add(node)
}
@@ -268,13 +259,12 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
// Move reroutes we overlap the centre point of
for (const reroute of reroutes.values()) {
if (isPointInRect(reroute.pos, this.boundingRect)) children.add(reroute)
if (isPointInRect(reroute.pos, this._bounding)) children.add(reroute)
}
// Move groups we wholly contain
for (const group of groups) {
if (containsRect(this.boundingRect, group.boundingRect))
children.add(group)
if (containsRect(this._bounding, group._bounding)) children.add(group)
}
groups.sort((a, b) => {

View File

@@ -18,6 +18,7 @@ import type { Reroute, RerouteId } from './Reroute'
import { getNodeInputOnPos, getNodeOutputOnPos } from './canvas/measureSlots'
import type { IDrawBoundingOptions } from './draw'
import { NullGraphError } from './infrastructure/NullGraphError'
import type { ReadOnlyRectangle } from './infrastructure/Rectangle'
import { Rectangle } from './infrastructure/Rectangle'
import type {
ColorOption,
@@ -36,6 +37,8 @@ import type {
ISlotType,
Point,
Positionable,
ReadOnlyPoint,
ReadOnlyRect,
Rect,
Size
} from './interfaces'
@@ -384,7 +387,7 @@ export class LGraphNode
* Called once at the start of every frame. Caller may change the values in {@link out}, which will be reflected in {@link boundingRect}.
* WARNING: Making changes to boundingRect via onBounding is poorly supported, and will likely result in strange behaviour.
*/
onBounding?(this: LGraphNode, out: Rectangle): void
onBounding?(this: LGraphNode, out: Rect): void
console?: string[]
_level?: number
_shape?: RenderShape
@@ -410,12 +413,12 @@ export class LGraphNode
}
/** @inheritdoc {@link renderArea} */
#renderArea: [number, number, number, number] = [0, 0, 0, 0]
#renderArea: Float32Array = new Float32Array(4)
/**
* Rect describing the node area, including shadows and any protrusions.
* Determines if the node is visible. Calculated once at the start of every frame.
*/
get renderArea(): Rect {
get renderArea(): ReadOnlyRect {
return this.#renderArea
}
@@ -426,12 +429,12 @@ export class LGraphNode
*
* Determines the node hitbox and other rendering effects. Calculated once at the start of every frame.
*/
get boundingRect(): Rectangle {
get boundingRect(): ReadOnlyRectangle {
return this.#boundingRect
}
/** The offset from {@link pos} to the top-left of {@link boundingRect}. */
get boundingOffset(): Point {
get boundingOffset(): ReadOnlyPoint {
const {
pos: [posX, posY],
boundingRect: [bX, bY]
@@ -440,9 +443,9 @@ export class LGraphNode
}
/** {@link pos} and {@link size} values are backed by this {@link Rect}. */
_posSize: [number, number, number, number] = [0, 0, 0, 0]
_pos: Point = [0, 0]
_size: Size = [0, 0]
_posSize: Float32Array = new Float32Array(4)
_pos: Point = this._posSize.subarray(0, 2)
_size: Size = this._posSize.subarray(2, 4)
public get pos() {
return this._pos
@@ -1650,7 +1653,7 @@ export class LGraphNode
inputs ? inputs.filter((input) => !isWidgetInputSlot(input)).length : 1,
outputs ? outputs.length : 1
)
const size = out || [0, 0]
const size = out || new Float32Array([0, 0])
rows = Math.max(rows, 1)
// although it should be graphcanvas.inner_text_font size
const font_size = LiteGraph.NODE_TEXT_SIZE
@@ -1975,7 +1978,7 @@ export class LGraphNode
* @param out `x, y, width, height` are written to this array.
* @param ctx The canvas context to use for measuring text.
*/
measure(out: Rectangle, ctx: CanvasRenderingContext2D): void {
measure(out: Rect, ctx: CanvasRenderingContext2D): void {
const titleMode = this.title_mode
const renderTitle =
titleMode != TitleMode.TRANSPARENT_TITLE &&
@@ -2001,13 +2004,13 @@ export class LGraphNode
/**
* returns the bounding of the object, used for rendering purposes
* @param out {Rect?} [optional] a place to store the output, to free garbage
* @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage
* @param includeExternal {boolean?} [optional] set to true to
* include the shadow and connection points in the bounding calculation
* @returns the bounding box in format of [topleft_cornerx, topleft_cornery, width, height]
*/
getBounding(out?: Rect, includeExternal?: boolean): Rect {
out ||= [0, 0, 0, 0]
out ||= new Float32Array(4)
const rect = includeExternal ? this.renderArea : this.boundingRect
out[0] = rect[0]
@@ -2028,10 +2031,7 @@ export class LGraphNode
this.onBounding?.(bounds)
const renderArea = this.#renderArea
renderArea[0] = bounds[0]
renderArea[1] = bounds[1]
renderArea[2] = bounds[2]
renderArea[3] = bounds[3]
renderArea.set(bounds)
// 4 offset for collapsed node connection points
renderArea[0] -= 4
renderArea[1] -= 4
@@ -3174,7 +3174,7 @@ export class LGraphNode
* @returns the position
*/
getConnectionPos(is_input: boolean, slot_number: number, out?: Point): Point {
out ||= [0, 0]
out ||= new Float32Array(2)
const {
pos: [nodeX, nodeY],
@@ -3839,7 +3839,7 @@ export class LGraphNode
slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT
}
#measureSlots(): Rect | null {
#measureSlots(): ReadOnlyRect | null {
const slots: (NodeInputSlot | NodeOutputSlot)[] = []
for (const [slotIndex, slot] of this.#concreteInputs.entries()) {

View File

@@ -109,7 +109,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
data?: number | string | boolean | { toToolTip?(): string }
_data?: unknown
/** Centre point of the link, calculated during render only - can be inaccurate */
_pos: [number, number]
_pos: Float32Array
/** @todo Clean up - never implemented in comfy. */
_last_time?: number
/** The last canvas 2D path that was used to render this link */
@@ -171,7 +171,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
this._data = null
// center
this._pos = [0, 0]
this._pos = new Float32Array(2)
}
/** @deprecated Use {@link LLink.create} */

View File

@@ -1,4 +1,3 @@
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { LayoutSource } from '@/renderer/core/layout/types'
@@ -13,8 +12,8 @@ import type {
LinkSegment,
Point,
Positionable,
ReadonlyLinkNetwork,
Rect
ReadOnlyRect,
ReadonlyLinkNetwork
} from './interfaces'
import { distance, isPointInRect } from './measure'
import type { Serialisable, SerialisableReroute } from './types/serialisation'
@@ -50,6 +49,8 @@ export class Reroute
return Reroute.radius + gap + Reroute.slotRadius
}
#malloc = new Float32Array(8)
/** The network this reroute belongs to. Contains all valid links and reroutes. */
#network: WeakRef<LinkNetwork>
@@ -72,7 +73,7 @@ export class Reroute
/** This property is only defined on the last reroute of a floating reroute chain (closest to input end). */
floating?: FloatingRerouteSlot
#pos: [number, number] = [0, 0]
#pos = this.#malloc.subarray(0, 2)
/** @inheritdoc */
get pos(): Point {
return this.#pos
@@ -88,17 +89,17 @@ export class Reroute
}
/** @inheritdoc */
get boundingRect(): Rectangle {
get boundingRect(): ReadOnlyRect {
const { radius } = Reroute
const [x, y] = this.#pos
return Rectangle.from([x - radius, y - radius, 2 * radius, 2 * radius])
return [x - radius, y - radius, 2 * radius, 2 * radius]
}
/**
* Slightly over-sized rectangle, guaranteed to contain the entire surface area for hover detection.
* Eliminates most hover positions using an extremely cheap check.
*/
get #hoverArea(): Rect {
get #hoverArea(): ReadOnlyRect {
const xOffset = 2 * Reroute.slotOffset
const yOffset = 2 * Math.max(Reroute.radius, Reroute.slotRadius)
@@ -125,14 +126,14 @@ export class Reroute
sin: number = 0
/** Bezier curve control point for the "target" (input) side of the link */
controlPoint: [number, number] = [0, 0]
controlPoint: Point = this.#malloc.subarray(4, 6)
/** @inheritdoc */
path?: Path2D
/** @inheritdoc */
_centreAngle?: number
/** @inheritdoc */
_pos: [number, number] = [0, 0]
_pos: Float32Array = this.#malloc.subarray(6, 8)
/** @inheritdoc */
_dragging?: boolean

View File

@@ -67,7 +67,7 @@ interface IDrawTextInAreaOptions {
*/
export function strokeShape(
ctx: CanvasRenderingContext2D,
area: Rect | Rectangle,
area: Rect,
{
shape = RenderShape.BOX,
round_radius,

View File

@@ -1,6 +1,10 @@
import { clamp } from 'es-toolkit/compat'
import type { Rect, Size } from '@/lib/litegraph/src/interfaces'
import type {
ReadOnlyRect,
ReadOnlySize,
Size
} from '@/lib/litegraph/src/interfaces'
/**
* Basic width and height, with min/max constraints.
@@ -51,15 +55,15 @@ export class ConstrainedSize {
this.desiredHeight = height
}
static fromSize(size: Size): ConstrainedSize {
static fromSize(size: ReadOnlySize): ConstrainedSize {
return new ConstrainedSize(size[0], size[1])
}
static fromRect(rect: Rect): ConstrainedSize {
static fromRect(rect: ReadOnlyRect): ConstrainedSize {
return new ConstrainedSize(rect[2], rect[3])
}
setSize(size: Size): void {
setSize(size: ReadOnlySize): void {
this.desiredWidth = size[0]
this.desiredHeight = size[1]
}

View File

@@ -1,6 +1,6 @@
import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { LLink, ResolvedConnection } from '@/lib/litegraph/src/LLink'
import type { Rect } from '@/lib/litegraph/src/interfaces'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
import type {
ExportedSubgraph,
@@ -29,7 +29,7 @@ export interface LGraphEventMap {
/** The type of subgraph to create. */
subgraph: Subgraph
/** The boundary around every item that was moved into the subgraph. */
bounds: Rect
bounds: ReadOnlyRect
/** The raw data that was used to create the subgraph. */
exportedSubgraph: ExportedSubgraph
/** The links that were used to create the subgraph. */

View File

@@ -1,50 +1,47 @@
import type {
CompassCorners,
Point,
Rect,
ReadOnlyPoint,
ReadOnlyRect,
ReadOnlySize,
ReadOnlyTypedArray,
Size
} from '@/lib/litegraph/src/interfaces'
import { isInRectangle } from '@/lib/litegraph/src/measure'
/**
* A rectangle, represented as an array of 4 numbers: [x, y, width, height].
* A rectangle, represented as a float64 array of 4 numbers: [x, y, width, height].
*
* This class extends Array and provides both array access (rect[0], rect[1], etc.)
* and convenient property access (rect.x, rect.y, rect.width, rect.height).
* This class is a subclass of Float64Array, and so has all the methods of that class. Notably,
* {@link Rectangle.from} can be used to convert a {@link ReadOnlyRect}. Typing of this however,
* is broken due to the base TS lib returning Float64Array rather than `this`.
*
* Sub-array properties ({@link Float64Array.subarray}):
* - {@link pos}: The position of the top-left corner of the rectangle.
* - {@link size}: The size of the rectangle.
*/
export class Rectangle extends Array<number> {
export class Rectangle extends Float64Array {
#pos: Point | undefined
#size: Size | undefined
constructor(
x: number = 0,
y: number = 0,
width: number = 0,
height: number = 0
) {
super()
super(4)
this[0] = x
this[1] = y
this[2] = width
this[3] = height
this.length = 4
}
static override from([x, y, width, height]: Rect): Rectangle {
static override from([x, y, width, height]: ReadOnlyRect): Rectangle {
return new Rectangle(x, y, width, height)
}
/** Set all values from an array (for TypedArray compatibility) */
set(values: ArrayLike<number>): void {
this[0] = values[0] ?? 0
this[1] = values[1] ?? 0
this[2] = values[2] ?? 0
this[3] = values[3] ?? 0
}
/** Create a subarray (for TypedArray compatibility) */
subarray(begin: number = 0, end?: number): number[] {
const endIndex = end ?? this.length
return this.slice(begin, endIndex)
}
/**
* Creates a new rectangle positioned at the given centre, with the given width/height.
* @param centre The centre of the rectangle, as an `[x, y]` point
@@ -52,38 +49,57 @@ export class Rectangle extends Array<number> {
* @param height The height of the rectangle. Default: {@link width}
* @returns A new rectangle whose centre is at {@link x}
*/
static fromCentre([x, y]: Point, width: number, height = width): Rectangle {
static fromCentre(
[x, y]: ReadOnlyPoint,
width: number,
height = width
): Rectangle {
const left = x - width * 0.5
const top = y - height * 0.5
return new Rectangle(left, top, width, height)
}
static ensureRect(rect: Rect | Rectangle): Rectangle {
static ensureRect(rect: ReadOnlyRect): Rectangle {
return rect instanceof Rectangle
? rect
: new Rectangle(rect[0], rect[1], rect[2], rect[3])
}
/**
* The position of the top-left corner of this rectangle.
*/
get pos(): Point {
return [this[0], this[1]]
override subarray(
begin: number = 0,
end?: number
): Float64Array<ArrayBuffer> {
const byteOffset = begin << 3
const length = end === undefined ? end : end - begin
return new Float64Array(this.buffer, byteOffset, length)
}
set pos(value: Point) {
/**
* A reference to the position of the top-left corner of this rectangle.
*
* Updating the values of the returned object will update this rectangle.
*/
get pos(): Point {
this.#pos ??= this.subarray(0, 2)
return this.#pos!
}
set pos(value: ReadOnlyPoint) {
this[0] = value[0]
this[1] = value[1]
}
/**
* The size of this rectangle.
* A reference to the size of this rectangle.
*
* Updating the values of the returned object will update this rectangle.
*/
get size(): Size {
return [this[2], this[3]]
this.#size ??= this.subarray(2, 4)
return this.#size!
}
set size(value: Size) {
set size(value: ReadOnlySize) {
this[2] = value[0]
this[3] = value[1]
}
@@ -176,7 +192,7 @@ export class Rectangle extends Array<number> {
* Updates the rectangle to the values of {@link rect}.
* @param rect The rectangle to update to.
*/
updateTo(rect: Rect) {
updateTo(rect: ReadOnlyRect) {
this[0] = rect[0]
this[1] = rect[1]
this[2] = rect[2]
@@ -199,7 +215,7 @@ export class Rectangle extends Array<number> {
* @param point The point to check
* @returns `true` if {@link point} is inside this rectangle, otherwise `false`.
*/
containsPoint([x, y]: Point): boolean {
containsPoint([x, y]: ReadOnlyPoint): boolean {
const [left, top, width, height] = this
return x >= left && x < left + width && y >= top && y < top + height
}
@@ -210,7 +226,7 @@ export class Rectangle extends Array<number> {
* @param other The rectangle to check
* @returns `true` if {@link other} is inside this rectangle, otherwise `false`.
*/
containsRect(other: Rect | Rectangle): boolean {
containsRect(other: ReadOnlyRect): boolean {
const { right, bottom } = this
const otherRight = other[0] + other[2]
const otherBottom = other[1] + other[3]
@@ -235,7 +251,7 @@ export class Rectangle extends Array<number> {
* @param rect The rectangle to check
* @returns `true` if {@link rect} overlaps with this rectangle, otherwise `false`.
*/
overlaps(rect: Rect | Rectangle): boolean {
overlaps(rect: ReadOnlyRect): boolean {
return (
this.x < rect[0] + rect[2] &&
this.y < rect[1] + rect[3] &&
@@ -368,12 +384,12 @@ export class Rectangle extends Array<number> {
}
/** @returns The offset from the top-left of this rectangle to the point [{@link x}, {@link y}], as a new {@link Point}. */
getOffsetTo([x, y]: Point): Point {
getOffsetTo([x, y]: ReadOnlyPoint): Point {
return [x - this[0], y - this[1]]
}
/** @returns The offset from the point [{@link x}, {@link y}] to the top-left of this rectangle, as a new {@link Point}. */
getOffsetFrom([x, y]: Point): Point {
getOffsetFrom([x, y]: ReadOnlyPoint): Point {
return [this[0] - x, this[1] - y]
}
@@ -454,4 +470,14 @@ export class Rectangle extends Array<number> {
}
}
// ReadOnlyRectangle is now just Rectangle since we unified the types
export type ReadOnlyRectangle = Omit<
ReadOnlyTypedArray<Rectangle>,
| 'setHeightBottomAnchored'
| 'setWidthRightAnchored'
| 'resizeTopLeft'
| 'resizeBottomLeft'
| 'resizeTopRight'
| 'resizeBottomRight'
| 'resizeBottomRight'
| 'updateTo'
>

View File

@@ -60,7 +60,7 @@ export interface HasBoundingRect {
* @readonly
* @see {@link move}
*/
readonly boundingRect: Rectangle
readonly boundingRect: ReadOnlyRect
}
/** An object containing a set of child objects */
@@ -194,7 +194,7 @@ export interface LinkSegment {
/** The last canvas 2D path that was used to render this segment */
path?: Path2D
/** Centre point of the {@link path}. Calculated during render only - can be inaccurate */
readonly _pos: [number, number]
readonly _pos: Float32Array
/**
* Y-forward along the {@link path} from its centre point, in radians.
* `undefined` if using circles for link centres.
@@ -226,13 +226,52 @@ export interface IFoundSlot extends IInputOrOutput {
}
/** A point represented as `[x, y]` co-ordinates */
export type Point = [x: number, y: number]
export type Point = [x: number, y: number] | Float32Array | Float64Array
/** A size represented as `[width, height]` */
export type Size = [width: number, height: number]
export type Size = [width: number, height: number] | Float32Array | Float64Array
/** A very firm array */
type ArRect = [x: number, y: number, width: number, height: number]
/** A rectangle starting at top-left coordinates `[x, y, width, height]` */
export type Rect = [number, number, number, number]
export type Rect = ArRect | Float32Array | Float64Array
/** A point represented as `[x, y]` co-ordinates that will not be modified */
export type ReadOnlyPoint =
| readonly [x: number, y: number]
| ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array>
/** A size represented as `[width, height]` that will not be modified */
export type ReadOnlySize =
| readonly [width: number, height: number]
| ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array>
/** A rectangle starting at top-left coordinates `[x, y, width, height]` that will not be modified */
export type ReadOnlyRect =
| readonly [x: number, y: number, width: number, height: number]
| ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array>
type TypedArrays =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
type TypedBigIntArrays = BigInt64Array | BigUint64Array
export type ReadOnlyTypedArray<T extends TypedArrays | TypedBigIntArrays> =
Omit<
Readonly<T>,
'fill' | 'copyWithin' | 'reverse' | 'set' | 'sort' | 'subarray'
>
/** Union of property names that are of type Match */
type KeysOfType<T, Match> = Exclude<
@@ -291,7 +330,7 @@ export interface INodeSlot extends HasBoundingRect {
nameLocked?: boolean
pos?: Point
/** @remarks Automatically calculated; not included in serialisation. */
boundingRect: Rectangle
boundingRect: Rect
/**
* A list of floating link IDs that are connected to this slot.
* This is calculated at runtime; it is **not** serialized.

View File

@@ -1,5 +1,10 @@
import type { Rectangle } from './infrastructure/Rectangle'
import type { HasBoundingRect, Point, Rect } from './interfaces'
import type {
HasBoundingRect,
Point,
ReadOnlyPoint,
ReadOnlyRect,
Rect
} from './interfaces'
import { Alignment, LinkDirection, hasFlag } from './types/globalEnums'
/**
@@ -8,7 +13,7 @@ import { Alignment, LinkDirection, hasFlag } from './types/globalEnums'
* @param b Point b as `x, y`
* @returns Distance between point {@link a} & {@link b}
*/
export function distance(a: Point, b: Point): number {
export function distance(a: ReadOnlyPoint, b: ReadOnlyPoint): number {
return Math.sqrt(
(b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1])
)
@@ -56,7 +61,10 @@ export function isInRectangle(
* @param rect The rectangle, as `x, y, width, height`
* @returns `true` if the point is inside the rect, otherwise `false`
*/
export function isPointInRect(point: Point, rect: Rect | Rectangle): boolean {
export function isPointInRect(
point: ReadOnlyPoint,
rect: ReadOnlyRect
): boolean {
return (
point[0] >= rect[0] &&
point[0] < rect[0] + rect[2] &&
@@ -72,11 +80,7 @@ export function isPointInRect(point: Point, rect: Rect | Rectangle): boolean {
* @param rect The rectangle, as `x, y, width, height`
* @returns `true` if the point is inside the rect, otherwise `false`
*/
export function isInRect(
x: number,
y: number,
rect: Rect | Rectangle
): boolean {
export function isInRect(x: number, y: number, rect: ReadOnlyRect): boolean {
return (
x >= rect[0] &&
x < rect[0] + rect[2] &&
@@ -117,10 +121,7 @@ export function isInsideRectangle(
* @param b Rectangle B as `x, y, width, height`
* @returns `true` if rectangles overlap, otherwise `false`
*/
export function overlapBounding(
a: Rect | Rectangle,
b: Rect | Rectangle
): boolean {
export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
const aRight = a[0] + a[2]
const aBottom = a[1] + a[3]
const bRight = b[0] + b[2]
@@ -136,7 +137,7 @@ export function overlapBounding(
* @param rect The rectangle, as `x, y, width, height`
* @returns The centre of the rectangle, as `x, y`
*/
export function getCentre(rect: Rect | Rectangle): Point {
export function getCentre(rect: ReadOnlyRect): Point {
return [rect[0] + rect[2] * 0.5, rect[1] + rect[3] * 0.5]
}
@@ -146,10 +147,7 @@ export function getCentre(rect: Rect | Rectangle): Point {
* @param b Sub-rectangle B as `x, y, width, height`
* @returns `true` if {@link a} contains most of {@link b}, otherwise `false`
*/
export function containsCentre(
a: Rect | Rectangle,
b: Rect | Rectangle
): boolean {
export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
const centreX = b[0] + b[2] * 0.5
const centreY = b[1] + b[3] * 0.5
return isInRect(centreX, centreY, a)
@@ -161,10 +159,7 @@ export function containsCentre(
* @param b Sub-rectangle B as `x, y, width, height`
* @returns `true` if {@link a} wholly contains {@link b}, otherwise `false`
*/
export function containsRect(
a: Rect | Rectangle,
b: Rect | Rectangle
): boolean {
export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
const aRight = a[0] + a[2]
const aBottom = a[1] + a[3]
const bRight = b[0] + b[2]
@@ -294,8 +289,8 @@ export function rotateLink(
* the right
*/
export function getOrientation(
lineStart: Point,
lineEnd: Point,
lineStart: ReadOnlyPoint,
lineEnd: ReadOnlyPoint,
x: number,
y: number
): number {
@@ -315,10 +310,10 @@ export function getOrientation(
*/
export function findPointOnCurve(
out: Point,
a: Point,
b: Point,
controlA: Point,
controlB: Point,
a: ReadOnlyPoint,
b: ReadOnlyPoint,
controlA: ReadOnlyPoint,
controlB: ReadOnlyPoint,
t: number = 0.5
): void {
const iT = 1 - t
@@ -335,13 +330,8 @@ export function findPointOnCurve(
export function createBounds(
objects: Iterable<HasBoundingRect>,
padding: number = 10
): Rect | null {
const bounds: [number, number, number, number] = [
Infinity,
Infinity,
-Infinity,
-Infinity
]
): ReadOnlyRect | null {
const bounds = new Float32Array([Infinity, Infinity, -Infinity, -Infinity])
for (const obj of objects) {
const rect = obj.boundingRect
@@ -389,11 +379,11 @@ export function snapPoint(pos: Point | Rect, snapTo: number): boolean {
* @returns The original {@link rect}, modified in place.
*/
export function alignToContainer(
rect: Rect | Rectangle,
rect: Rect,
anchors: Alignment,
[containerX, containerY, containerWidth, containerHeight]: Rect | Rectangle,
[insetX, insetY]: Point = [0, 0]
): Rect | Rectangle {
[containerX, containerY, containerWidth, containerHeight]: ReadOnlyRect,
[insetX, insetY]: ReadOnlyPoint = [0, 0]
): Rect {
if (hasFlag(anchors, Alignment.Left)) {
// Left
rect[0] = containerX + insetX
@@ -432,11 +422,11 @@ export function alignToContainer(
* @returns The original {@link rect}, modified in place.
*/
export function alignOutsideContainer(
rect: Rect | Rectangle,
rect: Rect,
anchors: Alignment,
[otherX, otherY, otherWidth, otherHeight]: Rect | Rectangle,
[outsetX, outsetY]: Point = [0, 0]
): Rect | Rectangle {
[otherX, otherY, otherWidth, otherHeight]: ReadOnlyRect,
[outsetX, outsetY]: ReadOnlyPoint = [0, 0]
): Rect {
if (hasFlag(anchors, Alignment.Left)) {
// Left
rect[0] = otherX - outsetX - rect[2]

View File

@@ -5,7 +5,7 @@ import type {
INodeInputSlot,
INodeOutputSlot,
OptionalProps,
Point
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
@@ -32,7 +32,7 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
this.#widget = widget ? new WeakRef(widget) : undefined
}
get collapsedPos(): Point {
get collapsedPos(): ReadOnlyPoint {
return [0, LiteGraph.NODE_TITLE_HEIGHT * -0.5]
}

View File

@@ -5,7 +5,7 @@ import type {
INodeInputSlot,
INodeOutputSlot,
OptionalProps,
Point
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
@@ -24,7 +24,7 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
return false
}
get collapsedPos(): Point {
get collapsedPos(): ReadOnlyPoint {
return [
this.#node._collapsed_width ?? LiteGraph.NODE_COLLAPSED_WIDTH,
LiteGraph.NODE_TITLE_HEIGHT * -0.5

View File

@@ -8,7 +8,8 @@ import type {
INodeSlot,
ISubgraphInput,
OptionalProps,
Point
Point,
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph, Rectangle } from '@/lib/litegraph/src/litegraph'
import { getCentre } from '@/lib/litegraph/src/measure'
@@ -35,7 +36,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
pos?: Point
/** The offset from the parent node to the centre point of this slot. */
get #centreOffset(): Point {
get #centreOffset(): ReadOnlyPoint {
const nodePos = this.node.pos
const { boundingRect } = this
@@ -51,7 +52,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
}
/** The center point of this slot when the node is collapsed. */
abstract get collapsedPos(): Point
abstract get collapsedPos(): ReadOnlyPoint
#node: LGraphNode
get node(): LGraphNode {

View File

@@ -7,7 +7,7 @@ import type {
INodeInputSlot,
INodeOutputSlot,
Point,
Rect
ReadOnlyRect
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
@@ -213,7 +213,7 @@ export class SubgraphInput extends SubgraphSlot {
}
/** For inputs, x is the right edge of the input node. */
override arrange(rect: Rect): void {
override arrange(rect: ReadOnlyRect): void {
const [right, top, width, height] = rect
const { boundingRect: b, pos } = this

View File

@@ -7,7 +7,7 @@ import type {
INodeInputSlot,
INodeOutputSlot,
Point,
Rect
ReadOnlyRect
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
@@ -119,7 +119,7 @@ export class SubgraphOutput extends SubgraphSlot {
return [x + height, y + height * 0.5]
}
override arrange(rect: Rect): void {
override arrange(rect: ReadOnlyRect): void {
const [left, top, width, height] = rect
const { boundingRect: b, pos } = this

View File

@@ -11,8 +11,8 @@ import type {
INodeInputSlot,
INodeOutputSlot,
Point,
Rect,
Size
ReadOnlyRect,
ReadOnlySize
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { SlotBase } from '@/lib/litegraph/src/node/SlotBase'
@@ -45,7 +45,7 @@ export abstract class SubgraphSlot
return LiteGraph.NODE_SLOT_HEIGHT
}
readonly #pos: Point = [0, 0]
readonly #pos: Point = new Float32Array(2)
readonly measurement: ConstrainedSize = new ConstrainedSize(
SubgraphSlot.defaultHeight,
@@ -133,7 +133,7 @@ export abstract class SubgraphSlot
}
}
measure(): Size {
measure(): ReadOnlySize {
const width = LGraphCanvas._measureText?.(this.displayName) ?? 0
const { defaultHeight } = SubgraphSlot
@@ -141,7 +141,7 @@ export abstract class SubgraphSlot
return this.measurement.toSize()
}
abstract arrange(rect: Rect): void
abstract arrange(rect: ReadOnlyRect): void
abstract connect(
slot: INodeInputSlot | INodeOutputSlot,

View File

@@ -1,6 +1,5 @@
import { afterEach, beforeEach, describe, expect, vi } from 'vitest'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { INodeInputSlot, Point } from '@/lib/litegraph/src/interfaces'
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LGraph } from '@/lib/litegraph/src/litegraph'
@@ -85,8 +84,8 @@ describe('LGraphNode', () => {
}))
}
node.configure(configureData)
expect(node.pos).toEqual([50, 60])
expect(node.size).toEqual([70, 80])
expect(node.pos).toEqual(new Float32Array([50, 60]))
expect(node.size).toEqual(new Float32Array([70, 80]))
})
test('should configure inputs correctly', () => {
@@ -572,7 +571,7 @@ describe('LGraphNode', () => {
name: 'test_in',
type: 'string',
link: null,
boundingRect: new Rectangle(0, 0, 0, 0)
boundingRect: new Float32Array([0, 0, 0, 0])
}
})
test('should return position based on title height when collapsed', () => {
@@ -595,7 +594,7 @@ describe('LGraphNode', () => {
name: 'test_in_2',
type: 'number',
link: null,
boundingRect: new Rectangle(0, 0, 0, 0)
boundingRect: new Float32Array([0, 0, 0, 0])
}
node.inputs = [inputSlot, inputSlot2]
const slotIndex = 0
@@ -630,13 +629,13 @@ describe('LGraphNode', () => {
name: 'in0',
type: 'string',
link: null,
boundingRect: new Rectangle(0, 0, 0, 0)
boundingRect: new Float32Array([0, 0, 0, 0])
}
const input1: INodeInputSlot = {
name: 'in1',
type: 'number',
link: null,
boundingRect: new Rectangle(0, 0, 0, 0),
boundingRect: new Float32Array([0, 0, 0, 0]),
pos: [5, 45]
}
node.inputs = [input0, input1]

View File

@@ -4,19 +4,19 @@ exports[`LGraph configure() > LGraph matches previous snapshot (normal configure
LGraph {
"_groups": [
LGraphGroup {
"_bounding": [
10,
10,
140,
80,
"_bounding": Float32Array [
20,
20,
1,
3,
],
"_children": Set {},
"_nodes": [],
"_pos": [
"_pos": Float32Array [
20,
20,
],
"_size": [
"_size": Float32Array [
1,
3,
],
@@ -39,19 +39,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -98,7 +98,6 @@ LGraph {
"selected": [Function],
},
"title": "LGraphNode",
"title_buttons": [],
"type": "mustBeSet",
"widgets": undefined,
"widgets_start_y": undefined,
@@ -109,19 +108,19 @@ LGraph {
"1": LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -168,7 +167,6 @@ LGraph {
"selected": [Function],
},
"title": "LGraphNode",
"title_buttons": [],
"type": "mustBeSet",
"widgets": undefined,
"widgets_start_y": undefined,
@@ -180,19 +178,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -239,7 +237,6 @@ LGraph {
"selected": [Function],
},
"title": "LGraphNode",
"title_buttons": [],
"type": "mustBeSet",
"widgets": undefined,
"widgets_start_y": undefined,
@@ -252,16 +249,7 @@ LGraph {
"config": {},
"elapsed_time": 0.01,
"errors_in_execution": undefined,
"events": CustomEventTarget {
Symbol(listeners): {
"bubbling": Map {},
"capturing": Map {},
},
Symbol(listenerOptions): {
"bubbling": Map {},
"capturing": Map {},
},
},
"events": CustomEventTarget {},
"execution_time": undefined,
"execution_timer_id": undefined,
"extra": {},
@@ -308,16 +296,7 @@ LGraph {
"config": {},
"elapsed_time": 0.01,
"errors_in_execution": undefined,
"events": CustomEventTarget {
Symbol(listeners): {
"bubbling": Map {},
"capturing": Map {},
},
Symbol(listenerOptions): {
"bubbling": Map {},
"capturing": Map {},
},
},
"events": CustomEventTarget {},
"execution_time": undefined,
"execution_timer_id": undefined,
"extra": {},

View File

@@ -4,19 +4,19 @@ exports[`LGraph > supports schema v0.4 graphs > oldSchemaGraph 1`] = `
LGraph {
"_groups": [
LGraphGroup {
"_bounding": [
10,
10,
140,
80,
"_bounding": Float32Array [
20,
20,
1,
3,
],
"_children": Set {},
"_nodes": [],
"_pos": [
"_pos": Float32Array [
20,
20,
],
"_size": [
"_size": Float32Array [
1,
3,
],
@@ -39,19 +39,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -111,19 +111,19 @@ LGraph {
"1": LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -184,19 +184,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -258,16 +258,7 @@ LGraph {
"config": {},
"elapsed_time": 0.01,
"errors_in_execution": undefined,
"events": CustomEventTarget {
Symbol(listeners): {
"bubbling": Map {},
"capturing": Map {},
},
Symbol(listenerOptions): {
"bubbling": Map {},
"capturing": Map {},
},
},
"events": CustomEventTarget {},
"execution_time": undefined,
"execution_timer_id": undefined,
"extra": {},

View File

@@ -4,19 +4,19 @@ exports[`LGraph (constructor only) > Matches previous snapshot > basicLGraph 1`]
LGraph {
"_groups": [
LGraphGroup {
"_bounding": [
10,
10,
140,
80,
"_bounding": Float32Array [
20,
20,
1,
3,
],
"_children": Set {},
"_nodes": [],
"_pos": [
"_pos": Float32Array [
20,
20,
],
"_size": [
"_size": Float32Array [
1,
3,
],
@@ -39,19 +39,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -109,19 +109,19 @@ LGraph {
"1": LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -180,19 +180,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -252,16 +252,7 @@ LGraph {
"config": {},
"elapsed_time": 0.01,
"errors_in_execution": undefined,
"events": CustomEventTarget {
Symbol(listeners): {
"bubbling": Map {},
"capturing": Map {},
},
Symbol(listenerOptions): {
"bubbling": Map {},
"capturing": Map {},
},
},
"events": CustomEventTarget {},
"execution_time": undefined,
"execution_timer_id": undefined,
"extra": {},
@@ -308,16 +299,7 @@ LGraph {
"config": {},
"elapsed_time": 0.01,
"errors_in_execution": undefined,
"events": CustomEventTarget {
Symbol(listeners): {
"bubbling": Map {},
"capturing": Map {},
},
Symbol(listenerOptions): {
"bubbling": Map {},
"capturing": Map {},
},
},
"events": CustomEventTarget {},
"execution_time": undefined,
"execution_timer_id": undefined,
"extra": {},

View File

@@ -1,6 +1,5 @@
import { test as baseTest } from 'vitest'
import { Rectangle } from '../src/infrastructure/Rectangle'
import type { Point, Rect } from '../src/interfaces'
import {
addDirectionalOffset,
@@ -132,8 +131,8 @@ test('snapPoint correctly snaps points to grid', ({ expect }) => {
test('createBounds correctly creates bounding box', ({ expect }) => {
const objects = [
{ boundingRect: new Rectangle(0, 0, 10, 10) },
{ boundingRect: new Rectangle(5, 5, 10, 10) }
{ boundingRect: [0, 0, 10, 10] as Rect },
{ boundingRect: [5, 5, 10, 10] as Rect }
]
const defaultBounds = createBounds(objects)

View File

@@ -1,15 +1,13 @@
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
// import type { RenderLink } from '@/lib/litegraph/src/canvas/RenderLink'
import type { Point } from '@/lib/litegraph/src/interfaces'
// import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import type { RenderLink } from '@/lib/litegraph/src/canvas/RenderLink'
import type { ReadOnlyPoint } from '@/lib/litegraph/src/interfaces'
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import { resolveConnectingLinkColor } from '@/lib/litegraph/src/utils/linkColors'
import { createLinkConnectorAdapter } from '@/renderer/core/canvas/links/linkConnectorAdapter'
import { useSlotLinkDragState } from '@/renderer/core/canvas/links/slotLinkDragState'
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
// import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
// import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
function buildContext(canvas: LGraphCanvas): LinkRenderContext {
return {
@@ -51,32 +49,24 @@ export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) {
const renderLinks = createLinkConnectorAdapter()?.renderLinks
if (!renderLinks || renderLinks.length === 0) return
const to: Point = [pointer.canvas.x, pointer.canvas.y]
const to: ReadOnlyPoint = [pointer.canvas.x, pointer.canvas.y]
ctx.save()
for (const link of renderLinks) {
// const startDir = link.fromDirection ?? LinkDirection.RIGHT
// const endDir = link.dragDirection ?? LinkDirection.CENTER
const color = resolveConnectingLinkColor(link.fromSlot.type)
const startDir = link.fromDirection ?? LinkDirection.RIGHT
const endDir = link.dragDirection ?? LinkDirection.CENTER
const colour = resolveConnectingLinkColor(link.fromSlot.type)
// const fromPoint = resolveRenderLinkOrigin(link)
const { node, fromSlot, fromSlotIndex } = link
if (node instanceof LGraphNode && 'link' in fromSlot) {
linkRenderer.renderDraggingLink(
ctx,
node,
fromSlot,
fromSlotIndex,
to,
context,
{
color
}
// colour,
// startDir,
// endDir,
// context
)
}
const fromPoint = resolveRenderLinkOrigin(link)
linkRenderer.renderDraggingLink(
ctx,
fromPoint,
to,
colour,
startDir,
endDir,
context
)
}
ctx.restore()
}
@@ -84,35 +74,35 @@ export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) {
canvas.onDrawForeground = patched
}
// function resolveRenderLinkOrigin(link: RenderLink): Point {
// if (link.fromReroute) {
// const rerouteLayout = layoutStore.getRerouteLayout(link.fromReroute.id)
// if (rerouteLayout) {
// return [rerouteLayout.position.x, rerouteLayout.position.y]
// }
function resolveRenderLinkOrigin(link: RenderLink): ReadOnlyPoint {
if (link.fromReroute) {
const rerouteLayout = layoutStore.getRerouteLayout(link.fromReroute.id)
if (rerouteLayout) {
return [rerouteLayout.position.x, rerouteLayout.position.y]
}
// const [x, y] = link.fromReroute.pos
// return [x, y]
// }
const [x, y] = link.fromReroute.pos
return [x, y]
}
// const nodeId = getRenderLinkNodeId(link)
// if (nodeId != null) {
// const isInputFrom = link.toType === 'output'
// const key = getSlotKey(String(nodeId), link.fromSlotIndex, isInputFrom)
// const layout = layoutStore.getSlotLayout(key)
// if (layout) {
// return [layout.position.x, layout.position.y]
// }
// }
const nodeId = getRenderLinkNodeId(link)
if (nodeId != null) {
const isInputFrom = link.toType === 'output'
const key = getSlotKey(String(nodeId), link.fromSlotIndex, isInputFrom)
const layout = layoutStore.getSlotLayout(key)
if (layout) {
return [layout.position.x, layout.position.y]
}
}
// return link.fromPos
// }
return link.fromPos
}
// function getRenderLinkNodeId(link: RenderLink): number | null {
// const node = link.node
// if (typeof node === 'object' && node !== null && 'id' in node) {
// const maybeId = node.id
// if (typeof maybeId === 'number') return maybeId
// }
// return null
// }
function getRenderLinkNodeId(link: RenderLink): number | null {
const node = link.node
if (typeof node === 'object' && node !== null && 'id' in node) {
const maybeId = node.id
if (typeof maybeId === 'number') return maybeId
}
return null
}

View File

@@ -6,14 +6,11 @@
* rendering data that can be consumed by the PathRenderer.
* Maintains backward compatibility with existing litegraph integration.
*/
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { LLink } from '@/lib/litegraph/src/LLink'
import type { Reroute } from '@/lib/litegraph/src/Reroute'
import type {
CanvasColour,
INodeInputSlot,
INodeOutputSlot,
Point as LitegraphPoint
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import {
@@ -21,12 +18,10 @@ import {
LinkMarkerShape,
LinkRenderType
} from '@/lib/litegraph/src/types/globalEnums'
import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations'
import {
type ArrowShape,
CanvasPathRenderer,
type Direction,
type DragLinkData,
type LinkRenderData,
type RenderContext as PathRenderContext,
type Point,
@@ -76,7 +71,6 @@ export class LitegraphLinkAdapter {
case LinkDirection.DOWN:
return 'down'
case LinkDirection.CENTER:
case LinkDirection.NONE:
return 'none'
default:
return 'right'
@@ -178,22 +172,22 @@ export class LitegraphLinkAdapter {
* Critically: does nothing for CENTER/NONE directions (no case for them)
*/
private applySplineOffset(
point: LitegraphPoint,
point: Point,
direction: LinkDirection,
distance: number
): void {
switch (direction) {
case LinkDirection.LEFT:
point[0] -= distance
point.x -= distance
break
case LinkDirection.RIGHT:
point[0] += distance
point.x += distance
break
case LinkDirection.UP:
point[1] -= distance
point.y -= distance
break
case LinkDirection.DOWN:
point[1] += distance
point.y += distance
break
// CENTER and NONE: no offset applied (original behavior)
}
@@ -205,8 +199,8 @@ export class LitegraphLinkAdapter {
*/
renderLinkDirect(
ctx: CanvasRenderingContext2D,
a: LitegraphPoint,
b: LitegraphPoint,
a: ReadOnlyPoint,
b: ReadOnlyPoint,
link: LLink | null,
skip_border: boolean,
flow: number | boolean | null,
@@ -216,8 +210,8 @@ export class LitegraphLinkAdapter {
context: LinkRenderContext,
extras: {
reroute?: Reroute
startControl?: LitegraphPoint
endControl?: LitegraphPoint
startControl?: ReadOnlyPoint
endControl?: ReadOnlyPoint
num_sublines?: number
disabled?: boolean
} = {}
@@ -278,19 +272,13 @@ export class LitegraphLinkAdapter {
y: a[1] + (extras.startControl![1] || 0)
}
const end = { x: b[0], y: b[1] }
const endArray: LitegraphPoint = [end.x, end.y]
this.applySplineOffset(endArray, endDir, dist * factor)
end.x = endArray[0]
end.y = endArray[1]
this.applySplineOffset(end, endDir, dist * factor)
cps.push(start, end)
linkData.controlPoints = cps
} else if (!hasStartCtrl && hasEndCtrl) {
// End provided, derive start via direction offset (CENTER => no offset)
const start = { x: a[0], y: a[1] }
const startArray: LitegraphPoint = [start.x, start.y]
this.applySplineOffset(startArray, startDir, dist * factor)
start.x = startArray[0]
start.y = startArray[1]
this.applySplineOffset(start, startDir, dist * factor)
const end = {
x: b[0] + (extras.endControl![0] || 0),
y: b[1] + (extras.endControl![1] || 0)
@@ -301,14 +289,8 @@ export class LitegraphLinkAdapter {
// Neither provided: derive both from directions (CENTER => no offset)
const start = { x: a[0], y: a[1] }
const end = { x: b[0], y: b[1] }
const startArray: LitegraphPoint = [start.x, start.y]
const endArray: LitegraphPoint = [end.x, end.y]
this.applySplineOffset(startArray, startDir, dist * factor)
this.applySplineOffset(endArray, endDir, dist * factor)
start.x = startArray[0]
start.y = startArray[1]
end.x = endArray[0]
end.y = endArray[1]
this.applySplineOffset(start, startDir, dist * factor)
this.applySplineOffset(end, endDir, dist * factor)
cps.push(start, end)
linkData.controlPoints = cps
}
@@ -333,7 +315,7 @@ export class LitegraphLinkAdapter {
// Copy calculated center position back to litegraph object
// This is needed for hit detection and menu interaction
if (linkData.centerPos) {
linkSegment._pos = linkSegment._pos || [0, 0]
linkSegment._pos = linkSegment._pos || new Float32Array(2)
linkSegment._pos[0] = linkData.centerPos.x
linkSegment._pos[1] = linkData.centerPos.y
@@ -347,8 +329,8 @@ export class LitegraphLinkAdapter {
if (this.enableLayoutStoreWrites && link && link.id !== -1) {
// Calculate bounds and center only when writing
const bounds = this.calculateLinkBounds(
[linkData.startPoint.x, linkData.startPoint.y] as LitegraphPoint,
[linkData.endPoint.x, linkData.endPoint.y] as LitegraphPoint,
[linkData.startPoint.x, linkData.startPoint.y] as ReadOnlyPoint,
[linkData.endPoint.x, linkData.endPoint.y] as ReadOnlyPoint,
linkData
)
const centerPos = linkData.centerPos || {
@@ -381,56 +363,33 @@ export class LitegraphLinkAdapter {
}
}
/**
* Render a link being dragged from a slot to mouse position
* Used during link creation/reconnection
*/
renderDraggingLink(
ctx: CanvasRenderingContext2D,
fromNode: LGraphNode | null,
fromSlot: INodeOutputSlot | INodeInputSlot,
fromSlotIndex: number,
toPosition: LitegraphPoint,
context: LinkRenderContext,
options: {
fromInput?: boolean
color?: CanvasColour
disabled?: boolean
} = {}
from: ReadOnlyPoint,
to: ReadOnlyPoint,
colour: CanvasColour,
startDir: LinkDirection,
endDir: LinkDirection,
context: LinkRenderContext
): void {
if (!fromNode) return
// Get slot position using layout tree if available
const slotPos = getSlotPosition(
fromNode,
fromSlotIndex,
options.fromInput || false
this.renderLinkDirect(
ctx,
from,
to,
null,
false,
null,
colour,
startDir,
endDir,
{
...context,
linkMarkerShape: LinkMarkerShape.None
},
{
disabled: false
}
)
if (!slotPos) return
// Get slot direction
const slotDir =
fromSlot.dir ||
(options.fromInput ? LinkDirection.LEFT : LinkDirection.RIGHT)
// Create drag data
const dragData: DragLinkData = {
fixedPoint: { x: slotPos[0], y: slotPos[1] },
fixedDirection: this.convertDirection(slotDir),
dragPoint: { x: toPosition[0], y: toPosition[1] },
color: options.color ? String(options.color) : undefined,
type: fromSlot.type !== undefined ? String(fromSlot.type) : undefined,
disabled: options.disabled || false,
fromInput: options.fromInput || false
}
// Convert context
const pathContext = this.convertToPathRenderContext(context)
// Hide center marker when dragging links
pathContext.style.showCenterMarker = false
// Render using pure renderer
this.pathRenderer.drawDraggingLink(ctx, dragData, pathContext)
}
/**
@@ -438,8 +397,8 @@ export class LitegraphLinkAdapter {
* Includes padding for line width and control points
*/
private calculateLinkBounds(
startPos: LitegraphPoint,
endPos: LitegraphPoint,
startPos: ReadOnlyPoint,
endPos: ReadOnlyPoint,
linkData: LinkRenderData
): Bounds {
let minX = Math.min(startPos[0], endPos[0])

View File

@@ -70,7 +70,7 @@ export interface RenderContext {
highlightedIds?: Set<string>
}
export interface DragLinkData {
interface DragLinkData {
/** Fixed end - the slot being dragged from */
fixedPoint: Point
fixedDirection: Direction
@@ -605,7 +605,6 @@ export class CanvasPathRenderer {
type: dragData.type,
disabled: dragData.disabled
}
console.log({ linkData })
// Use standard link drawing
return this.drawLink(ctx, linkData, context)

View File

@@ -4,7 +4,7 @@ import { computed, ref, toValue } from 'vue'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import { LLink } from '@/lib/litegraph/src/LLink'
import { Reroute } from '@/lib/litegraph/src/Reroute'
import type { Point as LitegraphPoint } from '@/lib/litegraph/src/interfaces'
import type { ReadOnlyPoint } from '@/lib/litegraph/src/interfaces'
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import { LitegraphLinkAdapter } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
@@ -113,7 +113,7 @@ export function useLinkLayoutSync() {
// Special handling for floating input chain
const isFloatingInputChain = !sourceNode && targetNode
const startControl: LitegraphPoint = isFloatingInputChain
const startControl: ReadOnlyPoint = isFloatingInputChain
? [0, 0]
: [dist * reroute.cos, dist * reroute.sin]
@@ -149,7 +149,7 @@ export function useLinkLayoutSync() {
(endPos[1] - lastReroute.pos[1]) ** 2
)
const finalDist = Math.min(Reroute.maxSplineOffset, finalDistance * 0.25)
const finalStartControl: LitegraphPoint = [
const finalStartControl: ReadOnlyPoint = [
finalDist * lastReroute.cos,
finalDist * lastReroute.sin
]

View File

@@ -211,7 +211,8 @@ const isSelected = computed(() => {
})
// Use execution state composable
const { executing, progress } = useNodeExecutionState(() => nodeData.id)
const nodeLocatorId = computed(() => getLocatorIdFromNodeData(nodeData))
const { executing, progress } = useNodeExecutionState(nodeLocatorId)
// Direct access to execution store for error state
const executionStore = useExecutionStore()

View File

@@ -42,7 +42,7 @@ import type {
INodeInputSlot,
INodeOutputSlot
} from '@/lib/litegraph/src/interfaces'
import { Rectangle, RenderShape } from '@/lib/litegraph/src/litegraph'
import { RenderShape } from '@/lib/litegraph/src/litegraph'
import NodeContent from '@/renderer/extensions/vueNodes/components/NodeContent.vue'
import NodeHeader from '@/renderer/extensions/vueNodes/components/NodeHeader.vue'
import NodeSlots from '@/renderer/extensions/vueNodes/components/NodeSlots.vue'
@@ -85,7 +85,7 @@ const nodeData = computed<VueNodeData>(() => {
name,
type: input.type,
shape: input.isOptional ? RenderShape.HollowCircle : undefined,
boundingRect: new Rectangle(0, 0, 0, 0),
boundingRect: [0, 0, 0, 0],
link: null
}))
@@ -94,13 +94,13 @@ const nodeData = computed<VueNodeData>(() => {
return {
name: output,
type: output,
boundingRect: new Rectangle(0, 0, 0, 0),
boundingRect: [0, 0, 0, 0],
links: []
}
}
return {
...output,
boundingRect: new Rectangle(0, 0, 0, 0),
boundingRect: [0, 0, 0, 0],
links: []
}
})

View File

@@ -9,7 +9,7 @@
:data-testid="`node-header-${nodeData?.id || ''}`"
@dblclick="handleDoubleClick"
>
<div class="flex items-center justify-between relative">
<div class="flex items-center justify-between gap-2.5 relative">
<!-- Collapse/Expand Button -->
<button
v-show="!readonly"
@@ -43,24 +43,22 @@
data-testid="node-pin-indicator"
/>
</div>
<div v-if="!readonly" class="flex items-center lod-toggle shrink-0">
<IconButton
v-if="isSubgraphNode"
size="sm"
type="transparent"
class="text-stone-200 dark-theme:text-slate-300"
data-testid="subgraph-enter-button"
title="Enter Subgraph"
@click.stop="handleEnterSubgraph"
@dblclick.stop
>
<i class="pi pi-external-link"></i>
</IconButton>
</div>
<LODFallback />
</div>
<!-- Title Buttons -->
<div v-if="!readonly" class="flex items-center lod-toggle">
<IconButton
v-if="isSubgraphNode"
size="sm"
type="transparent"
class="text-stone-200 dark-theme:text-slate-300"
data-testid="subgraph-enter-button"
title="Enter Subgraph"
@click.stop="handleEnterSubgraph"
@dblclick.stop
>
<i class="pi pi-external-link"></i>
</IconButton>
</div>
</div>
</template>

View File

@@ -7,7 +7,6 @@ import { createI18n } from 'vue-i18n'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import type { INodeOutputSlot } from '@/lib/litegraph/src/interfaces'
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
import { Rectangle } from '@/lib/litegraph/src/litegraph'
import enMessages from '@/locales/en/main.json' with { type: 'json' }
import NodeSlots from './NodeSlots.vue'
@@ -30,7 +29,7 @@ const makeNodeData = (overrides: Partial<VueNodeData> = {}): VueNodeData => ({
interface StubSlotData {
name?: string
type?: string
boundingRect?: Rectangle
boundingRect?: [number, number, number, number]
}
const InputSlotStub = defineComponent({
@@ -97,13 +96,13 @@ describe('NodeSlots.vue', () => {
const inputObjNoWidget = {
name: 'objNoWidget',
type: 'number',
boundingRect: new Rectangle(0, 0, 0, 0),
boundingRect: new Float32Array([0, 0, 0, 0]),
link: null
}
const inputObjWithWidget = {
name: 'objWithWidget',
type: 'number',
boundingRect: new Rectangle(0, 0, 0, 0),
boundingRect: new Float32Array([0, 0, 0, 0]),
widget: { name: 'objWithWidget' },
link: null
}
@@ -151,13 +150,13 @@ describe('NodeSlots.vue', () => {
const outputObj = {
name: 'outA',
type: 'any',
boundingRect: new Rectangle(0, 0, 0, 0),
boundingRect: new Float32Array([0, 0, 0, 0]),
links: []
}
const outputObjB = {
name: 'outB',
type: 'any',
boundingRect: new Rectangle(0, 0, 0, 0),
boundingRect: new Float32Array([0, 0, 0, 0]),
links: []
}
const outputs: INodeOutputSlot[] = [outputObj, outputObjB]

View File

@@ -34,7 +34,7 @@ import { computed, onErrorCaptured, ref } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { type INodeSlot, Rectangle } from '@/lib/litegraph/src/litegraph'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { isSlotObject } from '@/utils/typeGuardUtil'
import InputSlot from './InputSlot.vue'
@@ -60,30 +60,28 @@ const filteredInputs = computed(() => {
}
return true
})
.map(
(input): INodeSlot =>
isSlotObject(input)
? input
: {
name: typeof input === 'string' ? input : '',
type: 'any',
boundingRect: new Rectangle(0, 0, 0, 0)
}
.map((input) =>
isSlotObject(input)
? input
: ({
name: typeof input === 'string' ? input : '',
type: 'any',
boundingRect: [0, 0, 0, 0] as [number, number, number, number]
} as INodeSlot)
)
})
// Outputs don't have widgets, so we don't need to filter them
const filteredOutputs = computed(() => {
const outputs = nodeData?.outputs || []
return outputs.map(
(output): INodeSlot =>
isSlotObject(output)
? output
: {
name: typeof output === 'string' ? output : '',
type: 'any',
boundingRect: new Rectangle(0, 0, 0, 0)
}
return outputs.map((output) =>
isSlotObject(output)
? output
: ({
name: typeof output === 'string' ? output : '',
type: 'any',
boundingRect: [0, 0, 0, 0] as [number, number, number, number]
} as INodeSlot)
)
})

View File

@@ -30,7 +30,7 @@
:slot-data="{
name: widget.name,
type: widget.type,
boundingRect
boundingRect: [0, 0, 0, 0]
}"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
:index="getWidgetInputIndex(widget)"
@@ -60,7 +60,6 @@ import type {
VueNodeData
} from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { Rectangle } from '@/lib/litegraph/src/litegraph'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
// Import widget components directly
@@ -165,8 +164,6 @@ const processedWidgets = computed((): ProcessedWidget[] => {
return result
})
const boundingRect = new Rectangle(0, 0, 0, 0)
// TODO: Refactor to avoid O(n) lookup - consider storing input index on widget creation
// or restructuring data model to unify widgets and inputs
// Map a widget to its corresponding input slot index

View File

@@ -9,27 +9,27 @@ import { useExecutionStore } from '@/stores/executionStore'
* Provides reactive access to execution state and progress for a specific node
* by injecting execution data from the parent GraphCanvas provider.
*
* @param nodeIdMaybe - The ID of the node to track execution state for
* @param nodeLocatorIdMaybe - Locator ID (root or subgraph scoped) of the node to track
* @returns Object containing reactive execution state and progress
*/
export const useNodeExecutionState = (
nodeIdMaybe: MaybeRefOrGetter<string>
nodeLocatorIdMaybe: MaybeRefOrGetter<string | undefined>
) => {
const nodeId = toValue(nodeIdMaybe)
const { uniqueExecutingNodeIdStrings, nodeProgressStates } =
storeToRefs(useExecutionStore())
const locatorId = computed(() => toValue(nodeLocatorIdMaybe) ?? '')
const { nodeLocationProgressStates } = storeToRefs(useExecutionStore())
const executing = computed(() => {
return uniqueExecutingNodeIdStrings.value.has(nodeId)
const progressState = computed(() => {
const id = locatorId.value
return id ? nodeLocationProgressStates.value[id] : undefined
})
const executing = computed(() => progressState.value?.state === 'running')
const progress = computed(() => {
const state = nodeProgressStates.value[nodeId]
return state?.max > 0 ? state.value / state.max : undefined
const state = progressState.value
return state && state.max > 0 ? state.value / state.max : undefined
})
const progressState = computed(() => nodeProgressStates.value[nodeId])
const progressPercentage = computed(() => {
const prog = progress.value
return prog !== undefined ? Math.round(prog * 100) : undefined

View File

@@ -1,4 +1,4 @@
import type { Rect } from '@/lib/litegraph/src/interfaces'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
import type { Bounds } from '@/renderer/core/layout/types'
/**
@@ -33,7 +33,9 @@ export const lcm = (a: number, b: number): number => {
* @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 Rect[]): Bounds | null {
export function computeUnionBounds(
rectangles: readonly ReadOnlyRect[]
): Bounds | null {
const n = rectangles.length
if (n === 0) {
return null

View File

@@ -1,7 +1,6 @@
import { afterEach, beforeEach, describe, expect, vi } from 'vitest'
import type { INodeInputSlot, Point } from '@/lib/litegraph/src/litegraph'
import { Rectangle } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LGraph } from '@/lib/litegraph/src/litegraph'
import { NodeInputSlot } from '@/lib/litegraph/src/litegraph'
@@ -85,8 +84,8 @@ describe('LGraphNode', () => {
}))
}
node.configure(configureData)
expect(node.pos).toEqual([50, 60])
expect(node.size).toEqual([70, 80])
expect(node.pos).toEqual(new Float32Array([50, 60]))
expect(node.size).toEqual(new Float32Array([70, 80]))
})
test('should configure inputs correctly', () => {
@@ -572,7 +571,7 @@ describe('LGraphNode', () => {
name: 'test_in',
type: 'string',
link: null,
boundingRect: new Rectangle(0, 0, 0, 0)
boundingRect: new Float32Array([0, 0, 0, 0])
}
})
test('should return position based on title height when collapsed', () => {
@@ -595,7 +594,7 @@ describe('LGraphNode', () => {
name: 'test_in_2',
type: 'number',
link: null,
boundingRect: new Rectangle(0, 0, 0, 0)
boundingRect: new Float32Array([0, 0, 0, 0])
}
node.inputs = [inputSlot, inputSlot2]
const slotIndex = 0

View File

@@ -4,19 +4,19 @@ exports[`LGraph > supports schema v0.4 graphs > oldSchemaGraph 1`] = `
LGraph {
"_groups": [
LGraphGroup {
"_bounding": [
10,
10,
140,
80,
"_bounding": Float32Array [
20,
20,
1,
3,
],
"_children": Set {},
"_nodes": [],
"_pos": [
"_pos": Float32Array [
20,
20,
],
"_size": [
"_size": Float32Array [
1,
3,
],
@@ -39,19 +39,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -111,19 +111,19 @@ LGraph {
"1": LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -184,19 +184,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],

View File

@@ -1,7 +1,6 @@
// TODO: Fix these tests after migration
import { test as baseTest } from 'vitest'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { Point, Rect } from '@/lib/litegraph/src/interfaces'
import {
addDirectionalOffset,
@@ -133,8 +132,8 @@ test('snapPoint correctly snaps points to grid', ({ expect }) => {
test('createBounds correctly creates bounding box', ({ expect }) => {
const objects = [
{ boundingRect: new Rectangle(0, 0, 10, 10) },
{ boundingRect: new Rectangle(5, 5, 10, 10) }
{ boundingRect: [0, 0, 10, 10] as Rect },
{ boundingRect: [5, 5, 10, 10] as Rect }
]
const defaultBounds = createBounds(objects)

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { Rect } from '@/lib/litegraph/src/interfaces'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
import { computeUnionBounds, gcd, lcm } from '@/utils/mathUtil'
describe('mathUtil', () => {
@@ -27,9 +27,9 @@ describe('mathUtil', () => {
expect(computeUnionBounds([])).toBe(null)
})
// Tests for tuple format (Rect)
it('should work with Rect tuple format', () => {
const tuples: Rect[] = [
// Tests for tuple format (ReadOnlyRect)
it('should work with ReadOnlyRect tuple format', () => {
const tuples: ReadOnlyRect[] = [
[10, 20, 30, 40] as const, // bounds: 10,20 to 40,60
[50, 10, 20, 30] as const // bounds: 50,10 to 70,40
]
@@ -44,8 +44,8 @@ describe('mathUtil', () => {
})
})
it('should handle single Rect tuple', () => {
const tuple: Rect = [10, 20, 30, 40] as const
it('should handle single ReadOnlyRect tuple', () => {
const tuple: ReadOnlyRect = [10, 20, 30, 40] as const
const result = computeUnionBounds([tuple])
expect(result).toEqual({
@@ -57,7 +57,7 @@ describe('mathUtil', () => {
})
it('should handle tuple format with negative dimensions', () => {
const tuples: Rect[] = [
const tuples: ReadOnlyRect[] = [
[100, 50, -20, -10] as const, // x+width=80, y+height=40
[90, 45, 15, 20] as const // x+width=105, y+height=65
]
@@ -74,7 +74,7 @@ describe('mathUtil', () => {
it('should maintain optimal performance with SoA tuples', () => {
// Test that array access is as expected for typical selection sizes
const tuples: Rect[] = Array.from(
const tuples: ReadOnlyRect[] = Array.from(
{ length: 10 },
(_, i) =>
[