diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs index cde78b951aa..e4a82f2b823 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs @@ -4513,6 +4513,237 @@ internal bool CanDumpNodeIconData(object obj) return true; } + internal bool CanRecordNodeHelpData(object obj) + { + return true; + } + + /// + /// Records help documentation for the currently selected node. + /// Generates: .md file, _img.jpg screenshot, and .dyn sample graph. + /// Debug-only feature for creating node help documentation. + /// + internal void RecordNodeHelpData(object parameter) + { + // 1. Validate selection - need exactly one node selected + var selectedNodes = DynamoSelection.Instance.Selection.OfType().ToList(); + + if (selectedNodes.Count == 0) + { + ToastManager?.CreateRealTimeInfoWindow("Please select a node to record help data.", true); + return; + } + + if (selectedNodes.Count > 1) + { + ToastManager?.CreateRealTimeInfoWindow("Please select only one node to record help data.", true); + return; + } + + var selectedNode = selectedNodes.First(); + + // 2. Get minimum qualified name for file naming (no hash) + var mqn = GetMinimumQualifiedName(selectedNode); + + // 3. Use node name for display in toast + var nodeName = selectedNode.Name; + + // 4. Get output directory - backtrack from bin\AnyCPU\Debug to doc\distrib\NodeHelpFiles\en-US + DirectoryInfo nodeHelpDocPath; + try + { + // Get the executing assembly location (bin\AnyCPU\Debug) + var assemblyLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + // Backtrack to Dynamo root: go up 3 levels from bin\AnyCPU\Debug + var dynamoRoot = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(assemblyLocation))); + + // Build path to doc\distrib\NodeHelpFiles\en-US + var docPath = Path.Combine(dynamoRoot, "doc", "distrib", "NodeHelpFiles", "en-US"); + nodeHelpDocPath = new DirectoryInfo(docPath); + + // Create directory if it doesn't exist + if (!nodeHelpDocPath.Exists) + { + nodeHelpDocPath.Create(); + } + } + catch (Exception ex) + { + ToastManager?.CreateRealTimeInfoWindow($"Failed to create output directory: {ex.Message}", true); + return; + } + + // 5. Generate file paths using minimum qualified name directly (no hash) + var mdFileName = $"{mqn}.md"; + var imgFileName = $"{mqn}_img.jpg"; + var dynFileName = $"{mqn}.dyn"; + + var mdFilePath = Path.Combine(nodeHelpDocPath.FullName, mdFileName); + var imgFilePath = Path.Combine(nodeHelpDocPath.FullName, imgFileName); + var dynFilePath = Path.Combine(nodeHelpDocPath.FullName, dynFileName); + + try + { + // 6. Export workspace image with 3D background overlay + ExportWorkspaceImageWithBackground(imgFilePath); + + // 7. Save sample graph (.dyn) + SaveAs(dynFilePath, SaveContext.Save, false); + + // 8. Generate markdown file + GenerateNodeHelpMarkdown(selectedNode, mqn, mdFilePath, imgFileName); + + // 9. Show success toast with node name + var message = $"Node help files created for '{nodeName}':\n" + + $" {mdFileName}\n" + + $" {imgFileName}\n" + + $" {dynFileName}\n" + + $"Location: {nodeHelpDocPath.FullName}"; + ToastManager?.CreateRealTimeInfoWindow(message, true); + } + catch (Exception ex) + { + ToastManager?.CreateRealTimeInfoWindow($"Error recording node help data: {ex.Message}", true); + } + } + + /// + /// Exports workspace image with 3D background overlay. + /// Combines the 3D preview background with the workspace graph view. + /// + private void ExportWorkspaceImageWithBackground(string outputPath) + { + var tempDir = Path.GetTempPath(); + var backgroundPath = Path.Combine(tempDir, $"bg_{Guid.NewGuid()}.png"); + var foregroundPath = Path.Combine(tempDir, $"fg_{Guid.NewGuid()}.png"); + + try + { + // Export 3D background + BackgroundPreviewViewModel?.ZoomToFitCommand?.Execute(null); + var backgroundImageArgs = new ImageSaveEventArgs(backgroundPath); + OnRequestSave3DImage(this, backgroundImageArgs); + + // Export workspace (graph view) + var foregroundImageArgs = new ImageSaveEventArgs(foregroundPath); + OnRequestSaveImage(this, foregroundImageArgs); + + // Combine images if both exist + if (File.Exists(backgroundPath) && File.Exists(foregroundPath)) + { + using (var combined = OverlayWorkspaceImages(backgroundPath, foregroundPath)) + { + if (combined != null) + { + combined.Save(outputPath, System.Drawing.Imaging.ImageFormat.Jpeg); + } + else + { + // Fallback: just copy the workspace image + File.Copy(foregroundPath, outputPath, true); + } + } + } + else if (File.Exists(foregroundPath)) + { + // Fallback: just use workspace image if 3D background not available + File.Copy(foregroundPath, outputPath, true); + } + } + finally + { + // Clean up temp files + try { if (File.Exists(backgroundPath)) File.Delete(backgroundPath); } catch { } + try { if (File.Exists(foregroundPath)) File.Delete(foregroundPath); } catch { } + } + } + + /// + /// Overlay workspace graph over 3D background. + /// + private static System.Drawing.Bitmap OverlayWorkspaceImages(string backgroundPath, string foregroundPath) + { + try + { + using (var baseImage = (System.Drawing.Bitmap)System.Drawing.Image.FromFile(backgroundPath)) + using (var overlayImage = (System.Drawing.Bitmap)System.Drawing.Image.FromFile(foregroundPath)) + { + // Calculate scale to fit overlay properly + var scale = 1.5; + var scaleFactor = Math.Max( + overlayImage.Width / (float)baseImage.Width, + overlayImage.Height / (float)baseImage.Height); + var newWidth = (int)(baseImage.Width * scaleFactor * scale); + var newHeight = (int)(baseImage.Height * scaleFactor * scale); + + var finalImage = new System.Drawing.Bitmap(newWidth, newHeight, + System.Drawing.Imaging.PixelFormat.Format32bppArgb); + + using (var graphics = System.Drawing.Graphics.FromImage(finalImage)) + { + graphics.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceOver; + graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; + + // Dynamo-white background + graphics.Clear(System.Drawing.Color.FromArgb(249, 249, 249)); + + const float BACKGROUND_VERTICAL_OFFSET_RATIO = 0.15f; + const float OVERLAY_VERTICAL_POSITION_RATIO = 0.25f; + + // Draw resized background slightly down (15% from top) + using (var resizedBg = new System.Drawing.Bitmap(baseImage, newWidth, newHeight)) + { + graphics.DrawImage(resizedBg, 0, (int)(newHeight * BACKGROUND_VERTICAL_OFFSET_RATIO)); + } + + // Draw workspace overlay in upper center + var offsetX = (newWidth - overlayImage.Width) / 2; + var offsetY = (int)((newHeight - overlayImage.Height) * OVERLAY_VERTICAL_POSITION_RATIO); + graphics.DrawImage(overlayImage, offsetX, offsetY); + } + + return finalImage; + } + } + catch + { + return null; + } + } + + /// + /// Generates the markdown help file for a node. + /// + /// The node model instance for which the help markdown is generated. + /// The fully qualified node name used in the generated markdown header. + /// The full file path where the markdown help file will be written. + /// The image file name referenced from the markdown example section. + private void GenerateNodeHelpMarkdown(NodeModel node, string mqn, + string outputPath, string imgFileName) + { + var sb = new System.Text.StringBuilder(); + + // Add HTML comment header with node name (pattern from existing help files) + sb.AppendLine($""); + + // In Depth section with node description + sb.AppendLine("## In Depth"); + var description = !string.IsNullOrWhiteSpace(node.Description) + ? node.Description + : $"{node.Name} - [Add description here]"; + sb.AppendLine(description); + sb.AppendLine(); + + // Example file section with image + sb.AppendLine("___"); + sb.AppendLine("## Example File"); + sb.AppendLine(); + sb.AppendLine($"![{node.Name}](./{imgFileName})"); + + File.WriteAllText(outputPath, sb.ToString()); + } #region "Home And End key press events in Canvas" /// /// Enable the shortcut key events when there is no selection. diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelDelegateCommands.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelDelegateCommands.cs index 5c7c744ab03..4849412c4af 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelDelegateCommands.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelDelegateCommands.cs @@ -95,6 +95,7 @@ private void InitializeDelegateCommands() SetNumberFormatCommand = new DelegateCommand(SetNumberFormat, CanSetNumberFormat); DumpNodeHelpDataCommand = new DelegateCommand(DumpNodeHelpData, CanDumpNodeHelpData); DumpNodeIconsCommand = new DelegateCommand(DumpNodeIconData, CanDumpNodeIconData); + RecordNodeHelpDataCommand = new DelegateCommand(RecordNodeHelpData, CanRecordNodeHelpData); DumpLibraryToXmlCommand = new DelegateCommand(model.DumpLibraryToXml, model.CanDumpLibraryToXml); ShowNewPresetsDialogCommand = new DelegateCommand(ShowNewPresetStateDialogAndMakePreset, CanShowNewPresetStateDialog); NodeFromSelectionCommand = new DelegateCommand(CreateNodeFromSelection, CanCreateNodeFromSelection); @@ -184,6 +185,7 @@ private void InitializeDelegateCommands() public DelegateCommand SetNumberFormatCommand { get; set; } public DelegateCommand OpenRecentCommand { get; set; } public DelegateCommand CheckForLatestRenderCommand { get; set; } + public DelegateCommand RecordNodeHelpDataCommand { get; set; } public DelegateCommand DumpLibraryToXmlCommand { get; set; } public DelegateCommand DumpNodeHelpDataCommand { get; set; } public DelegateCommand DumpNodeIconsCommand { get; set; } diff --git a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml index 4aeb02b752c..1d288060bdc 100644 --- a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml +++ b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml @@ -696,6 +696,9 @@ +