unit Myc.Ast.Visualizer; interface uses System.SysUtils, System.Classes, System.UITypes, System.Types, System.Math.Vectors, System.Generics.Collections, FMX.Types, FMX.Controls, FMX.Objects, FMX.Graphics, Myc.Ast.Nodes; type TPinConnection = record OutputPin: TControl; InputPin: TControl; constructor Create(AOutputPin, AInputPin: TControl); end; TPinShape = (psCircle, psTriangle); TPinAlignment = (paLeft, paRight); // New enum to select the visualization style. TVisualizationMode = (vmDetailed, vmControlFlow); // A movable panel with a custom-painted border and title. TAuraNode = class(TStyledControl) private // Flag to indicate if the control is currently being dragged. FIsDragging: Boolean; // Stores the mouse coordinates at the beginning of the drag operation. FDownPos: TPointF; // Properties for the custom-drawn border. FBorderColor: TAlphaColor; FBorderWidth: Single; FBorderRadius: Single; // Properties for the title FTitle: string; FTitleFont: TFont; FTitleFontColor: TAlphaColor; // If true, the node is drawn without a border and with a transparent background. FFrameless: Boolean; procedure SetBorderColor(const Value: TAlphaColor); procedure SetBorderWidth(const Value: Single); procedure SetBorderRadius(const Value: Single); procedure SetTitle(const Value: string); procedure SetTitleFont(const Value: TFont); procedure SetTitleFontColor(const Value: TAlphaColor); procedure TitleFontChanged(Sender: TObject); procedure SetFrameless(const Value: Boolean); protected procedure Paint; override; procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override; procedure MouseMove(Shift: TShiftState; X, Y: Single); override; procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; published // Custom border properties property BorderColor: TAlphaColor read FBorderColor write SetBorderColor; property BorderWidth: Single read FBorderWidth write SetBorderWidth; property BorderRadius: Single read FBorderRadius write SetBorderRadius; // Title properties property Title: string read FTitle write SetTitle; property TitleFont: TFont read FTitleFont write SetTitleFont; property TitleFontColor: TAlphaColor read FTitleFontColor write SetTitleFontColor; // Behavior properties property Frameless: Boolean read FFrameless write SetFrameless; // Standard control properties property Align; property Anchors; property ClipChildren default True; property Cursor default crDefault; property DragMode default TDragMode.dmManual; property Enabled; property Height; property HelpContext; property HelpKeyword; property HelpType; property Hint; property HitTest default True; property Locked; property Margins; property Opacity; property Padding; property PopupMenu; property Position; property RotationAngle; property RotationCenter; property Scale; property Size; property StyleLookup; property Visible; property Width; property OnClick; property OnDblClick; property OnMouseDown; property OnMouseMove; property OnMouseUp; property OnMouseWheel; property OnMouseEnter; property OnMouseLeave; end; TAuraWorkspace = class(TStyledControl) private FConnections: TArray; // If the mouse is dragging the background (i.e., no child control is being dragged), // then FPanning should be dragged. FPanning: TSizeF; // Added for panning implementation. FIsPanning: Boolean; FLastPanPos: TPointF; // Added for zooming implementation. FZoom: Single; protected procedure Paint; override; procedure DoDeleteChildren; override; // Add a zoom factor that, along with FPanning, zooms the children via the mouse wheel. procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override; procedure MouseMove(Shift: TShiftState; X, Y: Single); override; procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override; procedure MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean); override; procedure DblClick; override; public constructor Create(AOwner: TComponent); override; procedure BuildTree(const Root: IAstNode; const Position: TPointF; AMode: TVisualizationMode); function GetChildrenMatrix(var Matrix: TMatrix; var Simple: Boolean): Boolean; override; function Zoom(Factor: Single): Boolean; published // Standard control properties property Align; property Anchors; property ClipChildren default True; property Cursor default crDefault; property DragMode default TDragMode.dmManual; property Enabled; property Height; property HelpContext; property HelpKeyword; property HelpType; property Hint; property HitTest default True; property Locked; property Margins; property Opacity; property Padding; property PopupMenu; property Position; property RotationAngle; property RotationCenter; property Scale; property Size; property StyleLookup; property Visible; property Width; property OnClick; property OnDblClick; property OnMouseDown; property OnMouseMove; property OnMouseUp; property OnMouseWheel; property OnMouseEnter; property OnMouseLeave; end; TAstToAuraNodeVisitor = class(TInterfacedObject, IAstVisitor) public type // Holds the result of a node visitation, separating layout and connection concerns. TAuraNodeResult = record LayoutNode: TAuraNode; OutputPin: TControl; end; TAuraNodeResultList = TList; TChildVisitorProc = reference to procedure(const Visitor: IAstVisitor); // The lambda now returns the full result, including the output pin reference. TCreateParentNodeProc = reference to function(const InputNodes: TAuraNodeResultList): TAuraNodeResult; TVerticalAlignment = (vaCenter, vaTop); private const // Pin Visuals cPinSize = 10; cDataPinColor = TAlphaColors.Dodgerblue; cExecPinColor = TAlphaColors.Lightgreen; // Node Layout cHorizontalPadding = 25; // Padding inside the node title bar cPinOffsetY = 18; // Vertical distance between pins cVerticalPadding = 10; // General vertical padding inside container nodes // Specific Node Heights cDefaultNodeHeight = 25; // Default height for simple nodes like BinaryExpr cIfNodeHeight = 40; // Taller height for If nodes with more pins private FWorkspace: TAuraWorkspace; FParentControl: TControl; FCurrentPos: TPointF; FSpacing: TPointF; FLastResult: TAuraNodeResult; FConnections: TList; FCurrentExec: TList; FMode: TVisualizationMode; procedure FinalizeNodeLayout(const Node: TAuraNode); function VisitContainerNode( const Title, Details: string; const InPin, OutPin: String; IsExec: Boolean; HasResult: Boolean; const ChildVisitorProc: TChildVisitorProc ): TAuraNode; function VisitOperatorNode( const InputExpressions: TArray; const CreateParentProc: TCreateParentNodeProc; Alignment: TVerticalAlignment = vaCenter ): TAuraNode; function BuildNodeControl(const Title, Details: string): TAuraNode; function CreateNodeControl(const Title, Details: string): TAuraNode; function CreatePin( ParentNode: TAuraNode; Shape: TPinShape; Alignment: TPinAlignment; Color: TAlphaColor; const Tag: string ): TShape; function CreateInput(ParentNode: TAuraNode; const Caption: string): TControl; function CreateOutput(ParentNode: TAuraNode): TControl; function CreateEntry(ParentNode: TAuraNode; connect: Boolean = true): TControl; function CreateExit(ParentNode: TAuraNode; const Caption: string): TControl; function TryGetDescr(const Node: IAstNode; out Text: String): Boolean; public constructor Create( AWorkspace: TAuraWorkspace; AParentControl: TControl; const AStartPosition: TPointF; AConnections: TList; const ACurrentExec: TArray; AMode: TVisualizationMode ); destructor Destroy; override; // Public access to the collected connections property Connections: TList read FConnections; { IAstVisitor } function VisitConstant(const Node: IConstantNode): TAstValue; function VisitIdentifier(const Node: IIdentifierNode): TAstValue; function VisitBinaryExpression(const Node: IBinaryExpressionNode): TAstValue; function VisitUnaryExpression(const Node: IUnaryExpressionNode): TAstValue; function VisitIfExpression(const Node: IIfExpressionNode): TAstValue; function VisitTernaryExpression(const Node: ITernaryExpressionNode): TAstValue; function VisitLambdaExpression(const Node: ILambdaExpressionNode): TAstValue; function VisitFunctionCall(const Node: IFunctionCallNode): TAstValue; function VisitBlockExpression(const Node: IBlockExpressionNode): TAstValue; function VisitVariableDeclaration(const Node: IVariableDeclarationNode): TAstValue; function VisitAssignment(const Node: IAssignmentNode): TAstValue; function VisitIndexer(const Node: IIndexerNode): TAstValue; function VisitMemberAccess(const Node: IMemberAccessNode): TAstValue; function VisitCreateSeries(const Node: ICreateSeriesNode): TAstValue; function VisitAddSeriesItem(const Node: IAddSeriesItemNode): TAstValue; function VisitSeriesLength(const Node: ISeriesLengthNode): TAstValue; end; implementation uses System.Math, System.StrUtils, FMX.Platform, Myc.Data.Scalar; type // This visitor converts an AST expression subtree into a single string. TAstToTextVisitor = class(TInterfacedObject, IAstVisitor) public { IAstVisitor } function VisitConstant(const Node: IConstantNode): TAstValue; function VisitIdentifier(const Node: IIdentifierNode): TAstValue; function VisitBinaryExpression(const Node: IBinaryExpressionNode): TAstValue; function VisitUnaryExpression(const Node: IUnaryExpressionNode): TAstValue; function VisitIfExpression(const Node: IIfExpressionNode): TAstValue; function VisitTernaryExpression(const Node: ITernaryExpressionNode): TAstValue; function VisitLambdaExpression(const Node: ILambdaExpressionNode): TAstValue; function VisitFunctionCall(const Node: IFunctionCallNode): TAstValue; function VisitBlockExpression(const Node: IBlockExpressionNode): TAstValue; function VisitVariableDeclaration(const Node: IVariableDeclarationNode): TAstValue; function VisitAssignment(const Node: IAssignmentNode): TAstValue; function VisitIndexer(const Node: IIndexerNode): TAstValue; function VisitMemberAccess(const Node: IMemberAccessNode): TAstValue; function VisitCreateSeries(const Node: ICreateSeriesNode): TAstValue; function VisitAddSeriesItem(const Node: IAddSeriesItemNode): TAstValue; function VisitSeriesLength(const Node: ISeriesLengthNode): TAstValue; end; { TAstToTextVisitor } function TAstToTextVisitor.VisitAddSeriesItem(const Node: IAddSeriesItemNode): TAstValue; var seriesStr, valueStr, lookbackStr: string; begin seriesStr := Node.Series.Accept(Self).AsText; valueStr := Node.Value.Accept(Self).AsText; lookbackStr := ''; if Assigned(Node.Lookback) then lookbackStr := ', ' + Node.Lookback.Accept(Self).AsText; Result := Format('%s.add(%s%s)', [seriesStr, valueStr, lookbackStr]); end; function TAstToTextVisitor.VisitAssignment(const Node: IAssignmentNode): TAstValue; begin Result := Node.Identifier.Name + ' := ' + Node.Value.Accept(Self).AsText; end; function TAstToTextVisitor.VisitBinaryExpression(const Node: IBinaryExpressionNode): TAstValue; begin var leftStr := Node.Left.Accept(Self).AsText; var rightStr := Node.Right.Accept(Self).AsText; Result := '(' + leftStr + ' ' + Node.Operator.ToString + ' ' + rightStr + ')'; end; function TAstToTextVisitor.VisitBlockExpression(const Node: IBlockExpressionNode): TAstValue; begin Result := '{...}'; end; function TAstToTextVisitor.VisitConstant(const Node: IConstantNode): TAstValue; begin Result := Node.Value.ToString; end; function TAstToTextVisitor.VisitCreateSeries(const Node: ICreateSeriesNode): TAstValue; begin Result := 'new series(' + Node.Definition + ')'; end; function TAstToTextVisitor.VisitFunctionCall(const Node: IFunctionCallNode): TAstValue; var i: Integer; sb: TStringBuilder; calleeStr: string; begin calleeStr := Node.Callee.Accept(Self).AsText; sb := TStringBuilder.Create; try sb.Append(calleeStr); sb.Append('('); for i := 0 to Node.Arguments.Count - 1 do begin sb.Append(Node.Arguments[i].Accept(Self).AsText); if i < Node.Arguments.Count - 1 then sb.Append(', '); end; sb.Append(')'); Result := sb.ToString; finally sb.Free; end; end; function TAstToTextVisitor.VisitIdentifier(const Node: IIdentifierNode): TAstValue; begin Result := Node.Name; end; function TAstToTextVisitor.VisitIfExpression(const Node: IIfExpressionNode): TAstValue; begin Result := Node.Condition.Accept(Self).AsText; end; function TAstToTextVisitor.VisitIndexer(const Node: IIndexerNode): TAstValue; var baseStr, indexStr: string; begin baseStr := Node.Base.Accept(Self).AsText; indexStr := Node.Index.Accept(Self).AsText; Result := Format('%s[%s]', [baseStr, indexStr]); end; function TAstToTextVisitor.VisitLambdaExpression(const Node: ILambdaExpressionNode): TAstValue; var i: Integer; sb: TStringBuilder; begin sb := TStringBuilder.Create; try sb.Append(#$03BB + '('); if Length(Node.Parameters) > 0 then begin for i := 0 to High(Node.Parameters) do begin // Correctly append the parameter name from the node. sb.Append(Node.Parameters[i].Name); if i < High(Node.Parameters) then sb.Append(', '); end; end; sb.Append(') => {...}'); Result := sb.ToString; finally sb.Free; end; end; function TAstToTextVisitor.VisitMemberAccess(const Node: IMemberAccessNode): TAstValue; var baseStr: string; begin baseStr := Node.Base.Accept(Self).AsText; Result := Format('%s.%s', [baseStr, Node.Member.Name]); end; function TAstToTextVisitor.VisitSeriesLength(const Node: ISeriesLengthNode): TAstValue; var seriesStr: string; begin // Get the string representation of the series identifier by visiting the node. seriesStr := Node.Series.Accept(Self).AsText; Result := Format('length(%s)', [seriesStr]); end; function TAstToTextVisitor.VisitTernaryExpression(const Node: ITernaryExpressionNode): TAstValue; begin var condStr := Node.Condition.Accept(Self).AsText; var thenStr := Node.ThenBranch.Accept(Self).AsText; var elseStr := Node.ElseBranch.Accept(Self).AsText; Result := Format('(%s ? %s : %s)', [condStr, thenStr, elseStr]); end; function TAstToTextVisitor.VisitUnaryExpression(const Node: IUnaryExpressionNode): TAstValue; begin Result := Node.Operator.ToString + ' ' + Node.Right.Accept(Self).AsText; end; function TAstToTextVisitor.VisitVariableDeclaration(const Node: IVariableDeclarationNode): TAstValue; var initStr: string; begin if Assigned(Node.Initializer) then initStr := ' := ' + Node.Initializer.Accept(Self).AsText else initStr := ''; Result := 'var ' + Node.Identifier.Name + initStr; end; { TPinConnection } constructor TPinConnection.Create(AOutputPin, AInputPin: TControl); begin OutputPin := AOutputPin; InputPin := AInputPin; end; { TAuraNode } constructor TAuraNode.Create(AOwner: TComponent); begin inherited Create(AOwner); FIsDragging := False; // Default border settings FBorderColor := TAlphaColors.Gray; FBorderWidth := 1; FBorderRadius := 9; // Sharp corners by default // Default title settings FTitle := 'Node'; FTitleFont := TFont.Create; FTitleFont.Family := 'Segoe UI'; FTitleFont.Size := 11; FTitleFont.Style := [TFontStyle.fsBold]; FTitleFont.OnChanged := TitleFontChanged; FTitleFontColor := TAlphaColors.Black; // Default state for frameless mode FFrameless := False; Width := 80; Height := 45; // The panel must be able to receive mouse events. // HitTest := True; // Clip children to the panel's bounds. ClipChildren := True; end; destructor TAuraNode.Destroy; begin FTitleFont.Free; inherited; end; procedure TAuraNode.Paint; var rect, titleRect: TRectF; effectiveBorderWidth: Single; begin inherited; // Allow styled painting to occur first (if any) if FFrameless then begin Canvas.Fill.Kind := TBrushKind.Solid; Canvas.Fill.Color := $20C0C0C0; // Transparent Silver Canvas.Stroke.Kind := TBrushKind.None; // In frameless mode, draw a transparent background and ignore the border. Canvas.FillRect(TRectF.Create(0, 0, Width, Height), 0, 0, [], 1.0); effectiveBorderWidth := 0; end else begin // Custom painting for the border if (FBorderWidth > 0) and (FBorderColor <> TAlphaColors.Null) then begin rect := TRectF.Create(0, 0, Width, Height); // Inflate inwards so the border is fully visible within the control's bounds rect.Inflate(-FBorderWidth / 2, -FBorderWidth / 2); Canvas.Stroke.Kind := TBrushKind.Solid; Canvas.Stroke.Color := FBorderColor; Canvas.Stroke.Thickness := FBorderWidth; if FFrameless then begin Canvas.Fill.Kind := TBrushKind.Solid; Canvas.Fill.Color := $20C0C0C0; // Transparent Silver Canvas.FillRect(rect, FBorderRadius, FBorderRadius, AllCorners, 1.0, TCornerType.Round); end else Canvas.DrawRect(rect, FBorderRadius, FBorderRadius, AllCorners, 1.0, TCornerType.Round); end; effectiveBorderWidth := FBorderWidth; end; // Draw the title if not FTitle.IsEmpty then begin // Define the rectangle for the title at the top of the control titleRect := TRectF.Create(0, effectiveBorderWidth, Width, effectiveBorderWidth + FTitleFont.Size * 1.8); Canvas.Font.Assign(FTitleFont); Canvas.Fill.Color := FTitleFontColor; // Draw the text centered horizontally and vertically within the title rectangle Canvas.FillText(titleRect, FTitle, False, 1.0, [], TTextAlign.Center, TTextAlign.Center); end; end; procedure TAuraNode.SetBorderColor(const Value: TAlphaColor); begin if FBorderColor <> Value then begin FBorderColor := Value; Repaint; end; end; procedure TAuraNode.SetBorderRadius(const Value: Single); begin if FBorderRadius <> Value then begin FBorderRadius := Value; Repaint; end; end; procedure TAuraNode.SetBorderWidth(const Value: Single); begin if FBorderWidth <> Value then begin FBorderWidth := Value; Repaint; end; end; procedure TAuraNode.SetFrameless(const Value: Boolean); begin if FFrameless <> Value then begin FFrameless := Value; Repaint; end; end; procedure TAuraNode.SetTitle(const Value: string); begin if FTitle <> Value then begin FTitle := Value; Repaint; end; end; procedure TAuraNode.SetTitleFont(const Value: TFont); begin FTitleFont.Assign(Value); end; procedure TAuraNode.SetTitleFontColor(const Value: TAlphaColor); begin if FTitleFontColor <> Value then begin FTitleFontColor := Value; Repaint; end; end; procedure TAuraNode.TitleFontChanged(Sender: TObject); begin Repaint; end; procedure TAuraNode.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); begin inherited; if Button = TMouseButton.mbLeft then begin if ObjectAtPoint(LocalToScreen(TPointF.Create(X, Y))) = Self as IControl then begin FIsDragging := True; FDownPos := TPointF.Create(X, Y); Capture; end; end; end; procedure TAuraNode.MouseMove(Shift: TShiftState; X, Y: Single); begin inherited; if FIsDragging then begin var deltaX := X - FDownPos.X; var deltaY := Y - FDownPos.Y; Position.X := Position.X + deltaX; Position.Y := Position.Y + deltaY; if ParentControl <> nil then ParentControl.Repaint; end; end; procedure TAuraNode.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); begin inherited; if (Button = TMouseButton.mbLeft) and (FIsDragging) then begin ReleaseCapture; FIsDragging := False; end; end; { TAuraWorkspace } constructor TAuraWorkspace.Create(AOwner: TComponent); begin inherited; // Initialize zoom factor. FZoom := 1.0; end; procedure TAuraWorkspace.BuildTree(const Root: IAstNode; const Position: TPointF; AMode: TVisualizationMode); begin var connections := TList.Create; try Root.Accept(TAstToAuraNodeVisitor.Create(Self, Self, Position, connections, nil, AMode)); FConnections := FConnections + connections.ToArray; finally connections.Free; end; end; procedure TAuraWorkspace.DoDeleteChildren; begin FConnections := nil; inherited; end; function TAuraWorkspace.GetChildrenMatrix(var Matrix: TMatrix; var Simple: Boolean): Boolean; begin // Combine scaling (zoom) and translation (pan) into a single matrix. // The order is important: scale first, then translate. Matrix := TMatrix.CreateScaling(FZoom, FZoom) * TMatrix.CreateTranslation(FPanning.cx, FPanning.cy); Simple := (FZoom = 1.0) and (FPanning.cx = 0) and (FPanning.cy = 0); Result := true; end; procedure TAuraWorkspace.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); begin inherited; // Start panning if left button is pressed on the background if (Button = TMouseButton.mbLeft) and (ObjectAtPoint(LocalToScreen(TPointF.Create(X, Y))) = Self as IControl) then begin FIsPanning := True; FLastPanPos := TPointF.Create(X, Y); Cursor := crHandPoint; end; end; procedure TAuraWorkspace.MouseMove(Shift: TShiftState; X, Y: Single); var delta: TPointF; begin inherited; if FIsPanning then begin // Calculate movement delta delta := TPointF.Create(X - FLastPanPos.X, Y - FLastPanPos.Y); // Update panning offset FPanning.cx := FPanning.cx + delta.X; FPanning.cy := FPanning.cy + delta.Y; // Store current position for next move FLastPanPos := TPointF.Create(X, Y); // Trigger repaint to apply the new transformation matrix RecalcAbsolute; RecalcUpdateRect; Repaint; end; end; procedure TAuraWorkspace.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); begin inherited; // Stop panning if (Button = TMouseButton.mbLeft) and (FIsPanning) then begin FIsPanning := False; Cursor := crDefault; end; end; procedure TAuraWorkspace.MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean); begin inherited; Handled := Zoom(IfThen(WheelDelta > 0, FZoom * 1.1, FZoom / 1.1)); end; procedure TAuraWorkspace.DblClick; begin inherited; Zoom(1.0); end; procedure TAuraWorkspace.Paint; var connection: TPinConnection; startPoint, endPoint, pinCenter: TPointF; path: TPathData; controlPoint1, controlPoint2: TPointF; controlOffset: Single; begin inherited; Canvas.Stroke.Kind := TBrushKind.Solid; Canvas.Stroke.Thickness := 2; // Go through all stored connections and draw them for connection in FConnections do begin // Get the absolute coordinates of the pin centers pinCenter := TPointF.Create(connection.OutputPin.Width / 2, connection.OutputPin.Height / 2); var absoluteStart := connection.OutputPin.LocalToAbsolute(pinCenter); pinCenter := TPointF.Create(connection.InputPin.Width / 2, connection.InputPin.Height / 2); var absoluteEnd := connection.InputPin.LocalToAbsolute(pinCenter); // Convert them to the local coordinates of the workspace startPoint := AbsoluteToLocal(absoluteStart); endPoint := AbsoluteToLocal(absoluteEnd); var str := connection.InputPin.TagString; if Pos('data', str) = 0 then Canvas.Stroke.Color := TAstToAuraNodeVisitor.cExecPinColor else Canvas.Stroke.Color := TAstToAuraNodeVisitor.cDataPinColor; // Draw a Bezier curve with horizontal tangents at start and end points. path := TPathData.Create; try controlOffset := max(25, 0.5 * endPoint.Distance(startPoint)); controlPoint1 := TPointF.Create(startPoint.X + controlOffset, startPoint.Y); controlPoint2 := TPointF.Create(endPoint.X - controlOffset, endPoint.Y); path.MoveTo(startPoint); path.CurveTo(controlPoint1, controlPoint2, endPoint); Canvas.DrawPath(path, 1.0); finally path.Free; end; end; end; function TAuraWorkspace.Zoom(Factor: Single): Boolean; var MouseService: IFMXMouseService; begin Result := TPlatformServices.Current.SupportsPlatformService(IFMXMouseService, MouseService); if Result then begin var MousePos := ScreenToLocal(MouseService.GetMousePos); // Clamp zoom level to a reasonable range Factor := Max(0.2, Min(3.0, Factor)); // Adjust panning to keep the point under the cursor stationary FPanning.cx := MousePos.X * (1 - Factor / FZoom) + FPanning.cx * (Factor / FZoom); FPanning.cy := MousePos.Y * (1 - Factor / FZoom) + FPanning.cy * (Factor / FZoom); FZoom := Factor; // Trigger repaint to apply the new transformation matrix RecalcAbsolute; RecalcUpdateRect; Repaint; end; end; { TAstToAuraNodeVisitor } constructor TAstToAuraNodeVisitor.Create( AWorkspace: TAuraWorkspace; AParentControl: TControl; const AStartPosition: TPointF; AConnections: TList; const ACurrentExec: TArray; AMode: TVisualizationMode ); begin inherited Create; FWorkspace := AWorkspace; FParentControl := AParentControl; FCurrentPos := AStartPosition; FSpacing := TPointF.Create(20, 10); // Horizontal indent, Vertical spacing FConnections := AConnections; FCurrentExec := TList.Create(ACurrentExec); FLastResult.LayoutNode := nil; FLastResult.OutputPin := nil; FMode := AMode; end; destructor TAstToAuraNodeVisitor.Destroy; begin FCurrentExec.Free; inherited; end; procedure TAstToAuraNodeVisitor.FinalizeNodeLayout(const Node: TAuraNode); var control: TControl; leftPins, rightPins: TList; numLeftPins, numRightPins, maxPins: Integer; reqHeight: Single; i: Integer; totalHeight, startY: Single; begin leftPins := TList.Create; rightPins := TList.Create; try // 1. Collect all pins and sort them by alignment for control in Node.Controls do begin if (Pos('exec.', control.TagString) = 1) or (Pos('data.', control.TagString) = 1) then begin // Use the pin's X position to determine if it's on the left or right side. if control.Position.X < (Node.Width / 2) then leftPins.Add(control) else rightPins.Add(control); end; end; numLeftPins := leftPins.Count; numRightPins := rightPins.Count; maxPins := Max(numLeftPins, numRightPins); if maxPins = 0 then exit; // 2. Calculate and set new node height. // It must accommodate the pins plus vertical padding. reqHeight := cVerticalPadding + (maxPins - 1) * cPinOffsetY + cVerticalPadding; Node.Height := Max(Node.Height, reqHeight); Node.Height := Max(cDefaultNodeHeight, Node.Height); // Ensure minimum height // 3. Position left pins. They are arranged centered on the node. if numLeftPins > 0 then begin totalHeight := (numLeftPins - 1) * cPinOffsetY; startY := (Node.Height / 2) - (totalHeight / 2.0); for i := 0 to numLeftPins - 1 do leftPins[i].Position.Y := startY + i * cPinOffsetY - (cPinSize / 2); end; // 4. Position right pins. if numRightPins > 0 then begin totalHeight := (numRightPins - 1) * cPinOffsetY; startY := (Node.Height / 2) - (totalHeight / 2.0); for i := 0 to numRightPins - 1 do rightPins[i].Position.Y := startY + i * cPinOffsetY - (cPinSize / 2); end; finally leftPins.Free; rightPins.Free; end; end; function TAstToAuraNodeVisitor.BuildNodeControl(const Title, Details: string): TAuraNode; var fullTitle: string; textWidth: Single; newWidth: Single; measureCanvas: TCanvas; begin Result := TAuraNode.Create(FParentControl); Result.Parent := FParentControl; // Build the full title string first fullTitle := Title; if not Details.IsEmpty then fullTitle := fullTitle + ': ' + Details; Result.Title := fullTitle; // Adjust the width so that the text is completely visible. measureCanvas := TCanvasManager.MeasureCanvas; if Assigned(measureCanvas) then begin measureCanvas.Font.Assign(Result.TitleFont); textWidth := measureCanvas.TextWidth(Result.Title); newWidth := textWidth + (2 * cHorizontalPadding); // Ensure the new width is not smaller than the default width of the node. Result.Width := Max(Result.Width, newWidth); end; end; function TAstToAuraNodeVisitor.CreateInput(ParentNode: TAuraNode; const Caption: string): TControl; begin var cap := Caption; if cap <> '' then cap := '.' + cap; Result := CreatePin(ParentNode, psCircle, paLeft, cDataPinColor, 'data.in' + cap); Result.Hint := Caption; Result.ShowHint := Caption <> ''; end; function TAstToAuraNodeVisitor.CreateNodeControl(const Title, Details: string): TAuraNode; begin Result := BuildNodeControl(Title, Details); Result.Position.Point := FCurrentPos; // Advance vertical position for the next node FCurrentPos.Y := FCurrentPos.Y + Result.Height + FSpacing.Y; FLastResult.LayoutNode := Result; FLastResult.OutputPin := nil; // Default: a new node has no specific data output pin end; function TAstToAuraNodeVisitor.CreateOutput(ParentNode: TAuraNode): TControl; begin Result := CreatePin(ParentNode, psCircle, paRight, cDataPinColor, 'data.out'); end; function TAstToAuraNodeVisitor.CreateEntry(ParentNode: TAuraNode; connect: Boolean = true): TControl; begin Result := CreatePin(ParentNode, psTriangle, paLeft, cExecPinColor, 'exec.in'); if connect then begin // Connect all open exits to this new entry for var curr in FCurrentExec do FConnections.Add(TPinConnection.Create(curr, Result)); FCurrentExec.Clear; end; end; function TAstToAuraNodeVisitor.CreateExit(ParentNode: TAuraNode; const Caption: string): TControl; begin var cap := Caption; if cap <> '' then cap := '.' + cap; Result := CreatePin(ParentNode, psTriangle, paRight, cExecPinColor, 'exec.out' + cap); FCurrentExec.Add(Result); end; function TAstToAuraNodeVisitor.CreatePin( ParentNode: TAuraNode; Shape: TPinShape; Alignment: TPinAlignment; Color: TAlphaColor; const Tag: string ): TShape; var posX, posY: Single; begin // Vertical position is now set later by FinalizeNodeLayout. // Set a temporary Y position. posY := 0; // Calculate horizontal position if Alignment = paLeft then posX := 0 else // paRight posX := ParentNode.Width - cPinSize; // Create the shape case Shape of psCircle: begin var circle := TEllipse.Create(ParentNode); circle.Fill.Color := Color; circle.Stroke.Kind := TBrushKind.None; Result := circle; end; psTriangle: begin var triangle := TPath.Create(ParentNode); triangle.Data.Data := Format('M 0 0 L %d %d L 0 %d Z', [cPinSize, cPinSize div 2, cPinSize]); triangle.Fill.Color := Color; triangle.Stroke.Kind := TBrushKind.None; Result := triangle; end; else Result := nil; exit; end; // Common properties Result.Parent := ParentNode; Result.SetBounds(posX, posY, cPinSize, cPinSize); Result.HitTest := true; // Use TagString to identify the control as a pin and describe its function. Result.TagString := Tag; end; function TAstToAuraNodeVisitor.TryGetDescr(const Node: IAstNode; out Text: String): Boolean; begin if FMode <> vmControlFlow then exit(false); var textVisitor: IAstVisitor := TAstToTextVisitor.Create; Text := Node.Accept(textVisitor).AsText; Result := Pos('{', Text) = 0; end; function TAstToAuraNodeVisitor.VisitAddSeriesItem(const Node: IAddSeriesItemNode): TAstValue; var details: string; inputs: TArray; begin if (FMode = vmControlFlow) and TryGetDescr(Node, details) then begin var addNode := CreateNodeControl('Add to Series', details); CreateEntry(addNode); CreateExit(addNode, ''); FinalizeNodeLayout(addNode); end else begin if Assigned(Node.Lookback) then inputs := [Node.Series, Node.Value, Node.Lookback] else inputs := [Node.Series, Node.Value]; VisitOperatorNode( inputs, function(const InputResults: TAuraNodeResultList): TAuraNodeResult var addNode: TAuraNode; seriesPin, valuePin, lookbackPin: TControl; begin addNode := BuildNodeControl('Add to Series', ''); CreateEntry(addNode); seriesPin := CreateInput(addNode, 'Series'); valuePin := CreateInput(addNode, 'Value'); if Assigned(InputResults[0].OutputPin) then FConnections.Add(TPinConnection.Create(InputResults[0].OutputPin, seriesPin)); if Assigned(InputResults[1].OutputPin) then FConnections.Add(TPinConnection.Create(InputResults[1].OutputPin, valuePin)); if InputResults.Count > 2 then begin lookbackPin := CreateInput(addNode, 'Lookback'); if Assigned(InputResults[2].OutputPin) then FConnections.Add(TPinConnection.Create(InputResults[2].OutputPin, lookbackPin)); end; CreateExit(addNode, ''); FinalizeNodeLayout(addNode); Result.LayoutNode := addNode; Result.OutputPin := nil; // No data output end, vaTop ); end; Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitAssignment(const Node: IAssignmentNode): TAstValue; begin var details: String; if (FMode = vmControlFlow) and TryGetDescr(Node, details) then begin var assignmentNode := CreateNodeControl('Assignment', details); CreateEntry(assignmentNode); CreateExit(assignmentNode, ''); FinalizeNodeLayout(assignmentNode); end else begin VisitOperatorNode( [Node.Value], function(const InputResults: TAuraNodeResultList): TAuraNodeResult var assignmentNode: TAuraNode; inputPin: TControl; valueResult: TAuraNodeResult; outPin: TControl; // variable for the output pin begin valueResult := InputResults[0]; assignmentNode := BuildNodeControl('Assignment', Node.Identifier.Name); CreateEntry(assignmentNode); inputPin := CreateInput(assignmentNode, Node.Identifier.Name); CreateExit(assignmentNode, ''); // Create the output pin before finalizing the layout. outPin := CreateOutput(assignmentNode); if Assigned(valueResult.OutputPin) then FConnections.Add(TPinConnection.Create(valueResult.OutputPin, inputPin)); // Now finalize, all pins are present. FinalizeNodeLayout(assignmentNode); Result.LayoutNode := assignmentNode; Result.OutputPin := outPin; end, vaTop ); end; Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitBinaryExpression(const Node: IBinaryExpressionNode): TAstValue; begin var exprStr: String; if (FMode = vmControlFlow) and TryGetDescr(Node, exprStr) then begin var exprNode := CreateNodeControl('Expression', exprStr); FLastResult.OutputPin := CreateOutput(exprNode); // Provide an output for potential connection FinalizeNodeLayout(exprNode); end else begin VisitOperatorNode( [Node.Left, Node.Right], function(const InputResults: TAuraNodeResultList): TAuraNodeResult var binaryExprNode: TAuraNode; inputPin1, inputPin2: TControl; leftResult, rightResult: TAuraNodeResult; outPin: TControl; begin leftResult := InputResults[0]; rightResult := InputResults[1]; binaryExprNode := BuildNodeControl(Node.Operator.ToString, ''); binaryExprNode.TitleFont.Size := 20; inputPin1 := CreateInput(binaryExprNode, ''); // Caption is in Hint inputPin2 := CreateInput(binaryExprNode, ''); outPin := CreateOutput(binaryExprNode); if Assigned(leftResult.OutputPin) then FConnections.Add(TPinConnection.Create(leftResult.OutputPin, inputPin1)); if Assigned(rightResult.OutputPin) then FConnections.Add(TPinConnection.Create(rightResult.OutputPin, inputPin2)); FinalizeNodeLayout(binaryExprNode); Result.LayoutNode := binaryExprNode; Result.OutputPin := outPin; end ); end; Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitBlockExpression(const Node: IBlockExpressionNode): TAstValue; var blockNode: TAuraNode; childLastResult: TAuraNodeResult; begin childLastResult.LayoutNode := nil; childLastResult.OutputPin := nil; // Use the helper to create and populate the container node. blockNode := VisitContainerNode( '', // No title for blocks '', '', '', true, false, procedure(const Visitor: IAstVisitor) var expression: IAstNode; begin for expression in Node.Expressions do expression.Accept(Visitor); // Capture the full result of the last expression inside the block. childLastResult := (Visitor as TAstToAuraNodeVisitor).FLastResult; end ); // Blocks are drawn without title and border, with a transparent fill. blockNode.Frameless := true; // Update the state of the main visitor FCurrentPos.Y := blockNode.Position.Y + blockNode.Height + FSpacing.Y; FLastResult.LayoutNode := blockNode; // The layout node is the container itself. FLastResult.OutputPin := childLastResult.OutputPin; // The connection pin is from the internal node. Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitConstant(const Node: IConstantNode): TAstValue; var constantNode: TAuraNode; begin // In both modes, a constant is a simple node with an output. constantNode := CreateNodeControl('Constant', Node.Value.ToString); FLastResult.OutputPin := CreateOutput(constantNode); FinalizeNodeLayout(constantNode); Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitContainerNode( const Title, Details: string; const InPin, OutPin: String; IsExec: Boolean; HasResult: Boolean; const ChildVisitorProc: TChildVisitorProc ): TAuraNode; var containerNode: TAuraNode; childVisitor: IAstVisitor; childStartPos: TPointF; maxRight: Single; maxBottom: Single; control: TControl; childLastResult: TAuraNodeResult; begin const pinNodeHeight = cPinSize + cVerticalPadding; // Step 1: Create container node containerNode := BuildNodeControl(Title, Details); containerNode.Position.Point := FCurrentPos; Result := containerNode; // Set result early childStartPos := TPointF.Create(FSpacing.X, FSpacing.Y + max(IfThen(InPin <> '', pinNodeHeight, 0), IfThen(Title <> '', cVerticalPadding, 0))); var currExec := FCurrentExec.ToArray; if InPin <> '' then begin // Create a small entry node to define the start of the execution flow inside the container. var entryNode := BuildNodeControl(InPin, ''); entryNode.Parent := containerNode; entryNode.Position.Point := TPointF.Create(0, 0); entryNode.Height := pinNodeHeight; if IsExec then begin var entryPin := CreateEntry(containerNode); var entryExitPin := CreateExit(entryNode, ''); FinalizeNodeLayout(entryNode); entryPin.Position.Y := containerNode.AbsoluteToLocal(entryNode.LocalToAbsolute(entryExitPin.Position.Point)).Y; end else begin FCurrentExec.Clear; CreateExit(entryNode, ''); FinalizeNodeLayout(entryNode); end; end; // Step 2: Create child visitor and run the closure. // The child visitor starts its execution flow from the entry node's exit pin. childVisitor := TAstToAuraNodeVisitor.Create(FWorkspace, containerNode, childStartPos, FConnections, FCurrentExec.ToArray, FMode); ChildVisitorProc(childVisitor); // Capture the result from the child visitor. childLastResult := (childVisitor as TAstToAuraNodeVisitor).FLastResult; FCurrentExec.Clear; FCurrentExec.AddRange((childVisitor as TAstToAuraNodeVisitor).FCurrentExec); // Step 3: Adjust the size of the container node to fit children. maxRight := 0; maxBottom := 0; for control in containerNode.Controls do begin if control is TAuraNode then begin maxRight := Max(maxRight, control.Position.X + control.Width); maxBottom := Max(maxBottom, control.Position.Y + control.Height); end; end; // The width must be at least the title width, and large enough for children. containerNode.Width := Max(containerNode.Width, maxRight + FSpacing.X); containerNode.Height := Max(containerNode.Height, maxBottom + FSpacing.Y); if OutPin <> '' then begin var exitNode := BuildNodeControl(OutPin, ''); exitNode.Parent := containerNode; exitNode.Height := pinNodeHeight; var exitPin := CreateEntry(exitNode); // Create and connect a data pin if the container returns a value. if HasResult and Assigned(childLastResult.OutputPin) then begin var dataResultPin := CreateInput(exitNode, 'Result'); FConnections.Add(TPinConnection.Create(childLastResult.OutputPin, dataResultPin)); end; FinalizeNodeLayout(exitNode); // Enlarge for the exit node containerNode.Height := Max(containerNode.Height, maxBottom + FSpacing.Y + exitNode.Height); exitNode.Position.Point := TPointF.Create(containerNode.Width - exitNode.Width, containerNode.Height - exitNode.Height); if not IsExec then begin FCurrentExec.Clear; FCurrentExec.AddRange(currExec); end else CreateExit(containerNode, '').Position.Y := containerNode.AbsoluteToLocal(exitNode.LocalToAbsolute(exitPin.Position.Point)).Y; end; FinalizeNodeLayout(containerNode); end; function TAstToAuraNodeVisitor.VisitCreateSeries(const Node: ICreateSeriesNode): TAstValue; var seriesNode: TAuraNode; begin // This node creates a new series. It has no inputs, only a data output. // The definition is a string literal, so it's part of the node's title. seriesNode := CreateNodeControl('Create Series', Node.Definition); FLastResult.OutputPin := CreateOutput(seriesNode); FinalizeNodeLayout(seriesNode); Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitFunctionCall(const Node: IFunctionCallNode): TAstValue; var inputExpressions: TArray; begin var details: String; if (FMode = vmControlFlow) and TryGetDescr(Node, details) then begin var funcCallNode := CreateNodeControl('FunctionCall', details); CreateEntry(funcCallNode); FLastResult.OutputPin := CreateOutput(funcCallNode); // Function calls can return values CreateExit(funcCallNode, ''); FinalizeNodeLayout(funcCallNode); end else begin SetLength(inputExpressions, 1 + Node.Arguments.Count); inputExpressions[0] := Node.Callee; for var i := 0 to Node.Arguments.Count - 1 do inputExpressions[i + 1] := Node.Arguments[i]; VisitOperatorNode( inputExpressions, function(const InputResults: TAuraNodeResultList): TAuraNodeResult var funcCallNode: TAuraNode; calleePin: TControl; argPin: array of TControl; i: Integer; outPin: TControl; begin funcCallNode := BuildNodeControl('FunctionCall', ''); CreateEntry(funcCallNode); calleePin := CreateInput(funcCallNode, 'Callee'); SetLength(argPin, Node.Arguments.Count); for i := 0 to Node.Arguments.Count - 1 do argPin[i] := CreateInput(funcCallNode, 'Arg' + i.ToString); CreateExit(funcCallNode, ''); outPin := CreateOutput(funcCallNode); if Assigned(InputResults[0].OutputPin) then FConnections.Add(TPinConnection.Create(InputResults[0].OutputPin, calleePin)); for i := 0 to Node.Arguments.Count - 1 do if Assigned(InputResults[i + 1].OutputPin) then FConnections.Add(TPinConnection.Create(InputResults[i + 1].OutputPin, argPin[i])); FinalizeNodeLayout(funcCallNode); Result.LayoutNode := funcCallNode; Result.OutputPin := outPin; end ); end; Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitIdentifier(const Node: IIdentifierNode): TAstValue; begin var identifierNode := CreateNodeControl('Identifier', Node.Name); FLastResult.OutputPin := CreateOutput(identifierNode); FinalizeNodeLayout(identifierNode); Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitIfExpression(const Node: IIfExpressionNode): TAstValue; var startPosition: TPointF; ifNode: TAuraNode; conditionEndY: Single; branchStartX: Single; execPathes: TList; thenEndY, elseEndY: Single; begin startPosition := FCurrentPos; var condStr: String; if (FMode = vmControlFlow) and TryGetDescr(Node, condStr) then begin ifNode := CreateNodeControl('If', condStr); ifNode.Height := cIfNodeHeight; end else begin // Use helper for the condition part ifNode := VisitOperatorNode( [Node.Condition], function(const InputResults: TAuraNodeResultList): TAuraNodeResult var conditionInputPin: TControl; condResult: TAuraNodeResult; ifNode: TAuraNode; begin condResult := InputResults[0]; ifNode := BuildNodeControl('If', ''); conditionInputPin := CreateInput(ifNode, 'Condition'); if Assigned(condResult.OutputPin) then FConnections.Add(TPinConnection.Create(condResult.OutputPin, conditionInputPin)); // Do not finalize here, more pins will be added. Result.LayoutNode := ifNode; Result.OutputPin := nil; end ); end; CreateEntry(ifNode); conditionEndY := FCurrentPos.Y; // Visit the 'Then' and 'Else' execution branches, placing them to the right of the 'If' node. branchStartX := ifNode.Position.X + ifNode.Width + FSpacing.X; execPathes := TList.Create; try // Then branch FCurrentExec.Clear; CreateExit(ifNode, 'Then'); FCurrentPos.X := branchStartX; FCurrentPos.Y := startPosition.Y; Node.ThenBranch.Accept(Self); thenEndY := FCurrentPos.Y; FLastResult.LayoutNode := ifNode; execPathes.AddRange(FCurrentExec); // Else branch (if it exists) elseEndY := thenEndY; FCurrentExec.Clear; CreateExit(ifNode, 'Else'); if Assigned(Node.ElseBranch) then begin FCurrentPos.X := branchStartX; FCurrentPos.Y := thenEndY; Node.ElseBranch.Accept(Self); elseEndY := FCurrentPos.Y; end; execPathes.AddRange(FCurrentExec); // Finalize layout now that all pins are added. FinalizeNodeLayout(ifNode); // Finalize visitor state. FCurrentPos.X := startPosition.X; FCurrentPos.Y := Max(conditionEndY, elseEndY); FLastResult.LayoutNode := ifNode; // This node is for control-flow only and never produces a data output. FLastResult.OutputPin := nil; // After a branch, the execution flow has diverged. FCurrentExec.Clear; FCurrentExec.AddRange(execPathes); finally execPathes.Free; end; Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitIndexer(const Node: IIndexerNode): TAstValue; var details: String; begin if (FMode = vmControlFlow) and TryGetDescr(Node, details) then begin var indexerNode := CreateNodeControl('Expression', details); FLastResult.OutputPin := CreateOutput(indexerNode); FinalizeNodeLayout(indexerNode); end else begin VisitOperatorNode( [Node.Base, Node.Index], function(const InputResults: TAuraNodeResultList): TAuraNodeResult var indexerNode: TAuraNode; basePin, indexPin, outPin: TControl; begin indexerNode := BuildNodeControl('[]', ''); indexerNode.TitleFont.Size := 20; basePin := CreateInput(indexerNode, 'Base'); indexPin := CreateInput(indexerNode, 'Index'); outPin := CreateOutput(indexerNode); if Assigned(InputResults[0].OutputPin) then FConnections.Add(TPinConnection.Create(InputResults[0].OutputPin, basePin)); if Assigned(InputResults[1].OutputPin) then FConnections.Add(TPinConnection.Create(InputResults[1].OutputPin, indexPin)); FinalizeNodeLayout(indexerNode); Result.LayoutNode := indexerNode; Result.OutputPin := outPin; end ); end; Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitLambdaExpression(const Node: ILambdaExpressionNode): TAstValue; var paramStr: String; lambdaNode: TAuraNode; i: Integer; begin // Prepare the parameter string for the title paramStr := '('; if Length(Node.Parameters) > 0 then begin paramStr := paramStr + Node.Parameters[0].Name; for i := 1 to High(Node.Parameters) do paramStr := paramStr + ', ' + Node.Parameters[i].Name; end; paramStr := paramStr + ')'; // Use the helper to create and populate the container node. lambdaNode := VisitContainerNode( #$03BB, paramStr, 'call', 'return', false, true, procedure(const Visitor: IAstVisitor) begin Node.Body.Accept(Visitor); end ); // Update the state of the main visitor FCurrentPos.Y := lambdaNode.Position.Y + lambdaNode.Height + FSpacing.Y; FLastResult.LayoutNode := lambdaNode; FLastResult.OutputPin := CreateOutput(lambdaNode); // Re-finalize layout after adding the data output pin. FinalizeNodeLayout(lambdaNode); Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitMemberAccess(const Node: IMemberAccessNode): TAstValue; var details: String; begin // Implements the visualization for member access (e.g., series.field). if (FMode = vmControlFlow) and TryGetDescr(Node, details) then begin var memberNode := CreateNodeControl('Expression', details); FLastResult.OutputPin := CreateOutput(memberNode); FinalizeNodeLayout(memberNode); end else begin VisitOperatorNode( [Node.Base], function(const InputResults: TAuraNodeResultList): TAuraNodeResult var memberAccessNode: TAuraNode; basePin, outPin: TControl; begin memberAccessNode := BuildNodeControl('Get ' + Node.Member.Name, ''); basePin := CreateInput(memberAccessNode, 'Base'); outPin := CreateOutput(memberAccessNode); if Assigned(InputResults[0].OutputPin) then FConnections.Add(TPinConnection.Create(InputResults[0].OutputPin, basePin)); FinalizeNodeLayout(memberAccessNode); Result.LayoutNode := memberAccessNode; Result.OutputPin := outPin; end ); end; Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitOperatorNode( const InputExpressions: TArray; const CreateParentProc: TCreateParentNodeProc; Alignment: TVerticalAlignment ): TAuraNode; var inputResults: TAuraNodeResultList; inputNode: IAstNode; startPosition: TPointF; endY, maxChildWidth: Single; parentNode: TAuraNode; createResult: TAuraNodeResult; begin startPosition := FCurrentPos; inputResults := TAuraNodeResultList.Create; try // 1. Visit all input nodes and collect their visual representations. for inputNode in InputExpressions do begin inputNode.Accept(Self); inputResults.Add(FLastResult); end; endY := FCurrentPos.Y; // 2. Determine the layout bounds of the input nodes. maxChildWidth := 0; for var res in inputResults do if Assigned(res.LayoutNode) then maxChildWidth := Max(maxChildWidth, res.LayoutNode.Position.X + res.LayoutNode.Width); // 3. Let the closure create the parent node (which also finalizes its layout). createResult := CreateParentProc(inputResults); parentNode := createResult.LayoutNode; Result := parentNode; // 4. Now position the parent node to the right of the inputs. var parentY: Single; if Alignment = vaCenter then begin var totalInputHeight := endY - startPosition.Y; var nodeCenterY := startPosition.Y + (totalInputHeight / 2); parentY := nodeCenterY - (parentNode.Height / 2); end else // vaTop begin parentY := startPosition.Y; end; parentNode.Position.Point := TPointF.Create(maxChildWidth + FSpacing.X, parentY); // 5. Finalize visitor state for the next operation. FCurrentPos.Y := Max(endY, parentNode.Position.Y + parentNode.Height + FSpacing.Y); FLastResult.LayoutNode := parentNode; FLastResult.OutputPin := createResult.OutputPin; finally inputResults.Free; end; end; function TAstToAuraNodeVisitor.VisitSeriesLength(const Node: ISeriesLengthNode): TAstValue; begin // Use the standard helper for operator-style nodes. VisitOperatorNode( [Node.Series], // The single input for this operator is the series identifier. function(const InputResults: TAuraNodeResultList): TAuraNodeResult var lengthNode: TAuraNode; seriesPin, outPin: TControl; begin // Create the visual node for the 'length' operation. lengthNode := BuildNodeControl('Series Length', ''); // Create an input pin for the series and an output pin for the result. seriesPin := CreateInput(lengthNode, 'Series'); outPin := CreateOutput(lengthNode); // Connect the output of the visited series identifier to our input pin. if Assigned(InputResults[0].OutputPin) then FConnections.Add(TPinConnection.Create(InputResults[0].OutputPin, seriesPin)); // Finalize the node's layout to correctly position the pins. FinalizeNodeLayout(lengthNode); // The result of this visitation is the new node and its output pin. Result.LayoutNode := lengthNode; Result.OutputPin := outPin; end ); Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitTernaryExpression(const Node: ITernaryExpressionNode): TAstValue; begin var exprStr: String; if (FMode = vmControlFlow) and TryGetDescr(Node, exprStr) then begin var ternaryNode := CreateNodeControl('Expression', exprStr); FLastResult.OutputPin := CreateOutput(ternaryNode); FinalizeNodeLayout(ternaryNode); end else begin VisitOperatorNode( [Node.Condition, Node.ThenBranch, Node.ElseBranch], function(const InputResults: TAuraNodeResultList): TAuraNodeResult var ternaryNode: TAuraNode; condPin, thenPin, elsePin, outPin: TControl; begin ternaryNode := BuildNodeControl('?', ''); ternaryNode.TitleFont.Size := 20; condPin := CreateInput(ternaryNode, 'Condition'); thenPin := CreateInput(ternaryNode, 'Then'); elsePin := CreateInput(ternaryNode, 'Else'); outPin := CreateOutput(ternaryNode); if Assigned(InputResults[0].OutputPin) then FConnections.Add(TPinConnection.Create(InputResults[0].OutputPin, condPin)); if Assigned(InputResults[1].OutputPin) then FConnections.Add(TPinConnection.Create(InputResults[1].OutputPin, thenPin)); if Assigned(InputResults[2].OutputPin) then FConnections.Add(TPinConnection.Create(InputResults[2].OutputPin, elsePin)); FinalizeNodeLayout(ternaryNode); Result.LayoutNode := ternaryNode; Result.OutputPin := outPin; end ); end; Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitUnaryExpression(const Node: IUnaryExpressionNode): TAstValue; begin var exprStr: String; if (FMode = vmControlFlow) and TryGetDescr(Node, exprStr) then begin var exprNode := CreateNodeControl('Expression', exprStr); FLastResult.OutputPin := CreateOutput(exprNode); FinalizeNodeLayout(exprNode); end else begin VisitOperatorNode( [Node.Right], function(const InputResults: TAuraNodeResultList): TAuraNodeResult var unaryExprNode: TAuraNode; inputPin: TControl; rightResult: TAuraNodeResult; outPin: TControl; begin rightResult := InputResults[0]; unaryExprNode := BuildNodeControl(Node.Operator.ToString, ''); unaryExprNode.TitleFont.Size := 20; inputPin := CreateInput(unaryExprNode, ''); outPin := CreateOutput(unaryExprNode); if Assigned(rightResult.OutputPin) then FConnections.Add(TPinConnection.Create(rightResult.OutputPin, inputPin)); FinalizeNodeLayout(unaryExprNode); Result.LayoutNode := unaryExprNode; Result.OutputPin := outPin; end ); end; Result := TAstValue.Void; end; function TAstToAuraNodeVisitor.VisitVariableDeclaration(const Node: IVariableDeclarationNode): TAstValue; var outPin: TControl; begin var details: String; if (FMode = vmControlFlow) and TryGetDescr(Node, details) then begin var varDeclNode := CreateNodeControl('VarDecl', details); CreateEntry(varDeclNode); CreateExit(varDeclNode, ''); FinalizeNodeLayout(varDeclNode); end else begin if Assigned(Node.Initializer) then begin VisitOperatorNode( [Node.Initializer], function(const InputResults: TAuraNodeResultList): TAuraNodeResult var varDeclNode: TAuraNode; inputPin: TControl; initializerResult: TAuraNodeResult; outPin: TControl; // variable for the output pin begin initializerResult := InputResults[0]; varDeclNode := BuildNodeControl('VarDecl', Node.Identifier.Name); CreateEntry(varDeclNode); inputPin := CreateInput(varDeclNode, 'Value'); CreateExit(varDeclNode, ''); // Create the output pin before finalizing the layout. outPin := CreateOutput(varDeclNode); if Assigned(initializerResult.OutputPin) then FConnections.Add(TPinConnection.Create(initializerResult.OutputPin, inputPin)); // Now finalize, all pins are present. FinalizeNodeLayout(varDeclNode); Result.LayoutNode := varDeclNode; Result.OutputPin := outPin; end ); end else begin // Case without initializer is a simple sequential node. var varDeclNode := CreateNodeControl('VarDecl', Node.Identifier.Name); CreateEntry(varDeclNode); CreateExit(varDeclNode, ''); // Create the output pin before finalizing the layout. outPin := CreateOutput(varDeclNode); FLastResult.OutputPin := outPin; // Now finalize, all pins are present. FinalizeNodeLayout(varDeclNode); end; end; Result := TAstValue.Void; end; end.