$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 GraphvizRadio Graphviz
')
foreach ($item in $items) {
$file = $item + '.' + $format
if (Test-Path -LiteralPath (Join-Path $outputDir $file)) {
$html.Add('- ' + $item + '
')
} else {
$html.Add('- ' + $item + ' (not generated)
')
}
}
$html.Add('
')
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