$ErrorActionPreference = 'Stop' $inputs = @PSC_INPUT_DIRS@ $excludeDirs = @PSC_EXCLUDE_DIRS@ $outputDir = '@PSC_OUTPUT_DIR@' $format = '@PSC_FORMAT@' $dotExe = '@PSC_DOT_EXECUTABLE@' $maxDepth = @PSC_MAX_DEPTH@ $collapseThreshold = @PSC_COLLAPSE_THRESHOLD@ $excludeThirdParty = @PSC_EXCLUDE_THIRD_PARTY@ New-Item -ItemType Directory -Force -Path $outputDir | Out-Null function Convert-To_Generic_Path([string]$path) { return ([System.IO.Path]::GetFullPath($path)).Replace('\', '/') } function Convert-To_Dot_Text([string]$text) { return $text.Replace('\', '/').Replace('"', '\"') } function Get-Root_Name([string]$root) { $path = Convert-To_Generic_Path $root if ($path -match '/module/radio$') { return 'radio' } if ($path -match '/CPP_Core/Core$') { return 'CPP_Core' } if ($path -match '/YSGraphic_Core$') { return 'YSGraphic_Core' } return [System.IO.Path]::GetFileName($path) } function Get-Relative_Path([string]$root, [string]$path) { $rootPath = Convert-To_Generic_Path $root $fullPath = Convert-To_Generic_Path $path if ($fullPath.StartsWith($rootPath)) { return $fullPath.Substring($rootPath.Length).TrimStart('/') } return $fullPath } function Test-Excluded_Path([string]$path) { $fullPath = Convert-To_Generic_Path $path if ($excludeThirdParty -and $fullPath.Contains('/third_party/')) { return $true } foreach ($excludeDir in $excludeDirs) { if ($excludeDir.Length -eq 0) { continue } $excludePath = Convert-To_Generic_Path $excludeDir if ($fullPath.StartsWith($excludePath)) { return $true } } return $false } function Get-Source_Files([string[]]$roots) { $extensions = @('.h', '.hpp', '.hh', '.cpp', '.cxx', '.cc') $files = [System.Collections.Generic.List[object]]::new() foreach ($root in $roots) { if (!(Test-Path -LiteralPath $root)) { continue } Get-ChildItem -LiteralPath $root -Recurse -File | Where-Object { $extensions -contains $_.Extension.ToLowerInvariant() -and !(Test-Excluded_Path $_.FullName) } | ForEach-Object { $files.Add($_) } } return $files } function Invoke-Dot([string]$name, [System.Collections.Generic.List[string]]$lines) { $dotFile = Join-Path $outputDir ($name + '.dot') $imageFile = Join-Path $outputDir ($name + '.' + $format) Set-Content -LiteralPath $dotFile -Value $lines -Encoding UTF8 & $dotExe ('-T' + $format) $dotFile '-o' $imageFile if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } function Write-Graphviz_Index() { $items = @( 'radio_directory_tree', 'radio_module_dependency', 'radio_cmake_targets', 'radio_include_dependency', 'radio_render_architecture' ) $html = [System.Collections.Generic.List[string]]::new() $html.Add('Radio Graphviz

Radio Graphviz

') Set-Content -LiteralPath (Join-Path $outputDir 'index.html') -Value $html -Encoding UTF8 } function Add-Header([System.Collections.Generic.List[string]]$lines, [string]$title) { $lines.Add('digraph "' + (Convert-To_Dot_Text $title) + '" {') $lines.Add('rankdir=LR;') $lines.Add('splines=ortho;') $lines.Add('concentrate=true;') $lines.Add('forcelabels=true;') $lines.Add('graph [fontname="Consolas", label="' + (Convert-To_Dot_Text $title) + '", labelloc=t, fontsize=18];') $lines.Add('node [shape=box, style="rounded,filled", fontname="Consolas", fontsize=10];') $lines.Add('edge [fontname="Consolas", fontsize=9];') } function Add-Legend([System.Collections.Generic.List[string]]$lines, [string]$mode) { $lines.Add('subgraph cluster_legend {') $lines.Add('label="legend";') $lines.Add('style="rounded,dashed";') $lines.Add('"legend_module" [label="module/folder", fillcolor="#dbeafe", tooltip="聚合模块或目录节点"];') $lines.Add('"legend_target" [label="cmake target", fillcolor="#dcfce7", tooltip="CMake target 节点"];') $lines.Add('"legend_buffer" [label="buffer/cache", fillcolor="#fef3c7", tooltip="缓冲区或长期缓存"];') $lines.Add('"legend_runtime" [label="runtime", fillcolor="#fee2e2", tooltip="运行期任务或 Qt 绘制入口"];') if ($mode -eq 'include') { $lines.Add('"legend_edge" [label="include:N node = include count", fillcolor="#f8fafc"];') } else { $lines.Add('"legend_edge" [label="edge label = relation", fillcolor="#f8fafc"];') } $lines.Add('}') } function Get-Immediate_Source_Count([string]$dir) { $extensions = @('.h', '.hpp', '.hh', '.cpp', '.cxx', '.cc') return @(Get-ChildItem -LiteralPath $dir -File | Where-Object { $extensions -contains $_.Extension.ToLowerInvariant() }).Count } function Get-Visible_Directories([string]$dir) { return @(Get-ChildItem -LiteralPath $dir -Directory | Where-Object { !(Test-Excluded_Path $_.FullName) }) } function Add-Directory_Node([System.Collections.Generic.List[string]]$lines, [string]$root, [string]$dir, [int]$depth, [string]$parentId) { if ($depth -gt $maxDepth -or (Test-Excluded_Path $dir)) { return } $rootName = Get-Root_Name $root $relative = Get-Relative_Path $root $dir $name = if ($relative.Length -eq 0) { $rootName } else { [System.IO.Path]::GetFileName($dir) } $idPath = if ($relative.Length -eq 0) { $rootName } else { $rootName + '/' + $relative } $id = 'dir:' + $idPath $fileCount = Get-Immediate_Source_Count $dir $children = Get-Visible_Directories $dir $label = (Convert-To_Dot_Text $name) + '\nfiles: ' + $fileCount + '\ndirs: ' + $children.Count $tooltip = Convert-To_Dot_Text $idPath $lines.Add('"' + $id + '" [label="' + $label + '", tooltip="' + $tooltip + '", fillcolor="#dbeafe"];') if ($parentId.Length -gt 0) { $lines.Add('"' + $parentId + '" -> "' + $id + '" [xlabel="contains"];') } if ($depth -eq $maxDepth) { return } if ($fileCount -gt $collapseThreshold -and $depth -gt 0) { return } foreach ($child in $children) { Add-Directory_Node $lines $root $child.FullName ($depth + 1) $id } } function New-Directory_Tree_Graph() { $lines = [System.Collections.Generic.List[string]]::new() Add-Header $lines 'radio directory_tree' foreach ($root in $inputs) { if (!(Test-Path -LiteralPath $root)) { continue } $rootName = Get-Root_Name $root $clusterName = $rootName.Replace('-', '_').Replace('/', '_') $lines.Add('subgraph "cluster_' + $clusterName + '" {') $lines.Add('label="' + (Convert-To_Dot_Text $rootName) + '";') $lines.Add('style="rounded";') Add-Directory_Node $lines $root $root 0 '' $lines.Add('}') } Add-Legend $lines 'directory' $lines.Add('}') Invoke-Dot 'radio_directory_tree' $lines } function Get-Root_For_File([string]$path) { $fullPath = Convert-To_Generic_Path $path foreach ($root in $inputs) { $rootPath = Convert-To_Generic_Path $root if ($fullPath.StartsWith($rootPath)) { return $root } } return '' } function Get-Module_Key([string]$path, [int]$depth) { $root = Get-Root_For_File $path if ($root.Length -eq 0) { return '' } $rootName = Get-Root_Name $root $relative = Get-Relative_Path $root $path $parts = @($relative.Split('/') | Where-Object { $_.Length -gt 0 }) if ($rootName -eq 'radio') { $base = 'module/radio' } elseif ($rootName -eq 'CPP_Core') { $base = 'third_party/CPP_Core/Core' } elseif ($rootName -eq 'YSGraphic_Core') { $base = 'third_party/Graphic_Core/YSGraphic_Core' } else { $base = $rootName } if ($parts.Count -eq 0 -or $depth -eq 0) { return $base } $take = [Math]::Min($depth, $parts.Count) return $base + '/' + (($parts | Select-Object -First $take) -join '/') } function Test-Ignored_Include([string]$includeText) { $text = $includeText.Trim() if ($text -match '^(Q[A-ZA-z0-9_]*|Qt[A-ZA-z0-9_/]*|QObject|QWidget|QString|QVector|QMap|QList)$') { return $true } if ($text -match '^(array|atomic|chrono|cmath|cstdint|cstdio|cstdlib|cstring|filesystem|functional|limits|map|memory|mutex|optional|set|span|string|string_view|thread|tuple|type_traits|unordered_map|utility|vector)$') { return $true } if ($text -match '^(boost/|asio|windows\.h|winsock|d3d|GL/)') { return $true } return $false } function Resolve-Include_Path([string]$includeText, [hashtable]$suffixMap, [hashtable]$nameMap) { $key = $includeText.Replace('\', '/').ToLowerInvariant() if ($suffixMap.ContainsKey($key)) { return $suffixMap[$key] } $leaf = [System.IO.Path]::GetFileName($key) if ($nameMap.ContainsKey($leaf)) { return $nameMap[$leaf] } return '' } function Add-Dependency_Graph([string]$name, [string]$title, [int]$sourceDepth, [int]$targetDepth) { $files = Get-Source_Files $inputs $suffixMap = @{} $nameMap = @{} foreach ($file in $files) { $root = Get-Root_For_File $file.FullName $relative = (Get-Relative_Path $root $file.FullName).ToLowerInvariant() $leaf = $file.Name.ToLowerInvariant() $suffixMap[$relative] = $file.FullName if (!$nameMap.ContainsKey($leaf)) { $nameMap[$leaf] = $file.FullName } } $edges = @{} foreach ($file in $files) { $sourceKey = Get-Module_Key $file.FullName $sourceDepth if ($sourceKey.Length -eq 0) { continue } Get-Content -LiteralPath $file.FullName | ForEach-Object { if ($_ -match '^\s*#\s*include\s*[<"]([^">]+)[">]') { $includeText = $Matches[1] if (Test-Ignored_Include $includeText) { return } $targetFile = Resolve-Include_Path $includeText $suffixMap $nameMap if ($targetFile.Length -eq 0) { return } $targetKey = Get-Module_Key $targetFile $targetDepth if ($targetKey.Length -eq 0 -or $targetKey -eq $sourceKey) { return } $edgeKey = $sourceKey + '|' + $targetKey if (!$edges.ContainsKey($edgeKey)) { $edges[$edgeKey] = 0 } $edges[$edgeKey]++ } } } $nodes = [System.Collections.Generic.HashSet[string]]::new() $lines = [System.Collections.Generic.List[string]]::new() Add-Header $lines $title foreach ($edgeKey in $edges.Keys) { $parts = $edgeKey.Split('|') $nodes.Add($parts[0]) | Out-Null $nodes.Add($parts[1]) | Out-Null } foreach ($node in $nodes) { $label = Convert-To_Dot_Text ([System.IO.Path]::GetFileName($node)) $tooltip = Convert-To_Dot_Text $node $lines.Add('"' + $node + '" [label="' + $label + '", tooltip="' + $tooltip + '", fillcolor="#dbeafe"];') } $edgeIndex = 0 foreach ($edgeKey in $edges.Keys) { $parts = $edgeKey.Split('|') $countNode = 'include_count:' + $edgeIndex $lines.Add('"' + $countNode + '" [label="include: ' + $edges[$edgeKey] + '", tooltip="' + (Convert-To_Dot_Text $edgeKey) + '", fillcolor="#f8fafc"];') $lines.Add('"' + $parts[0] + '" -> "' + $countNode + '";') $lines.Add('"' + $countNode + '" -> "' + $parts[1] + '";') $edgeIndex++ } Add-Legend $lines 'include' $lines.Add('}') Invoke-Dot $name $lines } function New-Render_Architecture_Graph() { $lines = [System.Collections.Generic.List[string]]::new() Add-Header $lines 'radio render_architecture' $lines.Add('"Plot" [label="Plot", fillcolor="#dbeafe"];') $lines.Add('"Renderable" [label="Renderable", fillcolor="#dbeafe"];') $lines.Add('"Render_Data" [label="Render_Data", fillcolor="#dcfce7"];') $lines.Add('"Render_State" [label="Render_State", fillcolor="#dcfce7"];') $lines.Add('"Input_Data" [label="Input_Data", fillcolor="#dcfce7"];') $lines.Add('"Render_Pipeline" [label="Render_Pipeline", fillcolor="#dcfce7"];') $lines.Add('"State_Buffer" [label="State_Buffer[3]", fillcolor="#fef3c7"];') $lines.Add('"Input_Buffer" [label="Input_Buffer[3]", fillcolor="#fef3c7"];') $lines.Add('"Render_Cache" [label="Render_Cache", fillcolor="#fef3c7"];') $lines.Add('"Color_Buffer" [label="Color_Buffer[3]", fillcolor="#fef3c7"];') $lines.Add('"CPU_Render_Task" [label="CPU Render Task", fillcolor="#fee2e2"];') $lines.Add('"Qt_paintEvent" [label="Qt paintEvent", fillcolor="#fee2e2"];') $lines.Add('"Plot" -> "Renderable" [xlabel="owns/schedules"];') $lines.Add('"Renderable" -> "Render_Data" [xlabel="owns"];') $lines.Add('"Render_Data" -> "Render_State" [xlabel="state snapshot"];') $lines.Add('"Render_Data" -> "Input_Data" [xlabel="input batch"];') $lines.Add('"Render_Data" -> "Render_Cache" [xlabel="long-lived cache"];') $lines.Add('"Renderable" -> "Render_Pipeline" [xlabel="submit/finish/publish"];') $lines.Add('"Render_Pipeline" -> "State_Buffer" [xlabel="edit/ready/render"];') $lines.Add('"Render_Pipeline" -> "Input_Buffer" [xlabel="write/ready/render"];') $lines.Add('"Render_Pipeline" -> "CPU_Render_Task" [xlabel="dispatch"];') $lines.Add('"CPU_Render_Task" -> "Color_Buffer" [xlabel="write render color"];') $lines.Add('"Color_Buffer" -> "Qt_paintEvent" [xlabel="ready/front consume"];') Add-Legend $lines 'architecture' $lines.Add('}') Invoke-Dot 'radio_render_architecture' $lines } New-Directory_Tree_Graph Add-Dependency_Graph 'radio_module_dependency' 'radio module_dependency' 0 0 Add-Dependency_Graph 'radio_include_dependency' 'radio include_dependency' 1 1 New-Render_Architecture_Graph Write-Graphviz_Index