diff --git a/src/LLink.ts b/src/LLink.ts index 7700f28e0..eee314944 100644 --- a/src/LLink.ts +++ b/src/LLink.ts @@ -59,6 +59,18 @@ export class LLink implements LinkSegment, Serialisable { this.#color = value === "" ? null : value } + public get isFloatingOutput(): boolean { + return this.origin_id === -1 && this.origin_slot === -1 + } + + public get isFloatingInput(): boolean { + return this.target_id === -1 && this.target_slot === -1 + } + + public get isFloating(): boolean { + return this.isFloatingOutput || this.isFloatingInput + } + constructor( id: LinkId, type: ISlotType, @@ -183,6 +195,28 @@ export class LLink implements LinkSegment, Serialisable { return this.target_id === nodeId && this.target_slot === inputIndex } + /** + * Creates a floating link from this link. + * @param slotType The side of the link that is still connected + * @param parentId The parent reroute ID of the link + * @returns A new LLink that is floating + */ + toFloating(slotType: "input" | "output", parentId: RerouteId): LLink { + const exported = this.asSerialisable() + exported.id = -1 + exported.parentId = parentId + + if (slotType === "input") { + exported.origin_id = -1 + exported.origin_slot = -1 + } else { + exported.target_id = -1 + exported.target_slot = -1 + } + + return LLink.create(exported) + } + /** * Disconnects a link and removes it from the graph, cleaning up any reroutes that are no longer used * @param network The container (LGraph) where reroutes should be updated diff --git a/src/canvas/ToInputRenderLink.ts b/src/canvas/ToInputRenderLink.ts index 78a5363dd..085976464 100644 --- a/src/canvas/ToInputRenderLink.ts +++ b/src/canvas/ToInputRenderLink.ts @@ -65,8 +65,6 @@ export class ToInputRenderLink implements RenderLink { // Set the parentId of the reroute we dropped on, to the reroute we dragged from reroute.parentId = fromReroute?.id - // Keep reroutes when disconnecting the original link - existingLink.disconnect(this.network, "output") const newLink = outputNode.connectSlots(fromSlot, inputNode, input, existingLink.parentId) // Connecting from the final reroute of a floating reroute chain @@ -77,7 +75,17 @@ export class ToInputRenderLink implements RenderLink { if (reroute.id === fromReroute?.id) break reroute.removeLink(existingLink) - if (reroute.totalLinks === 0) reroute.remove() + if (reroute.totalLinks === 0) { + if (existingLink.isFloating) { + // Cannot float from both sides - remove + reroute.remove() + } else { + // Convert to floating + const cl = existingLink.toFloating("output", reroute.id) + this.network.addFloatingLink(cl) + reroute.floating = { slotType: "output" } + } + } } events.dispatch("link-created", newLink) } diff --git a/test/LinkConnector.integration.test.ts b/test/LinkConnector.integration.test.ts index 594a6ad0e..83d8c3ade 100644 --- a/test/LinkConnector.integration.test.ts +++ b/test/LinkConnector.integration.test.ts @@ -734,7 +734,7 @@ describe("LinkConnector Integration", () => { for (const [index, parentId] of parentIds.entries()) { const reroute = graph.reroutes.get(parentId)! if (linksAfter[index] === undefined) { - expect(reroute).toBeUndefined() + expect(reroute).not.toBeUndefined() } else { expect(reroute.linkIds.size).toBe(linksAfter[index]) } @@ -852,10 +852,15 @@ describe("LinkConnector Integration", () => { expect([...toReroute.linkIds.values()]).toEqual(nextLinkIds) - // Parent reroutes should have lost the links or been removed for (const rerouteId of shouldBeRemoved) { const reroute = graph.reroutes.get(rerouteId)! - expect(reroute).toBeUndefined() + if (testFloatingInputs) { + // Already-floating reroutes should be removed + expect(reroute).toBeUndefined() + } else { + // Non-floating reroutes should still exist + expect(reroute).not.toBeUndefined() + } } for (const rerouteId of shouldHaveLinkIdsRemoved) {