Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
f42a4dd
[feat] Add core Vue widget infrastructure
christian-byrne Jun 24, 2025
2dcfe84
[feat] Add Vue widget registry system
christian-byrne Jun 24, 2025
06d0a63
[feat] Add Vue input widgets
christian-byrne Jun 24, 2025
ac60d1c
[feat] Add Vue selection widgets
christian-byrne Jun 24, 2025
9c4d782
[feat] Add Vue visual widgets
christian-byrne Jun 24, 2025
6e04cb7
[feat] Add Vue action widgets
christian-byrne Jun 24, 2025
19084e2
[feat] TransformPane - Viewport synchronization layer for Vue nodes (…
christian-byrne Aug 6, 2025
9db96f2
Update locales [skip ci]
invalid-email-address Aug 7, 2025
fc6943c
Fix TransformPane pos/size (#4826)
christian-byrne Aug 8, 2025
ac17752
Update locales [skip ci]
invalid-email-address Aug 8, 2025
301355e
refactor(litegraph): decouple render-time state from models for rerou…
benceruleanlu Aug 8, 2025
4171219
Revert "refactor(litegraph): decouple render-time state from models f…
benceruleanlu Aug 8, 2025
2b9a9e2
test(ci): skip transformPerformance suite on CI (#4843)
benceruleanlu Aug 9, 2025
8df41ab
Add vue node feature flag (#4927)
benceruleanlu Aug 12, 2025
c773230
feat: Implement CRDT-based layout system for Vue nodes (#4959)
christian-byrne Aug 17, 2025
889d136
[chore] Extract link rendering out of LGraphCanvas (#4994)
benceruleanlu Aug 18, 2025
0dd4ff2
refactor: Reorganize layout system into new renderer architecture (#5…
christian-byrne Aug 18, 2025
bfcbcf4
[refactor] Reorganize Vue nodes to domain-driven design architecture …
christian-byrne Aug 18, 2025
d1ed5ec
fix: Initialize Vue node manager when first node is added to empty gr…
christian-byrne Aug 19, 2025
b2a828d
[bugfix] Fix Vue node import path after refactoring
christian-byrne Aug 19, 2025
934c650
Remove layout logging noise from console (#5101)
christian-byrne Aug 19, 2025
5a74c01
remove logging from vue node layouting modules (#5111)
christian-byrne Aug 19, 2025
5171dec
feat: Add slot registration and spatial indexing for hit detection
benceruleanlu Aug 20, 2025
ba1fa1b
Revert "feat: Add slot registration and spatial indexing for hit dete…
benceruleanlu Aug 20, 2025
4a7c955
[bugfix] Fix link center dot hit detection when marker is disabled (#…
benceruleanlu Aug 20, 2025
1447b15
[bugfix] Hide center dot when dragging links (#5133)
benceruleanlu Aug 21, 2025
57db10f
feat: v3 style of node body (#5169)
LittleSound Aug 23, 2025
3982f29
Update lockfile after rebase (#5254)
benceruleanlu Aug 30, 2025
0830959
Fix lodash import (#5269)
benceruleanlu Aug 30, 2025
2a5e0d2
Decouple link and slot hit-testing out of Litegraph (#5134)
benceruleanlu Sep 1, 2025
62096d4
chore: Empty commit to trigger CI checks
benceruleanlu Sep 1, 2025
da042ae
[refactor] Remove unused legacy mutation types from layout system (#5…
christian-byrne Sep 1, 2025
3ce3b67
feat: localization fields (#5318)
LittleSound Sep 3, 2025
c6fc8e6
fix: remove clipping by removing unnecessary css contain (#5327)
simula-r Sep 4, 2025
969c8e6
[bugfix] Remove placeholder IMAGE widget to restore previous function…
benceruleanlu Sep 4, 2025
f83801e
- Convert class-based LayoutMutations to useLayoutMutations() composa…
christian-byrne Sep 4, 2025
1dbbf20
feat: widget styles for V3 UI (#5320)
LittleSound Sep 4, 2025
32cffa6
refactor: v3 ui slots connection dots (#5316)
LittleSound Sep 4, 2025
8a10387
add explicit typing on component IDs (#5352)
christian-byrne Sep 4, 2025
c30f5a4
Remove IMAGE widget cont. (#5355)
benceruleanlu Sep 4, 2025
6a3c075
Removes node's dependency on LGraph for access to layout mutations co…
christian-byrne Sep 4, 2025
2a64f53
Merge remote-tracking branch 'origin/main' into vue-nodes-migration
benceruleanlu Sep 4, 2025
f99c9de
[fix] Disable link markers on dragged connections (#5358)
benceruleanlu Sep 4, 2025
1480dd7
[bugfix] Fix NodeHeader test workflow path (#5359)
benceruleanlu Sep 4, 2025
2425f65
[Vue Nodes] Fix Node Header Tests (#5360)
benceruleanlu Sep 4, 2025
0f5315f
Update test expectations [skip ci]
invalid-email-address Sep 4, 2025
7b7f9bb
remove crdt ADR (moved to separate PR)
christian-byrne Sep 5, 2025
7130794
update adr README
christian-byrne Sep 5, 2025
73a1fee
removed unused IMAGE widget enum value
christian-byrne Sep 5, 2025
b0f2a1d
remove all unused (knip pass)
christian-byrne Sep 5, 2025
2f512b8
remove debug overlay panel
christian-byrne Sep 5, 2025
4fc8984
simplify unit tests
christian-byrne Sep 5, 2025
7149af6
change name "transformPaneEnabled" => "isVueNodesEnabled"
christian-byrne Sep 5, 2025
0b94158
remove debug viewport visualizer
christian-byrne Sep 5, 2025
3e5effe
remove debug viewport visualizer prop
christian-byrne Sep 5, 2025
9ab075f
remove outdated README
christian-byrne Sep 5, 2025
1e30756
skip all vue node operations if feature is turned off
christian-byrne Sep 5, 2025
e8dae57
remove debug logging and setting
christian-byrne Sep 5, 2025
f6051f6
remove event forwarding hack. todo: add link moving in vue
christian-byrne Sep 5, 2025
df36693
cleanup comments
christian-byrne Sep 5, 2025
358d98e
cleanup comments
christian-byrne Sep 5, 2025
b7fd1f4
add missing translations
christian-byrne Sep 5, 2025
0aed837
use camelCase for all non-component files
christian-byrne Sep 5, 2025
7d8bdcb
remove debug viewport test
christian-byrne Sep 5, 2025
6eeba70
- Fix memory leaks in node deletion (#5345)
christian-byrne Sep 5, 2025
817f4d0
remove redundant comment
christian-byrne Sep 5, 2025
07b7ed9
use camelcase for layoutStore filename
christian-byrne Sep 5, 2025
85fa2f4
removed unused type guards
christian-byrne Sep 5, 2025
8e098fc
simplify widget registration
christian-byrne Sep 5, 2025
896e44f
move back test that was mistakenly moved
christian-byrne Sep 5, 2025
f6ae1b6
remove unused typeguards
christian-byrne Sep 5, 2025
c52c798
removed unused node def type guards
christian-byrne Sep 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor(litegraph): decouple render-time state from models for rerou…
…tes and links\n\nIntroduce RenderedLinkSegment; compute reroute render params without mutating model; render into ephemeral segments instead of writing to Reroute/LLink.
  • Loading branch information
benceruleanlu committed Aug 29, 2025
commit 301355e018f23175069fe3c7911c4eb4d7fbfe28
65 changes: 47 additions & 18 deletions src/lib/litegraph/src/LGraphCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { LGraphGroup } from './LGraphGroup'
import { LGraphNode, type NodeId, type NodeProperty } from './LGraphNode'
import { LLink, type LinkId } from './LLink'
import { Reroute, type RerouteId } from './Reroute'
import { RenderedLinkSegment } from './canvas/RenderedLinkSegment'
import { isOverNodeInput, isOverNodeOutput } from './canvas/measureSlots'
import { strokeShape } from './draw'
import type {
Expand Down Expand Up @@ -5522,7 +5523,6 @@ export class LGraphCanvas
const endPos = node.getInputPos(link.target_slot)
const endDirection = node.inputs[link.target_slot]?.dir

firstReroute._dragging = true
this.#renderAllLinkSegments(
ctx,
link,
Expand All @@ -5542,7 +5542,6 @@ export class LGraphCanvas
const endPos = reroute.pos
const startDirection = node.outputs[link.origin_slot]?.dir

link._dragging = true
this.#renderAllLinkSegments(
ctx,
link,
Expand Down Expand Up @@ -5602,6 +5601,10 @@ export class LGraphCanvas

// Has reroutes
if (reroutes.length) {
const lastReroute = reroutes[reroutes.length - 1]
const floatingType = lastReroute?.floating?.slotType
const skipFirstSegment = floatingType === 'input'
const skipLastSegment = floatingType === 'output'
let startControl: Point | undefined

const l = reroutes.length
Expand All @@ -5610,7 +5613,6 @@ export class LGraphCanvas

// Only render once
if (!renderedPaths.has(reroute)) {
renderedPaths.add(reroute)
visibleReroutes.push(reroute)
reroute._colour =
link.color ||
Expand All @@ -5619,10 +5621,16 @@ export class LGraphCanvas

const prevReroute = graph.getReroute(reroute.parentId)
const rerouteStartPos = prevReroute?.pos ?? startPos
reroute.calculateAngle(this.last_draw_time, graph, rerouteStartPos)
const params = reroute.computeRenderParams(graph, rerouteStartPos)

// Skip the first segment if it is being dragged
if (!reroute._dragging) {
if (!(skipFirstSegment && j === 0)) {
const rendered = new RenderedLinkSegment({
id: reroute.id,
origin_id: link.origin_id,
origin_slot: link.origin_slot,
parentId: reroute.parentId
})
this.renderLink(
ctx,
rerouteStartPos,
Expand All @@ -5635,35 +5643,45 @@ export class LGraphCanvas
LinkDirection.CENTER,
{
startControl,
endControl: reroute.controlPoint,
reroute,
disabled
endControl: params.controlPoint,
disabled,
renderTarget: rendered
}
)
renderedPaths.add(rendered)
}
}

if (!startControl && reroutes.at(-1)?.floating?.slotType === 'input') {
if (!startControl && skipFirstSegment) {
// Floating link connected to an input
startControl = [0, 0]
} else {
// Calculate start control for the next iter control point
const nextPos = reroutes[j + 1]?.pos ?? endPos
const prevR = graph.getReroute(reroute.parentId)
const startPosForParams = prevR?.pos ?? startPos
const params = reroute.computeRenderParams(graph, startPosForParams)
const dist = Math.min(
Reroute.maxSplineOffset,
distance(reroute.pos, nextPos) * 0.25
)
startControl = [dist * reroute.cos, dist * reroute.sin]
startControl = [dist * params.cos, dist * params.sin]
}
}

// Skip the last segment if it is being dragged
if (link._dragging) return
// For floating links from output, skip the last segment
if (skipLastSegment) return

// Use runtime fallback; TypeScript cannot evaluate this correctly.
const segmentStartPos = points.at(-2) ?? startPos

// Render final link segment
const rendered = new RenderedLinkSegment({
id: link.id,
origin_id: link.origin_id,
origin_slot: link.origin_slot,
parentId: link.parentId
})
this.renderLink(
ctx,
segmentStartPos,
Expand All @@ -5674,10 +5692,17 @@ export class LGraphCanvas
null,
LinkDirection.CENTER,
end_dir,
{ startControl, disabled }
{ startControl, disabled, renderTarget: rendered }
)
renderedPaths.add(rendered)
// Skip normal render when link is being dragged
} else if (!link._dragging) {
const rendered = new RenderedLinkSegment({
id: link.id,
origin_id: link.origin_id,
origin_slot: link.origin_slot,
parentId: link.parentId
})
this.renderLink(
ctx,
startPos,
Expand All @@ -5687,10 +5712,11 @@ export class LGraphCanvas
0,
null,
start_dir,
end_dir
end_dir,
{ renderTarget: rendered }
)
renderedPaths.add(rendered)
}
renderedPaths.add(link)

// event triggered rendered on top
if (link?._last_time && now - link._last_time < 1000) {
Expand Down Expand Up @@ -5739,7 +5765,8 @@ export class LGraphCanvas
endControl,
reroute,
num_sublines = 1,
disabled = false
disabled = false,
renderTarget
}: {
/** When defined, render data will be saved to this reroute instead of the {@link link}. */
reroute?: Reroute
Expand All @@ -5751,6 +5778,8 @@ export class LGraphCanvas
num_sublines?: number
/** Whether this is a floating link segment */
disabled?: boolean
/** Where to store the drawn path for hit testing if not using a reroute */
renderTarget?: RenderedLinkSegment
} = {}
): void {
const linkColour =
Expand Down Expand Up @@ -5781,13 +5810,13 @@ export class LGraphCanvas
const path = new Path2D()

/** The link or reroute we're currently rendering */
const linkSegment = reroute ?? link
const linkSegment = reroute ?? renderTarget
if (linkSegment) linkSegment.path = path

const innerA = LGraphCanvas.#lTempA
const innerB = LGraphCanvas.#lTempB

/** Reference to {@link reroute._pos} if present, or {@link link._pos} if present. Caches the centre point of the link. */
/** Reference to render-time centre point of this segment. */
const pos: Point = linkSegment?._pos ?? [0, 0]

for (let i = 0; i < num_sublines; i++) {
Expand Down
60 changes: 60 additions & 0 deletions src/lib/litegraph/src/Reroute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,66 @@ export class Reroute
}
}

/**
* Computes render-time parameters for this reroute without mutating the model.
* Returns the bezier end control-point offset and the direction cos/sin.
*/
computeRenderParams(
network: ReadonlyLinkNetwork,
linkStart: Point
): { cos: number; sin: number; controlPoint: Point } {
const thisPos = this.#pos
const angles: number[] = []
let sum = 0

// Collect angles of all links passing through this reroute
addAngles(this.linkIds, network.links)
addAngles(this.floatingLinkIds, network.floatingLinks)

// Default values when invalid
if (!angles.length) {
return { cos: 0, sin: 0, controlPoint: [0, 0] }
}

sum /= angles.length

const originToReroute = Math.atan2(
thisPos[1] - linkStart[1],
thisPos[0] - linkStart[0]
)
let diff = (originToReroute - sum) * 0.5
if (Math.abs(diff) > Math.PI * 0.5) diff += Math.PI
const dist = Math.min(
Reroute.maxSplineOffset,
distance(linkStart, thisPos) * 0.25
)

const originDiff = originToReroute - diff
const cos = Math.cos(originDiff)
const sin = Math.sin(originDiff)
const controlPoint: Point = [dist * -cos, dist * -sin]

return { cos, sin, controlPoint }

function addAngles(
linkIds: Iterable<LinkId>,
links: ReadonlyMap<LinkId, LLink>
) {
for (const linkId of linkIds) {
const link = links.get(linkId)
const pos = getNextPos(network, link, thisObj.id)
if (!pos) continue
const angle = getDirection(thisPos, pos)
angles.push(angle)
sum += angle
}
}

// Preserve lexical `this` values inside helper
// eslint-disable-next-line @typescript-eslint/no-this-alias
const thisObj = this
}

/**
* Renders the reroute on the canvas.
* @param ctx Canvas context to draw on
Expand Down
32 changes: 32 additions & 0 deletions src/lib/litegraph/src/canvas/RenderedLinkSegment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LinkId } from '@/lib/litegraph/src/LLink'
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
import type { LinkSegment } from '@/lib/litegraph/src/interfaces'

/**
* Lightweight, render-only representation of a link segment used for hit testing and tooltips.
* Decouples canvas state from the LLink data model.
*/
export class RenderedLinkSegment implements LinkSegment {
readonly id: LinkId | RerouteId
readonly origin_id: NodeId
readonly origin_slot: number
readonly parentId?: RerouteId

path?: Path2D
readonly _pos: Float32Array = new Float32Array(2)
_centreAngle?: number
_dragging?: boolean

constructor(args: {
id: LinkId | RerouteId
origin_id: NodeId
origin_slot: number
parentId?: RerouteId
}) {
this.id = args.id
this.origin_id = args.origin_id
this.origin_slot = args.origin_slot
this.parentId = args.parentId
}
}