import type { ObsidianGraphModel } from './obsidian-graph'; export const FILTERED_GRAPH_SELECTION_ID = '__filtered-graph-selection__'; function oppositeNode(edge: { source: string; target: string }, nodeId: string): string | undefined { if (edge.source === nodeId) return edge.target; if (edge.target === nodeId) return edge.source; return undefined; } /** The visible path of one selected node ends at its direct connections. */ export function selectedPathNodeIds(graph: ObsidianGraphModel, nodeId: string): string[] { if (!graph.nodes.some(node => node.id === nodeId)) return []; const selected = new Set([nodeId]); graph.edges.forEach(edge => { const neighbor = oppositeNode(edge, nodeId); if (neighbor) selected.add(neighbor); }); return graph.nodes.map(node => node.id).filter(id => selected.has(id)); } export function relationBranchNodeIds(graph: ObsidianGraphModel, rootId: string): string[] { const kindById = new Map(graph.nodes.map(node => [node.id, node.kind])); const rootKind = kindById.get(rootId); if (!rootKind || rootKind === 'finding') return []; const selected = new Set([rootId]); const findingAnchors = rootKind === 'tactic' ? [rootId, ...graph.edges.flatMap(edge => { const candidate = oppositeNode(edge, rootId); return candidate && kindById.get(candidate) === 'technique' ? [candidate] : []; })] : [rootId]; findingAnchors.forEach(id => selected.add(id)); const findingIds = graph.edges.flatMap(edge => findingAnchors.flatMap(anchorId => { const candidate = oppositeNode(edge, anchorId); return candidate && kindById.get(candidate) === 'finding' ? [candidate] : []; })); findingIds.forEach(id => selected.add(id)); // Alle direkten Beziehungen der zur Wurzel gehoerenden Findings werden // aufgenommen, auch weitere Endpunkte derselben Art wie die Wurzel. Das ist // insbesondere fuer Findings mit mehreren Kategorien oder D3FEND-Zuordnungen // wichtig. Ein gemeinsam genutzter Relationsknoten darf die Auswahl trotzdem // nicht rekursiv in weitere Findings ausdehnen. graph.edges.forEach(edge => { const sourceFinding = findingIds.includes(edge.source); const targetFinding = findingIds.includes(edge.target); if (!sourceFinding && !targetFinding) return; const candidate = sourceFinding ? edge.target : edge.source; selected.add(candidate); }); // Techniques besitzen eine zusaetzliche, fachlich gewollte Ebene zu ihren // Tactics. Dort endet die Traversierung bewusst. const techniqueIds = [...selected].filter(id => kindById.get(id) === 'technique'); graph.edges.forEach(edge => { const techniqueId = techniqueIds.includes(edge.source) ? edge.source : techniqueIds.includes(edge.target) ? edge.target : undefined; if (!techniqueId) return; const candidate = oppositeNode(edge, techniqueId); if (candidate && kindById.get(candidate) === 'tactic' && (rootKind !== 'tactic' || candidate === rootId)) selected.add(candidate); }); return graph.nodes.map(node => node.id).filter(id => selected.has(id)); } export function graphSelectionNodeIds(graph: ObsidianGraphModel, selectionId: string): string[] { return selectionId === FILTERED_GRAPH_SELECTION_ID ? graph.nodes.map(node => node.id) : relationBranchNodeIds(graph, selectionId); } export function graphSelectionUnionNodeIds(graph: ObsidianGraphModel, selectionIds: string[]): string[] { const selected = new Set(); for (const selectionId of selectionIds) { if (selectionId === FILTERED_GRAPH_SELECTION_ID) return graph.nodes.map(node => node.id); const branchNodeIds = relationBranchNodeIds(graph, selectionId); if (branchNodeIds.length) { branchNodeIds.forEach(nodeId => selected.add(nodeId)); continue; } if (!graph.nodes.some(node => node.id === selectionId)) continue; selected.add(selectionId); graph.edges.forEach(edge => { if (edge.source === selectionId) selected.add(edge.target); if (edge.target === selectionId) selected.add(edge.source); }); } return graph.nodes.map(node => node.id).filter(nodeId => selected.has(nodeId)); }