(store: CanvasStore, nodeDef: NodeDefinition, position?: { x: number; y: number })
| 101 | |
| 102 | /** Add a node to the store given a node definition and optional position. Returns the new node ID, or null if the node cannot be added. */ |
| 103 | export function addNodeToStore(store: CanvasStore, nodeDef: NodeDefinition, position?: { x: number; y: number }): string | null { |
| 104 | if (!canAddNode(store, nodeDef)) return null; |
| 105 | |
| 106 | const nodeId = generateId(); |
| 107 | |
| 108 | // Initialize parameters with default values. Clone so object/array defaults |
| 109 | // (Expression, weekdays `[]`, memory-refs `[]`) aren't shared by reference |
| 110 | // across instances or with the definition itself — a shared mutable default |
| 111 | // would alias on edit. |
| 112 | const args: Record<string, unknown> = {}; |
| 113 | nodeDef.parameters.forEach((param) => { |
| 114 | if (param.default !== undefined) { |
| 115 | args[param.id] = structuredClone(param.default); |
| 116 | } |
| 117 | }); |
| 118 | |
| 119 | // Seed outputs directly into args: |
| 120 | // - Static outputs: `args[out.id] = { active: true, mode: "emit", name: out.id }` (OutputBinding) |
| 121 | // - List outputs: `args[out.id] = []` (empty OutputDeclaration[] — user adds entries) |
| 122 | for (const out of nodeDef.outputs ?? []) { |
| 123 | if (out.type === "static") { |
| 124 | args[out.id] = { active: true, mode: "emit", name: out.id }; |
| 125 | } else { |
| 126 | args[out.id] = []; |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | const nodeData: NodeBase = { |
| 131 | id: nodeId, |
| 132 | type: nodeDef.type, |
| 133 | arguments: args, |
| 134 | }; |
| 135 | if (nodeDef.type === "FunctionCall") { |
| 136 | const functionDef = nodeDef as FunctionNodeDefinition; |
| 137 | const functionNode = nodeData as FunctionCallNode; |
| 138 | functionNode.functionInfo = functionDef.functionInfo; |
| 139 | // Seed input bindings keyed by arg uid — empty expressions with the declared dataType. |
| 140 | // Output bindings were already seeded above by the generic outputs[] loop since |
| 141 | // buildFunctionNodeDef now emits real StaticOutput entries. |
| 142 | for (const arg of functionDef.functionInfo.arguments) { |
| 143 | args[paramKey(arg)] = { expression: "", references: [], dataType: arg.dataType }; |
| 144 | } |
| 145 | // Generic seeding uses the output id as the default emit name, but for function |
| 146 | // returns we want the return's variable name. |
| 147 | for (const ret of functionDef.functionInfo.returns) { |
| 148 | args[paramKey(ret)] = { active: true, mode: "emit" as const, name: ret.name }; |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | // Deduplicate emit binding names against existing variables on the canvas |
| 153 | const existingNames = collectVariableNames(store); |
| 154 | deduplicateEmitNames(nodeData as NodeData, existingNames); |
| 155 | |
| 156 | // Nudge position if another node is already nearby (avoids stacking on click-to-add) |
| 157 | const currentNodes = store.getState().nodes; |
| 158 | let pos = position || { x: 250, y: 100 }; |
| 159 | const SNAP_DISTANCE = 50; |
| 160 | while (currentNodes.some((n) => Math.abs(n.position.x - pos.x) < SNAP_DISTANCE && Math.abs(n.position.y - pos.y) < SNAP_DISTANCE)) { |
no test coverage detected