Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/ir/ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ export interface LocationMapEntry {
nodeId: string;
}

export interface AnimationPath {
id: string;
nodes: string[];
edges: string[];
description: string;
}

export interface FlowchartIR {
nodes: FlowchartNode[];
edges: FlowchartEdge[];
Expand All @@ -84,4 +91,5 @@ export interface FlowchartIR {
rating: "low" | "medium" | "high" | "very-high";
description: string;
};
animationPaths?: AnimationPath[];
}
21 changes: 21 additions & 0 deletions src/logic/EnhancedMermaidGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { FlowchartIR, FlowchartNode, FlowchartEdge, NodeType } from "../ir/ir";
import { StringProcessor } from "./utils/StringProcessor";
import { SubtleThemeManager, ThemeStyles } from "./utils/ThemeManager";
import { getComplexityConfig } from "./utils/ComplexityConfig";
import { AnimationPathGenerator } from "./utils/AnimationPathGenerator";

// Optimized string building
class StringBuilder {
Expand Down Expand Up @@ -89,6 +90,26 @@ export class EnhancedMermaidGenerator {
this.sb.appendLine(`%% ${ir.functionComplexity.description}`);
}

// Generate animation paths if not already present
if (!ir.animationPaths) {
try {
ir.animationPaths = AnimationPathGenerator.generatePaths(ir);
} catch (error) {
console.warn('Failed to generate animation paths:', error);
ir.animationPaths = [];
}
}

// Add animation path metadata as comments
if (ir.animationPaths && ir.animationPaths.length > 0) {
this.sb.appendLine("");
this.sb.appendLine(" %% Animation paths metadata");
for (const path of ir.animationPaths) {
this.sb.appendLine(` %% ANIM_PATH:${path.id}:${path.nodes.join(',')}`);
this.sb.appendLine(` %% ANIM_DESC:${path.id}:${path.description}`);
}
}

// Apply sanitization to all IDs first to ensure consistency
for (const node of ir.nodes) {
node.id = this.sanitizeId(node.id);
Expand Down
299 changes: 299 additions & 0 deletions src/logic/utils/AnimationPathGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
import { FlowchartIR, FlowchartNode, FlowchartEdge, NodeType } from "../../ir/ir";

export interface AnimationPath {
id: string;
nodes: string[];
edges: string[];
description: string;
}
Comment on lines +3 to +8
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The AnimationPath interface is duplicated here. It has also been added to src/ir/ir.ts in this pull request. To maintain a single source of truth and avoid potential inconsistencies, please remove this local definition and import it from ../../ir/ir instead. You will need to update the import statement on line 1 accordingly.


export interface AnimationPathStep {
nodeId: string;
edgeId?: string;
isLoopIteration?: boolean;
}

export class AnimationPathGenerator {
private static readonly MAX_LOOP_ITERATIONS = 2;
private static readonly MAX_PATH_LENGTH = 50; // Prevent infinite paths

/**
* Generate all possible execution paths through the flowchart
*/
public static generatePaths(ir: FlowchartIR): AnimationPath[] {
if (!ir.entryNodeId || ir.nodes.length === 0) {
console.log('AnimationPathGenerator: No entry node or empty nodes');
return [];
}

try {
const paths: AnimationPath[] = [];
const visited = new Set<string>();
const pathSteps: AnimationPathStep[] = [];
const nodeMap = new Map<string, FlowchartNode>();
const edgeMap = new Map<string, FlowchartEdge[]>();

// Build lookup maps
for (const node of ir.nodes) {
nodeMap.set(node.id, node);
}

for (const edge of ir.edges) {
if (!edgeMap.has(edge.from)) {
edgeMap.set(edge.from, []);
}
edgeMap.get(edge.from)!.push(edge);
}

console.log(`AnimationPathGenerator: Starting path generation from ${ir.entryNodeId}`);

// Generate paths starting from entry node
this.generatePathsFromNode(
ir.entryNodeId,
nodeMap,
edgeMap,
paths,
visited,
pathSteps,
0
);

console.log(`AnimationPathGenerator: Generated ${paths.length} paths`);
return paths.length > 0 ? paths : this.generateFallbackPath(ir);
} catch (error) {
console.error('AnimationPathGenerator: Error generating paths:', error);
return this.generateFallbackPath(ir);
}
}

private static generatePathsFromNode(
nodeId: string,
nodeMap: Map<string, FlowchartNode>,
edgeMap: Map<string, FlowchartEdge[]>,
paths: AnimationPath[],
visited: Set<string>,
pathSteps: AnimationPathStep[],
depth: number
): void {
// Prevent infinite recursion
if (depth > this.MAX_PATH_LENGTH) {
console.log(`AnimationPathGenerator: Max depth reached for node ${nodeId}`);
return;
}

// Prevent cycles
if (visited.has(nodeId)) {
console.log(`AnimationPathGenerator: Cycle detected at node ${nodeId}`);
return;
}
Comment on lines +84 to +88
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The current cycle detection logic prevents the correct traversal of loops. The visited.has(nodeId) check will cause the path generation to terminate immediately upon re-entering a node that is part of a loop, meaning loops will not be animated correctly. The logic needs to be adjusted to allow visiting a node multiple times within a path, up to a certain limit, to correctly represent loops. One approach could be to pass a map of node visit counts instead of a Set and check against a maximum visit count.


const node = nodeMap.get(nodeId);
if (!node) {
console.log(`AnimationPathGenerator: Node ${nodeId} not found`);
return;
}

// Add current node to path and visited set
pathSteps.push({ nodeId });
visited.add(nodeId);

// Check if this is an exit node
if (this.isExitNode(node)) {
this.createPathFromSteps(pathSteps, paths, nodeMap);
pathSteps.pop();
visited.delete(nodeId);
return;
}

// Get outgoing edges
const outgoingEdges = edgeMap.get(nodeId) || [];

if (outgoingEdges.length === 0) {
// Dead end - create path
this.createPathFromSteps(pathSteps, paths, nodeMap);
pathSteps.pop();
visited.delete(nodeId);
return;
}

// Handle different node types
if (this.isDecisionNode(node)) {
// For decision nodes, create separate paths for each branch
for (const edge of outgoingEdges) {
const edgeId = `${edge.from}_${edge.to}`;
pathSteps[pathSteps.length - 1].edgeId = edgeId;

this.generatePathsFromNode(
edge.to,
nodeMap,
edgeMap,
paths,
new Set(visited), // Create new visited set for each branch
[...pathSteps], // Create new path steps array
depth + 1
);
}
} else if (this.isLoopNode(node)) {
// Handle loops with limited iterations
const loopIterations = Math.min(outgoingEdges.length, this.MAX_LOOP_ITERATIONS);

for (let i = 0; i < loopIterations; i++) {
const edge = outgoingEdges[0]; // Assume first edge is the loop continuation
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The assumption that outgoingEdges[0] is always the loop continuation path is risky and may not be true for all control flow structures (e.g., a for loop could have its exit condition check result in a different edge order). This could lead to incorrect animation paths. The logic should be made more robust, for example by analyzing edge properties or labels if available to distinguish between the loop body and loop exit paths.

const edgeId = `${edge.from}_${edge.to}`;

pathSteps[pathSteps.length - 1].edgeId = edgeId;
pathSteps[pathSteps.length - 1].isLoopIteration = i > 0;

this.generatePathsFromNode(
edge.to,
nodeMap,
edgeMap,
paths,
new Set(visited),
[...pathSteps],
depth + 1
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Loop Handling Fails to Explore Multiple Exit Paths

The loop handling in generatePathsFromNode has two issues: the number of simulated iterations is incorrectly tied to the count of outgoing edges (Math.min(outgoingEdges.length, this.MAX_LOOP_ITERATIONS)), leading to inconsistent loop counts. Also, it always follows the first outgoing edge (outgoingEdges[0]) for all iterations, preventing exploration of other potential loop exit paths.

Fix in Cursor Fix in Web

} else {
// Regular node - follow first outgoing edge
const edge = outgoingEdges[0];
const edgeId = `${edge.from}_${edge.to}`;

pathSteps[pathSteps.length - 1].edgeId = edgeId;

this.generatePathsFromNode(
edge.to,
nodeMap,
edgeMap,
paths,
visited,
pathSteps,
depth + 1
);
}

pathSteps.pop();
visited.delete(nodeId);
}

private static isExitNode(node: FlowchartNode): boolean {
return (
node.nodeType === NodeType.EXIT ||
node.nodeType === NodeType.RETURN ||
node.label.toLowerCase().includes('return') ||
node.label.toLowerCase().includes('exit')
);
}

private static isDecisionNode(node: FlowchartNode): boolean {
return (
node.nodeType === NodeType.DECISION ||
node.shape === 'diamond' ||
node.label.toLowerCase().includes('if') ||
node.label.toLowerCase().includes('else') ||
node.label.toLowerCase().includes('switch') ||
node.label.toLowerCase().includes('case')
);
}

private static isLoopNode(node: FlowchartNode): boolean {
return (
node.nodeType === NodeType.LOOP_START ||
node.nodeType === NodeType.LOOP_END ||
node.label.toLowerCase().includes('for') ||
node.label.toLowerCase().includes('while') ||
node.label.toLowerCase().includes('loop')
);
}

private static createPathFromSteps(
steps: AnimationPathStep[],
paths: AnimationPath[],
nodeMap: Map<string, FlowchartNode>
): void {
if (steps.length === 0) {
return;
}

const pathId = `path_${paths.length}`;
const nodes: string[] = [];
const edges: string[] = [];
let description = '';

for (let i = 0; i < steps.length; i++) {
const step = steps[i];
nodes.push(step.nodeId);

if (step.edgeId) {
edges.push(step.edgeId);
}

// Build description
const node = nodeMap.get(step.nodeId);
if (node) {
if (i === 0) {
description = `Start: ${node.label}`;
} else if (step.isLoopIteration) {
description += ` → [Loop] ${node.label}`;
} else {
description += ` → ${node.label}`;
}
}
}

// Add end marker
if (steps.length > 1) {
description += ' → End';
}

paths.push({
id: pathId,
nodes,
edges,
description
});
}

private static generateFallbackPath(ir: FlowchartIR): AnimationPath[] {
// If no paths were generated, create a simple linear path
const nodes = ir.nodes.map(n => n.id);
const edges: string[] = [];

// Create edges between consecutive nodes
for (let i = 0; i < nodes.length - 1; i++) {
edges.push(`${nodes[i]}_${nodes[i + 1]}`);
}

return [{
id: 'path_0',
nodes,
edges,
description: `Linear path: ${nodes.join(' → ')}`
}];
}
Comment on lines +257 to +273
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The fallback path generation is flawed. It assumes a linear execution flow based on the order of nodes in the ir.nodes array, which is not guaranteed to be correct. It also fabricates edge IDs between consecutive nodes, which may not correspond to actual edges in the graph. This can result in a nonsensical or broken fallback animation. A more robust fallback would be to perform a simple depth-first traversal from the entry node without exploring branches, to construct a single, valid path.


/**
* Get a simplified path description for UI display
*/
public static getPathDescription(path: AnimationPath, maxLength: number = 50): string {
if (path.description.length <= maxLength) {
return path.description;
}

return path.description.substring(0, maxLength - 3) + '...';
}

/**
* Check if a path contains loops
*/
public static hasLoops(path: AnimationPath): boolean {
const nodeCounts = new Map<string, number>();

for (const nodeId of path.nodes) {
const count = nodeCounts.get(nodeId) || 0;
nodeCounts.set(nodeId, count + 1);
}

return Array.from(nodeCounts.values()).some(count => count > 1);
}
Comment on lines +278 to +298
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These methods (getPathDescription and hasLoops) are public but seem to be unused in the current implementation. If they are intended for future use, it might be worth adding a comment indicating that. Otherwise, they should be removed to avoid dead code.

}
Loading