Compare commits

..

2 Commits

Author SHA1 Message Date
bymyself
a476be3933 [refactor] Extract keybinding functionality into @comfyorg/keybinding package
Create framework-agnostic keybinding package following domain-driven design patterns.
Move pure business logic to package while keeping Vue integration in workbench layer.

Changes:
- Add @comfyorg/keybinding package with KeyComboImpl and KeybindingImpl classes
- Move core keybindings and reserved key constants to package
- Update workbench layer to import from package with backward compatibility
- Update all imports across codebase to use package exports
- Maintain existing API surface for consumers
2025-10-12 20:51:31 -07:00
bymyself
42ffdb2141 refactor keybinding files 2025-09-30 11:55:27 -07:00
72 changed files with 1035 additions and 754 deletions

View File

@@ -1,3 +1,4 @@
import type { KeyCombo } from '@comfyorg/keybinding'
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base } from '@playwright/test'
@@ -6,7 +7,6 @@ import * as fs from 'fs'
import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar'

View File

@@ -1,7 +1,7 @@
import type { Keybinding } from '@comfyorg/keybinding'
import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -0,0 +1,134 @@
# Keybinding Domain DDD Refactoring
## Overview
This document outlines the refactoring of ComfyUI's keybinding functionality from technical layers to domain-driven design (DDD) following VSCode's proven three-layer architecture.
## Architecture
### Three-Layer Design
Following VSCode's enterprise-grade architecture:
1. **Base Layer** - Foundational utilities (no dependencies)
2. **Platform Layer** - Core abstractions (reusable across frontends)
3. **Workbench Layer** - UI-specific implementation
### Final Structure
```
src/
├── base/keybinding/ # Base Layer
│ └── reservedKeyCombos.ts # Reserved key combinations
├── platform/keybinding/ # Platform Layer
│ ├── constants/
│ │ └── coreKeybindings.ts # Core keybinding definitions
│ └── types/
│ └── keybinding.ts # Schema types and interfaces
└── workbench/keybindings/ # Workbench Layer
├── components/ # UI Components
│ ├── KeybindingPanel.vue # Settings panel
│ ├── KeyComboDisplay.vue # Key display component
│ └── shortcuts/ # Shortcuts panel
│ ├── EssentialsPanel.vue
│ ├── ShortcutsList.vue
│ └── ViewControlsPanel.vue
├── composables/
│ └── useShortcutsTab.ts # UI logic
├── services/
│ └── keybindingService.ts # Service implementation
└── stores/
└── keybindingStore.ts # State management
```
## Migration Summary
### Before (Technical Layers)
- `services/keybindingService.ts`
- `stores/keybindingStore.ts`
- `constants/coreKeybindings.ts`
- `schemas/keyBindingSchema.ts`
- `components/dialog/content/setting/KeybindingPanel.vue`
- `components/bottomPanel/tabs/shortcuts/`
- `composables/bottomPanelTabs/useShortcutsTab.ts`
### After (Domain-Driven)
All keybinding functionality organized by architectural layer and domain responsibility.
## Key Import Paths
```typescript
// Platform types and constants
import type { Keybinding, KeyCombo } from '@/platform/keybinding/types/keybinding'
import { CORE_KEYBINDINGS } from '@/platform/keybinding/constants/coreKeybindings'
// Base utilities
import { RESERVED_BY_TEXT_INPUT } from '@/base/keybinding/reservedKeyCombos'
// Workbench services
import { useKeybindingService } from '@/workbench/keybindings/services/keybindingService'
import {
KeyComboImpl,
KeybindingImpl,
useKeybindingStore
} from '@/workbench/keybindings/stores/keybindingStore'
// UI components
import KeybindingPanel from '@/workbench/keybindings/components/KeybindingPanel.vue'
import { useShortcutsTab } from '@/workbench/keybindings/composables/useShortcutsTab'
```
## Benefits
### 1. Clear Architectural Boundaries
- **Base**: Reusable across any JavaScript environment
- **Platform**: Reusable across any UI framework
- **Workbench**: Vue/PrimeVue specific implementation
### 2. Frontend Flexibility
The base + platform layers enable building alternative frontends (React, Angular, etc.) while reusing core keybinding logic.
### 3. VSCode Alignment
Follows the same proven patterns used by one of the most successful code editors, ensuring scalability and maintainability.
### 4. Domain Cohesion
All keybinding-related functionality is now located together, making it easier to:
- Find related code
- Make changes across the domain
- Test domain functionality
- Understand feature scope
### 5. Dependency Management
Clear dependency flow: `base/``platform/``workbench/`
## Migration Process
1.**Analysis**: Identified all keybinding-related files
2.**Structure Creation**: Built three-layer directory structure
3.**File Migration**: Moved files to appropriate layers
4.**Import Updates**: Updated all import paths using `@` aliases
5.**Testing**: Verified TypeScript compilation and linting
6.**Cleanup**: Removed old files and empty directories
## Quality Assurance
- **TypeScript**: `pnpm typecheck` passes
- **Code Quality**: `pnpm lint --fix` applied
- **Testing**: All test imports updated
- **No Breaking Changes**: All functionality preserved
## Future Considerations
### Potential Enhancements
- Add platform-level abstractions as needed
- Create additional keybinding editor components
- Consider splitting large components into smaller, focused ones
### Extension Points
- New UI components can be added to `workbench/keybindings/components/`
- Platform-level services can be extended in `platform/keybinding/`
- Base utilities can be enhanced in `base/keybinding/`
This refactoring establishes a solid foundation for future keybinding feature development while maintaining backward compatibility and improving code organization.

View File

@@ -107,6 +107,7 @@
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "0.4.73-0",
"@comfyorg/design-system": "workspace:*",
"@comfyorg/keybinding": "workspace:*",
"@comfyorg/registry-types": "workspace:*",
"@comfyorg/tailwind-utils": "workspace:*",
"@iconify/json": "^2.2.380",

View File

@@ -0,0 +1,29 @@
{
"name": "@comfyorg/keybinding",
"version": "1.0.0",
"type": "module",
"description": "Framework-agnostic keybinding system for ComfyUI",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts"
}
},
"scripts": {
"typecheck": "tsc --noEmit"
},
"nx": {
"tags": [
"scope:shared",
"type:lib"
]
},
"dependencies": {
"zod": "^3.22.4"
},
"devDependencies": {
"typescript": "^5.4.5"
}
}

View File

@@ -1,4 +1,4 @@
import type { Keybinding } from '@/schemas/keyBindingSchema'
import type { Keybinding } from '../types/keybinding'
export const CORE_KEYBINDINGS: Keybinding[] = [
{

View File

@@ -0,0 +1,11 @@
// Types
export type * from './types/keybinding'
export { zKeybinding } from './types/keybinding'
// Models (Implementation classes)
export { KeyComboImpl } from './models/KeyCombo'
export { KeybindingImpl } from './models/Keybinding'
// Constants
export { CORE_KEYBINDINGS } from './constants/coreKeybindings'
export { RESERVED_BY_TEXT_INPUT } from './constants/reservedKeyCombos'

View File

@@ -0,0 +1,83 @@
import { RESERVED_BY_TEXT_INPUT } from '../constants/reservedKeyCombos'
import type { KeyCombo } from '../types/keybinding'
export class KeyComboImpl implements KeyCombo {
key: string
// ctrl or meta(cmd on mac)
ctrl: boolean
alt: boolean
shift: boolean
constructor(obj: KeyCombo) {
this.key = obj.key
this.ctrl = obj.ctrl ?? false
this.alt = obj.alt ?? false
this.shift = obj.shift ?? false
}
static fromEvent(event: KeyboardEvent) {
return new KeyComboImpl({
key: event.key,
ctrl: event.ctrlKey || event.metaKey,
alt: event.altKey,
shift: event.shiftKey
})
}
equals(other: unknown): boolean {
return other instanceof KeyComboImpl
? this.key.toUpperCase() === other.key.toUpperCase() &&
this.ctrl === other.ctrl &&
this.alt === other.alt &&
this.shift === other.shift
: false
}
serialize(): string {
return `${this.key.toUpperCase()}:${this.ctrl}:${this.alt}:${this.shift}`
}
toString(): string {
return this.getKeySequences().join(' + ')
}
get hasModifier(): boolean {
return this.ctrl || this.alt || this.shift
}
get isModifier(): boolean {
return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key)
}
get modifierCount(): number {
const modifiers = [this.ctrl, this.alt, this.shift]
return modifiers.reduce((acc, cur) => acc + Number(cur), 0)
}
get isShiftOnly(): boolean {
return this.shift && this.modifierCount === 1
}
get isReservedByTextInput(): boolean {
return (
!this.hasModifier ||
this.isShiftOnly ||
RESERVED_BY_TEXT_INPUT.has(this.toString())
)
}
getKeySequences(): string[] {
const sequences: string[] = []
if (this.ctrl) {
sequences.push('Ctrl')
}
if (this.alt) {
sequences.push('Alt')
}
if (this.shift) {
sequences.push('Shift')
}
sequences.push(this.key)
return sequences
}
}

View File

@@ -0,0 +1,22 @@
import type { Keybinding } from '../types/keybinding'
import { KeyComboImpl } from './KeyCombo'
export class KeybindingImpl implements Keybinding {
commandId: string
combo: KeyComboImpl
targetElementId?: string
constructor(obj: Keybinding) {
this.commandId = obj.commandId
this.combo = new KeyComboImpl(obj.combo)
this.targetElementId = obj.targetElementId
}
equals(other: unknown): boolean {
return other instanceof KeybindingImpl
? this.commandId === other.commandId &&
this.combo.equals(other.combo) &&
this.targetElementId === other.targetElementId
: false
}
}

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

13
pnpm-lock.yaml generated
View File

@@ -20,6 +20,9 @@ importers:
'@comfyorg/design-system':
specifier: workspace:*
version: link:packages/design-system
'@comfyorg/keybinding':
specifier: workspace:*
version: link:packages/keybinding
'@comfyorg/registry-types':
specifier: workspace:*
version: link:packages/registry-types
@@ -371,6 +374,16 @@ importers:
specifier: ^5.4.5
version: 5.9.2
packages/keybinding:
dependencies:
zod:
specifier: ^3.22.4
version: 3.24.1
devDependencies:
typescript:
specifier: ^5.4.5
version: 5.9.2
packages/registry-types: {}
packages/shared-frontend-utils:

View File

@@ -38,10 +38,10 @@ import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useUserStore } from '@/stores/userStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import type { SidebarTabExtension } from '@/types/extensionTypes'
import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore'
import SidebarHelpCenterIcon from './SidebarHelpCenterIcon.vue'
import SidebarIcon from './SidebarIcon.vue'

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

@@ -101,7 +101,7 @@ export function useSettingUI(
children: []
},
component: defineAsyncComponent(
() => import('@/components/dialog/content/setting/KeybindingPanel.vue')
() => import('@/workbench/keybindings/components/KeybindingPanel.vue')
)
}

View File

@@ -1,8 +1,9 @@
import type { Keybinding } from '@comfyorg/keybinding'
import { LinkMarkerShape, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { SettingParams } from '@/platform/settings/types'
import type { ColorPalettes } from '@/schemas/colorPaletteSchema'
import type { Keybinding } from '@/schemas/keyBindingSchema'
import { NodeBadgeMode } from '@/types/nodeSource'
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'

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

@@ -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

@@ -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

@@ -1,3 +1,4 @@
import { zKeybinding } from '@comfyorg/keybinding'
import { z } from 'zod'
import { LinkMarkerShape } from '@/lib/litegraph/src/litegraph'
@@ -6,7 +7,6 @@ import {
zNodeId
} from '@/platform/workflow/validation/schemas/workflowSchema'
import { colorPalettesSchema } from '@/schemas/colorPaletteSchema'
import { zKeybinding } from '@/schemas/keyBindingSchema'
import { NodeBadgeMode } from '@/types/nodeSource'
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'

View File

@@ -1,3 +1,4 @@
import { KeyComboImpl } from '@comfyorg/keybinding'
import _ from 'es-toolkit/compat'
import type { ToastMessageOptions } from 'primevue/toast'
import { reactive } from 'vue'
@@ -53,7 +54,6 @@ import { useExecutionStore } from '@/stores/executionStore'
import { useExtensionStore } from '@/stores/extensionStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
import { useModelStore } from '@/stores/modelStore'
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
import { useSubgraphStore } from '@/stores/subgraphStore'
@@ -81,6 +81,7 @@ import {
} from '@/utils/migration/migrateReroute'
import { getSelectedModelsMetadata } from '@/utils/modelMetadataUtil'
import { deserialiseAndCreate } from '@/utils/vintageClipboard'
import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore'
import { type ComfyApi, PromptExecutionError, api } from './api'
import { defaultGraph } from './defaultGraph'

View File

@@ -5,11 +5,14 @@ import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useExtensionStore } from '@/stores/extensionStore'
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useWidgetStore } from '@/stores/widgetStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import type { ComfyExtension } from '@/types/comfy'
import {
KeybindingImpl,
useKeybindingStore
} from '@/workbench/keybindings/stores/keybindingStore'
export const useExtensionService = () => {
const extensionStore = useExtensionStore()

View File

@@ -3,8 +3,10 @@ import { computed, ref } from 'vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { ComfyExtension } from '@/types/comfy'
import { type KeybindingImpl, useKeybindingStore } from './keybindingStore'
import {
type KeybindingImpl,
useKeybindingStore
} from '@/workbench/keybindings/stores/keybindingStore'
export interface ComfyCommand {
id: string

View File

@@ -1,7 +1,6 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useShortcutsTab } from '@/composables/bottomPanelTabs/useShortcutsTab'
import {
useCommandTerminalTab,
useLogsTerminalTab
@@ -10,6 +9,7 @@ import { useCommandStore } from '@/stores/commandStore'
import type { ComfyExtension } from '@/types/comfy'
import type { BottomPanelExtension } from '@/types/extensionTypes'
import { isElectron } from '@/utils/envUtil'
import { useShortcutsTab } from '@/workbench/keybindings/composables/useShortcutsTab'
type PanelType = 'terminal' | 'shortcuts'

View File

@@ -1,8 +1,9 @@
import type { Keybinding } from '@comfyorg/keybinding'
import type { Positionable } from '@/lib/litegraph/src/interfaces'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { SettingParams } from '@/platform/settings/types'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { Keybinding } from '@/schemas/keyBindingSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import type { ComfyApp } from '@/scripts/app'
import type { ComfyWidgetConstructor } from '@/scripts/widgets'

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

@@ -53,7 +53,6 @@ import type { StatusWsMessageStatus } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { setupAutoQueueHandler } from '@/services/autoQueueService'
import { useKeybindingService } from '@/services/keybindingService'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
@@ -69,6 +68,7 @@ import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { useKeybindingService } from '@/workbench/keybindings/services/keybindingService'
setupAutoQueueHandler()
useProgressFavicon()

View File

@@ -13,7 +13,7 @@
import Tag from 'primevue/tag'
import { computed } from 'vue'
import type { KeyComboImpl } from '@/stores/keybindingStore'
import type { KeyComboImpl } from '@/workbench/keybindings/stores/keybindingStore'
const { keyCombo, isModified = false } = defineProps<{
keyCombo: KeyComboImpl

View File

@@ -142,17 +142,17 @@ import { computed, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import SearchBox from '@/components/common/SearchBox.vue'
import { useKeybindingService } from '@/services/keybindingService'
import PanelTemplate from '@/components/dialog/content/setting/PanelTemplate.vue'
import { useCommandStore } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { useKeybindingService } from '@/workbench/keybindings/services/keybindingService'
import {
KeyComboImpl,
KeybindingImpl,
useKeybindingStore
} from '@/stores/keybindingStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
} from '@/workbench/keybindings/stores/keybindingStore'
import PanelTemplate from './PanelTemplate.vue'
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'
import KeyComboDisplay from './KeyComboDisplay.vue'
const filters = ref({
global: { value: '', matchMode: FilterMatchMode.CONTAINS }

View File

@@ -0,0 +1,33 @@
<template>
<div class="h-full flex flex-col p-4">
<div class="flex-1 min-h-0 overflow-auto">
<ShortcutsList
:commands="essentialsCommands"
:subcategories="essentialsSubcategories"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import {
ESSENTIALS_CONFIG,
useCommandSubcategories
} from '@/composables/bottomPanelTabs/useCommandSubcategories'
import { useCommandStore } from '@/stores/commandStore'
import ShortcutsList from './ShortcutsList.vue'
const commandStore = useCommandStore()
const essentialsCommands = computed(() =>
commandStore.commands.filter((cmd) => cmd.category === 'essentials')
)
const { subcategories: essentialsSubcategories } = useCommandSubcategories(
essentialsCommands,
ESSENTIALS_CONFIG
)
</script>

View File

@@ -0,0 +1,120 @@
<template>
<div class="shortcuts-list flex justify-center">
<div class="grid gap-4 md:gap-24 h-full grid-cols-1 md:grid-cols-3 w-[90%]">
<div
v-for="(subcategoryCommands, subcategory) in filteredSubcategories"
:key="subcategory"
class="flex flex-col"
>
<h3
class="subcategory-title text-xs font-bold uppercase tracking-wide text-surface-600 dark-theme:text-surface-400 mb-4"
>
{{ getSubcategoryTitle(subcategory) }}
</h3>
<div class="flex flex-col gap-1">
<div
v-for="command in subcategoryCommands"
:key="command.id"
class="shortcut-item flex justify-between items-center py-2 rounded hover:bg-surface-100 dark-theme:hover:bg-surface-700 transition-colors duration-200"
>
<div class="shortcut-info grow pr-4">
<div class="shortcut-name text-sm font-medium">
{{ t(`commands.${normalizeI18nKey(command.id)}.label`) }}
</div>
</div>
<div class="keybinding-display shrink-0">
<div
class="keybinding-combo flex gap-1"
:aria-label="`Keyboard shortcut: ${command.keybinding!.combo.getKeySequences().join(' + ')}`"
>
<span
v-for="key in command.keybinding!.combo.getKeySequences()"
:key="key"
class="key-badge px-2 py-1 text-xs font-mono bg-surface-200 dark-theme:bg-surface-600 rounded border min-w-6 text-center"
>
{{ formatKey(key) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ComfyCommandImpl } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
const { t } = useI18n()
const { subcategories } = defineProps<{
commands: ComfyCommandImpl[]
subcategories: Record<string, ComfyCommandImpl[]>
}>()
const filteredSubcategories = computed(() => {
const result: Record<string, ComfyCommandImpl[]> = {}
for (const [subcategory, commands] of Object.entries(subcategories)) {
result[subcategory] = commands.filter((cmd) => !!cmd.keybinding)
}
return result
})
const getSubcategoryTitle = (subcategory: string): string => {
const titleMap: Record<string, string> = {
workflow: t('shortcuts.subcategories.workflow'),
node: t('shortcuts.subcategories.node'),
queue: t('shortcuts.subcategories.queue'),
view: t('shortcuts.subcategories.view'),
'panel-controls': t('shortcuts.subcategories.panelControls')
}
return titleMap[subcategory] || subcategory
}
const formatKey = (key: string): string => {
const keyMap: Record<string, string> = {
Control: 'Ctrl',
Meta: 'Cmd',
ArrowUp: '↑',
ArrowDown: '↓',
ArrowLeft: '←',
ArrowRight: '→',
Backspace: '⌫',
Delete: '⌦',
Enter: '↵',
Escape: 'Esc',
Tab: '⇥',
' ': 'Space'
}
return keyMap[key] || key
}
</script>
<style scoped>
.subcategory-title {
color: var(--p-text-muted-color);
}
.key-badge {
background-color: var(--p-surface-200);
border: 1px solid var(--p-surface-300);
min-width: 1.5rem;
text-align: center;
}
.dark-theme .key-badge {
background-color: var(--p-surface-600);
border-color: var(--p-surface-500);
}
</style>

View File

@@ -1,9 +1,9 @@
import { markRaw } from 'vue'
import { useI18n } from 'vue-i18n'
import EssentialsPanel from '@/components/bottomPanel/tabs/shortcuts/EssentialsPanel.vue'
import ViewControlsPanel from '@/components/bottomPanel/tabs/shortcuts/ViewControlsPanel.vue'
import type { BottomPanelExtension } from '@/types/extensionTypes'
import EssentialsPanel from '@/workbench/keybindings/components/shortcuts/EssentialsPanel.vue'
import ViewControlsPanel from '@/workbench/keybindings/components/shortcuts/ViewControlsPanel.vue'
export const useShortcutsTab = (): BottomPanelExtension[] => {
const { t } = useI18n()

View File

@@ -1,13 +1,14 @@
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
import {
CORE_KEYBINDINGS,
KeyComboImpl,
KeybindingImpl
} from '@comfyorg/keybinding'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import {
KeyComboImpl,
KeybindingImpl,
useKeybindingStore
} from '@/stores/keybindingStore'
import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore'
export const useKeybindingService = () => {
const keybindingStore = useKeybindingStore()

View File

@@ -1,115 +1,11 @@
import { KeyComboImpl, KeybindingImpl } from '@comfyorg/keybinding'
import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import type { Ref } from 'vue'
import { computed, ref, toRaw } from 'vue'
import { computed, ref } from 'vue'
import { RESERVED_BY_TEXT_INPUT } from '@/constants/reservedKeyCombos'
import type { KeyCombo, Keybinding } from '@/schemas/keyBindingSchema'
export class KeybindingImpl implements Keybinding {
commandId: string
combo: KeyComboImpl
targetElementId?: string
constructor(obj: Keybinding) {
this.commandId = obj.commandId
this.combo = new KeyComboImpl(obj.combo)
this.targetElementId = obj.targetElementId
}
equals(other: unknown): boolean {
const raw = toRaw(other)
return raw instanceof KeybindingImpl
? this.commandId === raw.commandId &&
this.combo.equals(raw.combo) &&
this.targetElementId === raw.targetElementId
: false
}
}
export class KeyComboImpl implements KeyCombo {
key: string
// ctrl or meta(cmd on mac)
ctrl: boolean
alt: boolean
shift: boolean
constructor(obj: KeyCombo) {
this.key = obj.key
this.ctrl = obj.ctrl ?? false
this.alt = obj.alt ?? false
this.shift = obj.shift ?? false
}
static fromEvent(event: KeyboardEvent) {
return new KeyComboImpl({
key: event.key,
ctrl: event.ctrlKey || event.metaKey,
alt: event.altKey,
shift: event.shiftKey
})
}
equals(other: unknown): boolean {
const raw = toRaw(other)
return raw instanceof KeyComboImpl
? this.key.toUpperCase() === raw.key.toUpperCase() &&
this.ctrl === raw.ctrl &&
this.alt === raw.alt &&
this.shift === raw.shift
: false
}
serialize(): string {
return `${this.key.toUpperCase()}:${this.ctrl}:${this.alt}:${this.shift}`
}
toString(): string {
return this.getKeySequences().join(' + ')
}
get hasModifier(): boolean {
return this.ctrl || this.alt || this.shift
}
get isModifier(): boolean {
return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key)
}
get modifierCount(): number {
const modifiers = [this.ctrl, this.alt, this.shift]
return modifiers.reduce((acc, cur) => acc + Number(cur), 0)
}
get isShiftOnly(): boolean {
return this.shift && this.modifierCount === 1
}
get isReservedByTextInput(): boolean {
return (
!this.hasModifier ||
this.isShiftOnly ||
RESERVED_BY_TEXT_INPUT.has(this.toString())
)
}
getKeySequences(): string[] {
const sequences: string[] = []
if (this.ctrl) {
sequences.push('Ctrl')
}
if (this.alt) {
sequences.push('Alt')
}
if (this.shift) {
sequences.push('Shift')
}
sequences.push(this.key)
return sequences
}
}
// Re-export classes from package for backward compatibility
export { KeybindingImpl, KeyComboImpl }
export const useKeybindingStore = defineStore('keybinding', () => {
/**

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,15 +1,15 @@
import {
CORE_KEYBINDINGS,
KeyComboImpl,
KeybindingImpl
} from '@comfyorg/keybinding'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
import { useKeybindingService } from '@/services/keybindingService'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import {
KeyComboImpl,
KeybindingImpl,
useKeybindingStore
} from '@/stores/keybindingStore'
import { useKeybindingService } from '@/workbench/keybindings/services/keybindingService'
import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore'
// Mock stores
vi.mock('@/platform/settings/settingStore', () => ({

View File

@@ -2,9 +2,9 @@ import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { app } from '@/scripts/app'
import { useKeybindingService } from '@/services/keybindingService'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useKeybindingService } from '@/workbench/keybindings/services/keybindingService'
// Mock the app and canvas using factory functions
vi.mock('@/scripts/app', () => {

View File

@@ -1,7 +1,8 @@
import { KeybindingImpl } from '@comfyorg/keybinding'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore'
describe('useKeybindingStore', () => {
beforeEach(() => {

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) =>
[