| 1 | #nullable enable |
| 2 | using CascadeIDE.Models; |
| 3 | |
| 4 | namespace CascadeIDE.Cockpit.Graph.Layout; |
| 5 | |
| 6 | /// <summary>Геометрия и политика укладки control-flow graph-backed surface (ADR 0055).</summary> |
| 7 | public static class GraphControlFlowLayoutMetrics |
| 8 | { |
| 9 | public const double IntrinsicHeightBasePx = 32; |
| 10 | |
| 11 | public static double VerticalSpacingForDetailLevel(CodeNavigationMapDetailLevel detailLevel) => |
| 12 | detailLevel switch |
| 13 | { |
| 14 | CodeNavigationMapDetailLevel.Glance => 30, |
| 15 | CodeNavigationMapDetailLevel.Inspect => 40, |
| 16 | _ => 36, |
| 17 | }; |
| 18 | |
| 19 | public static double EstimatePreferredHeight(int estimatedLevelCount, CodeNavigationMapDetailLevel detailLevel) |
| 20 | { |
| 21 | var spacing = VerticalSpacingForDetailLevel(detailLevel); |
| 22 | var levelBands = Math.Max(1, estimatedLevelCount); |
| 23 | return IntrinsicHeightBasePx + Math.Max(0, levelBands - 1) * spacing; |
| 24 | } |
| 25 | |
| 26 | public const double TopPadding = 12; |
| 27 | public const double BottomPadding = 12; |
| 28 | public const double SidePadding = 12; |
| 29 | public const double LegendGap = 2; |
| 30 | public const double BesideLegendInkSlack = 4; |
| 31 | public const double LegendBesideMinClearance = 6; |
| 32 | public const double LegendSideColumnMinTextWidth = 100; |
| 33 | public const double LegendBelowBlockGap = 6; |
| 34 | public const double MinGraphHeightForBelowLegend = 50; |
| 35 | public const double MinGraphWidth = 40; |
| 36 | public const double MaxReadableBandWidth = 400; |
| 37 | public const double LegendReserveWidthFraction = 0.30; |
| 38 | public const double LegendReserveMin = 88; |
| 39 | public const double LegendReserveHardCap = 340; |
| 40 | |
| 41 | public static double ResolveLegendReserveCap(double viewportWidth) => |
| 42 | Math.Min(LegendReserveHardCap, viewportWidth * 0.52); |
| 43 | |
| 44 | public const double RefVerticalStep = 38; |
| 45 | public const double MaxReadableVerticalStep = 44; |
| 46 | public const double MaxReadableVerticalStepCap = 96; |
| 47 | public const double AnchorRadiusBase = 15.5; |
| 48 | public const double NodeRadiusBase = 13.5; |
| 49 | public const double RadiusScaleMin = 0.4; |
| 50 | public const double RadiusScaleMax = 1.12; |
| 51 | public const double HorizontalRadiusScaleMin = 0.74; |
| 52 | |
| 53 | public static double MinVerticalStepForLevelCount(int levelCount) => |
| 54 | levelCount switch |
| 55 | { |
| 56 | >= 16 => 12, |
| 57 | >= 12 => 14, |
| 58 | >= 9 => 16, |
| 59 | >= 6 => 18, |
| 60 | _ => 20, |
| 61 | }; |
| 62 | |
| 63 | /// <summary>Ось потока: приоритет — уместить шаги по главной оси; иначе соотношение сторон и «длина цепочки vs ширина уровня».</summary> |
| 64 | public static GraphControlFlowMainAxis ChooseMainAxis( |
| 65 | double graphWidth, |
| 66 | double heightForLayout, |
| 67 | int levelCount, |
| 68 | int maxNodesOnAnyLevel) |
| 69 | { |
| 70 | var innerH = heightForLayout - TopPadding - BottomPadding; |
| 71 | var innerW = graphWidth - 2 * SidePadding; |
| 72 | if (innerH < 1 || innerW < 1 || levelCount < 1) |
| 73 | return GraphControlFlowMainAxis.Vertical; |
| 74 | |
| 75 | var slotCount = Math.Max(1, levelCount - 1); |
| 76 | var minStep = MinVerticalStepForLevelCount(levelCount); |
| 77 | var slackMainIfVertical = innerH / slotCount; |
| 78 | var slackMainIfHorizontal = innerW / slotCount; |
| 79 | |
| 80 | if (slackMainIfHorizontal >= minStep && slackMainIfVertical < minStep * 0.92) |
| 81 | return GraphControlFlowMainAxis.Horizontal; |
| 82 | if (slackMainIfVertical >= minStep && slackMainIfHorizontal < minStep * 0.92) |
| 83 | return GraphControlFlowMainAxis.Vertical; |
| 84 | |
| 85 | var aspect = innerW / innerH; |
| 86 | |
| 87 | if (aspect >= 1.18 && slackMainIfHorizontal >= minStep * 0.85) |
| 88 | return GraphControlFlowMainAxis.Horizontal; |
| 89 | if (aspect <= 0.85 && slackMainIfVertical >= minStep * 0.85) |
| 90 | return GraphControlFlowMainAxis.Vertical; |
| 91 | |
| 92 | if (Math.Abs(slackMainIfHorizontal - slackMainIfVertical) > 5) |
| 93 | return slackMainIfHorizontal > slackMainIfVertical |
| 94 | ? GraphControlFlowMainAxis.Horizontal |
| 95 | : GraphControlFlowMainAxis.Vertical; |
| 96 | |
| 97 | var depthHeavy = slotCount > Math.Max(2, maxNodesOnAnyLevel); |
| 98 | if (depthHeavy && slackMainIfHorizontal >= minStep && innerW >= innerH) |
| 99 | return GraphControlFlowMainAxis.Horizontal; |
| 100 | |
| 101 | return innerW > innerH * 1.02 |
| 102 | ? GraphControlFlowMainAxis.Horizontal |
| 103 | : GraphControlFlowMainAxis.Vertical; |
| 104 | } |
| 105 | |
| 106 | /// <summary> |
| 107 | /// Не-null — ось из <c>[code_navigation_map].control_flow_main_axis</c>; <see langword="null"/> для <c>auto</c> (см. <see cref="ChooseMainAxis"/>). |
| 108 | /// </summary> |
| 109 | public static GraphControlFlowMainAxis? TryControlFlowMainAxisOverride(string normalizedSetting) => |
| 110 | normalizedSetting switch |
| 111 | { |
| 112 | CodeNavigationMapControlFlowMainAxisKind.Vertical => GraphControlFlowMainAxis.Vertical, |
| 113 | CodeNavigationMapControlFlowMainAxisKind.Horizontal => GraphControlFlowMainAxis.Horizontal, |
| 114 | _ => null, |
| 115 | }; |
| 116 | |
| 117 | public const int LabelMaxLength = 22; |
| 118 | public const int LabelTruncateLength = 19; |
| 119 | public const int LabelCharBudgetMin = 8; |
| 120 | |
| 121 | public static double ResolveReadableBandWidth(double graphWidth) => |
| 122 | Math.Clamp(graphWidth, MinGraphWidth, MaxReadableBandWidth); |
| 123 | |
| 124 | public static int ResolveLabelCharBudget(double bandWidth) |
| 125 | { |
| 126 | var approx = (int)Math.Floor((bandWidth - 52) / 7.0); |
| 127 | return Math.Clamp(approx, LabelCharBudgetMin, LabelMaxLength); |
| 128 | } |
| 129 | |
| 130 | public static double ResolveSideLabelFontSize(double bandWidth, double verticalStep) |
| 131 | { |
| 132 | var widthFactor = Math.Clamp(bandWidth / 220.0, 0.68, 1.06); |
| 133 | var stepFactor = Math.Clamp(verticalStep / RefVerticalStep, 0.82, 1.08); |
| 134 | var combined = GraphRenderInvariants.MinSideLabelFontSize * widthFactor * stepFactor; |
| 135 | return Math.Clamp( |
| 136 | combined, |
| 137 | GraphRenderInvariants.CompactSideLabelFontSizeFloor, |
| 138 | GraphRenderInvariants.MaxSideLabelFontSize); |
| 139 | } |
| 140 | |
| 141 | public static double EstimateLegendBlockHeight( |
| 142 | int rowCount, |
| 143 | bool hasShapeKeys, |
| 144 | int edgeStyleKeyRowCount = 0, |
| 145 | double captionSize = 11) |
| 146 | { |
| 147 | var lineH = Math.Max(15, captionSize * 1.2); |
| 148 | const double keyRowHBase = 17d; |
| 149 | var keyRowH = Math.Max(keyRowHBase, captionSize + 5); |
| 150 | const double gapBeforeKeys = 6d; |
| 151 | var shapeKeyBlockH = hasShapeKeys ? (keyRowH * 3 + gapBeforeKeys + 8) : 0d; |
| 152 | var edgeStyleBlockH = edgeStyleKeyRowCount > 0 |
| 153 | ? edgeStyleKeyRowCount * keyRowH + 8d |
| 154 | : 0d; |
| 155 | const double betweenEdgeAndShape = 6d; |
| 156 | var betweenBlocks = 0d; |
| 157 | if (edgeStyleKeyRowCount > 0 && hasShapeKeys) |
| 158 | betweenBlocks = betweenEdgeAndShape; |
| 159 | var keyBlockH = edgeStyleBlockH + betweenBlocks + shapeKeyBlockH; |
| 160 | var rowBlock = rowCount * lineH; |
| 161 | var hasAnyKeyBlock = hasShapeKeys || edgeStyleKeyRowCount > 0; |
| 162 | if (rowCount == 0 && hasAnyKeyBlock) |
| 163 | return keyBlockH + 12d; |
| 164 | var between = rowCount > 0 && hasAnyKeyBlock ? gapBeforeKeys : 0d; |
| 165 | return rowBlock + between + keyBlockH + 12d; |
| 166 | } |
| 167 | } |
| 168 | |