diff --git a/packages/xod-project/src/flatten.js b/packages/xod-project/src/flatten.js index b894ddc4..60f489d4 100644 --- a/packages/xod-project/src/flatten.js +++ b/packages/xod-project/src/flatten.js @@ -645,7 +645,7 @@ const isLinkOutputNodeIdOneOf = R.curry((terminalNodeIds, link) => // :: [NodeId] -> [Link] -> [[NodeId], [Link]] const traverseUpAndCollectTerminalChain = R.curry( (terminalNodeIds, links, collected) => { - const curLink = R.compose(R.last, R.last)(collected); + const curLink = R.last(collected[1]); if (isLinkOutputNodeIdOneOf(terminalNodeIds, curLink)) { // collect nodeId + next link & run recursion @@ -654,8 +654,10 @@ const traverseUpAndCollectTerminalChain = R.curry( R.compose(R.equals(nextNodeId), Link.getLinkInputNodeId), links ); - // If the uppermost node has no input link — return collected data - if (!nextLink) return collected; + // If the upper terminal node has no input link — return tuple of empty lists + // to avoid removing this chain of terminals, because it will be done in the + // further step with passing bound values + if (!nextLink) return [[], []]; return R.compose( traverseUpAndCollectTerminalChain(terminalNodeIds, links), @@ -683,16 +685,26 @@ const replaceGenericTerminalsWithCastNodes = R.curry( const linksConnectedToTerminals = R.filter( R.compose( - linkNodeIds => R.any(R.contains(R.__, linkNodeIds), terminalNodeIds), + linkNodeIds => R.any(isAmong(linkNodeIds), terminalNodeIds), Link.getLinkNodeIds ), links ); const lowermostTerminalLinks = R.filter( - R.compose( - R.complement(R.contains)(R.__, terminalNodeIds), - Link.getLinkInputNodeId + R.either( + // link inputs into non terminal node + R.compose( + R.complement(isAmong(terminalNodeIds)), + Link.getLinkInputNodeId + ), + // or into terminal node, which not have a links below + R.compose( + R.not, + R.find(R.__, links), + Link.isLinkOutputNodeIdEquals, + Link.getLinkInputNodeId + ) ), linksConnectedToTerminals ); @@ -742,14 +754,13 @@ const replaceGenericTerminalsWithCastNodes = R.curry( )(terminalChains); const nodesWithoutTerminals = R.reject( - R.compose(R.contains(R.__, nodeIdsToOmit), Node.getNodeId), + R.compose(isAmong(nodeIdsToOmit), Node.getNodeId), nodes ); const linksWithoutTerminalLinks = R.reject( - R.compose(R.contains(R.__, linkIdsToOmit), Link.getLinkId), + R.compose(isAmong(linkIdsToOmit), Link.getLinkId), links ); - const newNodesAndLinks = R.map( splitLinkWithCastNode(project, patchTuples, nodesWithoutTerminals), directLinks diff --git a/packages/xod-project/test/fixtures/cast-custom-types.xodball b/packages/xod-project/test/fixtures/cast-custom-types.xodball index 5a6ead60..2bffd9ec 100644 --- a/packages/xod-project/test/fixtures/cast-custom-types.xodball +++ b/packages/xod-project/test/fixtures/cast-custom-types.xodball @@ -665,6 +665,261 @@ } }, "path": "xod/i2c/test" + }, + "@/test-unlinked-lowermost-terminal": { + "links": { + "original1": { + "id": "original1", + "input": { + "nodeId": "testNode", + "pinKey": "input-color" + }, + "output": { + "nodeId": "colorHslNode", + "pinKey": "output-color" + } + }, + "toWatch": { + "id": "toWatch", + "input": { + "nodeId": "watch-node", + "pinKey": "input-string" + }, + "output": { + "nodeId": "testNode", + "pinKey": "output-string" + } + } + }, + "nodes": { + "testNode": { + "id": "testNode", + "position": { + "x": -8, + "y": -1, + "units": "slots" + }, + "type": "@/unlinked-custom-terminal" + }, + "colorHslNode": { + "id": "colorHslNode", + "position": { + "x": -8, + "y": -2, + "units": "slots" + }, + "type": "xod/color/color-hsl" + }, + "watch-node": { + "id": "watch-node", + "position": { + "x": -8, + "y": 1, + "units": "slots" + }, + "type": "xod/debug/watch" + } + }, + "path": "@/test-unlinked-lowermost-terminal" + }, + "@/unlinked-custom-terminal": { + "links": { + "toFormatColor": { + "id": "toFormatColor", + "input": { + "nodeId": "formatColorNode", + "pinKey": "input-color" + }, + "output": { + "nodeId": "input-color", + "pinKey": "__out__" + } + }, + "toOutputString": { + "id": "toOutputString", + "input": { + "nodeId": "output-string", + "pinKey": "__in__" + }, + "output": { + "nodeId": "formatColorNode", + "pinKey": "output-string" + } + }, + "toOutputColor": { + "id": "toOutputColor", + "input": { + "nodeId": "output-color", + "pinKey": "__in__" + }, + "output": { + "nodeId": "input-color", + "pinKey": "__out__" + } + } + }, + "nodes": { + "output-color": { + "id": "output-color", + "position": { + "x": 0, + "y": 2, + "units": "slots" + }, + "type": "xod/color/output-color" + }, + "formatColorNode": { + "id": "formatColorNode", + "position": { + "x": 2, + "y": 1, + "units": "slots" + }, + "type": "xod/color/format-color" + }, + "input-color": { + "id": "input-color", + "position": { + "x": 0, + "y": 0, + "units": "slots" + }, + "type": "xod/color/input-color" + }, + "output-string": { + "id": "output-string", + "position": { + "x": 2, + "y": 2, + "units": "slots" + }, + "type": "xod/patch-nodes/output-string" + } + }, + "path": "@/unlinked-custom-terminal" + }, + "@/test-nested-input-color": { + "nodes": { + "nested-format-color": { + "boundLiterals": { + "input-color-1": "#0000FF", + "input-color-2": "#FF0000" + }, + "id": "nested-format-color", + "position": { + "x": -5, + "y": -1, + "units": "slots" + }, + "type": "@/nested-format-color" + } + }, + "path": "@/test-nested-input-color" + }, + "@/nested-format-color": { + "links": { + "ByAbAISQv": { + "id": "ByAbAISQv", + "input": { + "nodeId": "format-color-1", + "pinKey": "input-color" + }, + "output": { + "nodeId": "input-color-1", + "pinKey": "__out__" + } + }, + "H1MG0IBQv": { + "id": "H1MG0IBQv", + "input": { + "nodeId": "output-string-1", + "pinKey": "__in__" + }, + "output": { + "nodeId": "format-color-2", + "pinKey": "output-string" + } + }, + "SJLMAIH7w": { + "id": "SJLMAIH7w", + "input": { + "nodeId": "output-string-2", + "pinKey": "__in__" + }, + "output": { + "nodeId": "format-color-1", + "pinKey": "output-string" + } + }, + "SJqbR8BQw": { + "id": "SJqbR8BQw", + "input": { + "nodeId": "format-color-2", + "pinKey": "input-color" + }, + "output": { + "nodeId": "input-color-2", + "pinKey": "__out__" + } + } + }, + "nodes": { + "input-color-1": { + "id": "input-color-1", + "position": { + "x": 3, + "y": 0, + "units": "slots" + }, + "type": "xod/color/input-color" + }, + "format-color-1": { + "id": "format-color-1", + "position": { + "x": 3, + "y": 1, + "units": "slots" + }, + "type": "xod/color/format-color" + }, + "output-string-1": { + "id": "output-string-1", + "position": { + "x": 1, + "y": 2, + "units": "slots" + }, + "type": "xod/patch-nodes/output-string" + }, + "input-color-2": { + "id": "input-color-2", + "position": { + "x": 1, + "y": 0, + "units": "slots" + }, + "type": "xod/color/input-color" + }, + "output-string-2": { + "id": "output-string-2", + "position": { + "x": 3, + "y": 2, + "units": "slots" + }, + "type": "xod/patch-nodes/output-string" + }, + "format-color-2": { + "id": "format-color-2", + "position": { + "x": 1, + "y": 1, + "units": "slots" + }, + "type": "xod/color/format-color" + } + }, + "path": "@/nested-format-color" } } } diff --git a/packages/xod-project/test/flatten.spec.js b/packages/xod-project/test/flatten.spec.js index e5714911..7ccb8365 100644 --- a/packages/xod-project/test/flatten.spec.js +++ b/packages/xod-project/test/flatten.spec.js @@ -7,6 +7,7 @@ import * as Helper from './helpers'; import * as Project from '../src/project'; import * as Patch from '../src/patch'; import * as Node from '../src/node'; +import * as Link from '../src/link'; import * as Attachment from '../src/attachment'; import * as CONST from '../src/constants'; import flatten, { extractPatches, extractLeafPatches } from '../src/flatten'; @@ -1378,6 +1379,60 @@ describe('Flatten', () => { }, flatProject); }); + it('should get rid of output terminals without output links', () => { + const flatProject = flatten( + project, + '@/test-unlinked-lowermost-terminal' + ); + Helper.expectEitherRight(newProject => { + const nodeIds = R.compose( + R.map(Node.getNodeId), + Patch.listNodes, + Project.getPatchByPathUnsafe('@/test-unlinked-lowermost-terminal') + )(newProject); + + const linkIds = R.compose( + R.map(Link.getLinkId), + Patch.listLinks, + Project.getPatchByPathUnsafe('@/test-unlinked-lowermost-terminal') + )(newProject); + + assert.sameMembers( + ['testNode~formatColorNode', 'colorHslNode', 'watch-node'], + nodeIds + ); + assert.sameMembers( + ['toWatch', 'original1-to-testNode~toFormatColor'], + linkIds + ); + }, flatProject); + }); + + it('should pass bound values to nested castable custom type terminals', () => { + const flatProject = flatten(project, '@/test-nested-input-color'); + Helper.expectEitherRight(newProject => { + const nodeIds = R.compose( + R.map(Node.getNodeId), + Patch.listNodes, + Project.getPatchByPathUnsafe('@/test-nested-input-color') + )(newProject); + + const links = R.compose( + Patch.listLinks, + Project.getPatchByPathUnsafe('@/test-nested-input-color') + )(newProject); + + assert.sameMembers( + [ + 'nested-format-color~format-color-1', + 'nested-format-color~format-color-2', + ], + nodeIds + ); + assert.isEmpty(links); + }, flatProject); + }); + it('should return Either.Left if custom type does not have a cast node', () => { const flatProject = flatten(project, '@/test-no-cast-node'); Helper.expectEitherError(