import React, {memo, useMemo} from "react"; import {EdgeProps, getSmoothStepPath, SmoothStepEdgeProps} from "@xyflow/react"; import {Color} from "./tree/Color"; import {colors, getColorForComponentType} from "./tree/Colors"; import {useNodesStore} from "./tree/store"; import {StateBranchNode} from "./tree/StateBranchNode"; import {TestableCompositeNode} from "./tree/TestableCompositeNode"; import {TreeNode} from "./tree/TreeNode"; import {getConditionPaletteFromNodeData, getTierBaseColor} from "./tree/tierColorTheme"; export type ConnectingLineProps = SmoothStepEdgeProps & EdgeProps function resolveComponentType(mode: string): 'condition' | 'filter' | 'offer' | string { return mode === 'test' ? 'condition' : mode } function getTestableNodeColor(node: TestableCompositeNode): Color { const testable = node.getTestable() const testableData = testable.getData() const parentData = testable.getParent()?.getData() const testableType = ( testableData && typeof testableData === 'object' && 'testableType' in testableData && typeof testableData.testableType === 'string' ) ? testableData.testableType : undefined const conditionPalette = getConditionPaletteFromNodeData(testableData) || getConditionPaletteFromNodeData(parentData) const isTierTestableNode = node.getTreeNode().isTier() || node.isBaseTestableForTier() if (isTierTestableNode) { return getTierBaseColor({ place: 'tier-edge', mode: testable.getMode(), testableType, inheritedConditionPalette: conditionPalette, }) } return getColorForComponentType( resolveComponentType(testable.getMode()), {conditionPalette} ) } function resolveColor(getGroveNode: (id: string) => unknown, nodeId: string): Color { const node = getGroveNode(nodeId); const getBaseTestableColor = (branch: StateBranchNode): Color => { const baseTestableId = branch.getExtraData<{baseTestableId?: string}>()?.baseTestableId if (!baseTestableId) { return colors.gray } const baseTestable = getGroveNode(baseTestableId) if (baseTestable instanceof TestableCompositeNode) { return getTestableNodeColor(baseTestable) } return colors.gray } if (node instanceof TestableCompositeNode) { return getTestableNodeColor(node) } else if (node instanceof StateBranchNode) { if (node.isSimple()) { return colors.blue; } else if (node.isTiered()) { return getBaseTestableColor(node); } return colors.gray; } else if (node instanceof TreeNode) { if (node.hasParent()) { const parentBranch = node.getParentBranch() return parentBranch ? getBaseTestableColor(parentBranch) : colors.gray } return colors.gray; } if (nodeId.endsWith('.add-tier-button')) { const tieredBranch = getGroveNode(nodeId.replace(/\.add-tier-button$/, '')) if (tieredBranch instanceof StateBranchNode && tieredBranch.isTiered()) { return getBaseTestableColor(tieredBranch) } } return colors.gray; } function getEdgeColorSource(color: Color, preferredIntensity: number, fallbackIntensity: number = 100): string { const preferredColor = color.get(preferredIntensity) as { source?: string } | undefined if (preferredColor?.source) { return preferredColor.source } const fallbackColor = color.get(fallbackIntensity) as { source?: string } | undefined if (fallbackColor?.source) { return fallbackColor.source } const defaultColor = color.get(100) as { source?: string } | undefined return defaultColor?.source || '#9ca3af' } // Single selector extracts both colors in one subscription to avoid redundant re-renders. // Returns stable string tuple so useShallow isn't needed. function useEdgeColors(sourceID: string, targetID: string): [string, string] { return useNodesStore( (state) => { const getGroveNode = state.getGroveNode; const srcColor = resolveColor(getGroveNode, sourceID); const tgtColor = resolveColor(getGroveNode, targetID); return [ getEdgeColorSource(srcColor, 80), getEdgeColorSource(tgtColor, 100) ] as [string, string]; }, // Custom equality: only re-render when the actual color strings change (a, b) => a[0] === b[0] && a[1] === b[1] ); } export const ConnectingLine = memo(({id, source: sourceID, target: targetID, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition}) => { const [sourceColorStr, targetColorStr] = useEdgeColors(sourceID, targetID); const gradientId = `edge-gradient-${id.replace(/[^a-zA-Z0-9_-]/g, '_')}`; const [edgePath] = useMemo(() => getSmoothStepPath({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, borderRadius: 30 }), [sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition]); return <> })