@markdy/core 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +8 -1
- package/dist/index.js +448 -211
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -85,6 +85,8 @@ type SceneMeta = {
|
|
|
85
85
|
explicitHeight?: boolean;
|
|
86
86
|
/** Whether theme was explicitly specified by the author in the script. */
|
|
87
87
|
explicitTheme?: boolean;
|
|
88
|
+
/** Whether layout direction was explicitly specified by the author in the script. */
|
|
89
|
+
explicitDirection?: boolean;
|
|
88
90
|
/** Opt-in diagram mode; defaults to architecture. */
|
|
89
91
|
type?: DiagramType;
|
|
90
92
|
/** Playback, controls, interaction, and chrome behavior. Source of truth. */
|
|
@@ -376,6 +378,10 @@ type RenderPlan = {
|
|
|
376
378
|
duration: number;
|
|
377
379
|
};
|
|
378
380
|
|
|
381
|
+
declare function computeNodeDimensions(decl: NodeDecl, baseW?: number, baseH?: number): {
|
|
382
|
+
width: number;
|
|
383
|
+
height: number;
|
|
384
|
+
};
|
|
379
385
|
/**
|
|
380
386
|
* Computes optimal, content-adaptive canvas width and height for a diagram AST
|
|
381
387
|
* when dimensions are omitted or partially specified in the script.
|
|
@@ -434,6 +440,7 @@ type PlayerOverrides = {
|
|
|
434
440
|
playback?: boolean;
|
|
435
441
|
});
|
|
436
442
|
interactiveViewport?: boolean;
|
|
443
|
+
clickToPlay?: boolean;
|
|
437
444
|
progress?: PlayerProgress;
|
|
438
445
|
progressColor?: string;
|
|
439
446
|
};
|
|
@@ -1131,4 +1138,4 @@ interface AutoHealResult {
|
|
|
1131
1138
|
*/
|
|
1132
1139
|
declare function autoHealArchitectureDrift(ast: DiagramAST, report: ArchitectureDriftReport, existingFiles?: string[]): AutoHealResult;
|
|
1133
1140
|
|
|
1134
|
-
export { ARCHITECTURE_RECIPES, ARCH_RULE_PRESETS, type AnnotationDecl, type ArchRuleType, type ArchitectureDriftReport, type ArchitecturePreset, type ArchitectureRecipe, type ArchitectureRecommendation, type ArchitectureRule, type ArchitectureViolation, type AutoHealResult, type AutoRepairResult, BEAT_CUE_KEYWORDS, type BeatDecl, type BeatRange, type Box, type BrokenAnchorDrift, type C4Level, type C4LevelViewExport, type C4ModelReport, type C4NodeMeta, CUE_ALIASES, type CardinalPort, type CodeProvenanceAnchor, type CodeProvenanceDiagnostic, type CodeProvenanceVerificationReport, type Cue, DIAGRAM_TYPES, type Diagnostic, type DiagnosticIssue, type DiagramAST, type DiagramContext, type DiagramDiffResult, type DiagramQualityMetrics, type DiagramType, type DiffChangeType, EDGE_OPERATORS, type EdgeDecl, type EdgeDiff, type EdgeKind, type EdgeSelector, type ExtractedBeat, type ExtractedGroup, type ExtractedNode, type FlowSegment, type GhostTextSuggestion, type GroupBoundary, type GroupDecl, type GroupDiff, type IntelliCodeItem, type IntelliCodeItemKind, type LayoutDirection, type MarkdyConfig, NODE_ALIASES, NODE_KINDS, type NodeDecl, type NodeDiff, type NodeSelector, type NodeShape, OUTPUT_PRESETS, type OrphanCodeServiceDrift, type OutputPreset, PLAYER_FLAT_KEYS, PLAYER_GROUPS, POPULAR_TECHS, ParseError, type ParseOptions, type ParseResult, type PatternDecl, type PatternRecommendationResult, type PlayerChromeConfig, type PlayerConfig, type PlayerControlsConfig, type PlayerInteractionConfig, type PlayerOverrides, type PlayerPlaybackConfig, type PlayerProgress, type PlayerScope, type Point, type PortLane, type PositionedNode, type QualityCheckItem, type QualityGateOptions, type QualityGateReport, type QualityProfile, type RenderPlan, type RepairPromptBundle, type ResolvedPlayer, type RouteOptions, type RoutedEdge, type RoutedPath, type RuleSeverity, SCENE_KEYS, type SceneMeta, type SemanticProfile, type SequenceActivation, type SequenceMessage, type StyleDecl, type SyntaxDiagnosticReport, type SynthesizedRecipeResult, TECHNICAL_NODE_KINDS, TECHNICAL_NODE_TYPES, THEMES, type TechPreset, type ThemeGeneratorOptions, type ThemeTokens, type TimedCue, type TreeBus, VECTOR_SYMBOLS, VISUAL_PRIMITIVE_TYPES, type VectorSymbol, allocatePortLanes, analyzeAndBuildRepairPrompt, analyzeC4Model, applyPlayerSetting, autoHealArchitectureDrift, buildSmoothSvgPath, canonicalNodeKind, classifyTechnology, compile, compilePlan, compressMarkdyToUrlHash, computeAdaptiveDimensions, damerauLevenshteinDistance, decompressMarkdyFromUrlHash, detectArchitectureDrift, diagnoseMarkdyCode, diffDiagramASTs, exportC4LevelViews, extractDiagramCodeAnchors, extractDiagramContext, filterC4Hierarchy, findClosestMatch, formatScene, generateC4Storyboard, generateThemeFromBrand, getArchitectureRecipe, getArchitectureSuggestions, getBoxPortPosition, getIntelliCodeCompletions, humanizeId, inferNodeC4Level, listArchitectureRecipes, listAvailableSymbols, listOutputPresets, nodeRole, parse, parseAndCompile, parseCodeAnchor, predictNextLineSuggestion, recommendArchitecturePattern, renderSymbolSvg, repairMarkdyCode, resolveArchitectureConfig, resolveOutputPreset, resolvePlayer, resolveTheme, resolveVectorSymbol, routeOrthogonalEdge, selectOptimalPorts, synthesizeCustomRecipe, validateArchitecture, validateC4Containment, verifyCodeAnchorsWithReader, verifyDiagramQuality };
|
|
1141
|
+
export { ARCHITECTURE_RECIPES, ARCH_RULE_PRESETS, type AnnotationDecl, type ArchRuleType, type ArchitectureDriftReport, type ArchitecturePreset, type ArchitectureRecipe, type ArchitectureRecommendation, type ArchitectureRule, type ArchitectureViolation, type AutoHealResult, type AutoRepairResult, BEAT_CUE_KEYWORDS, type BeatDecl, type BeatRange, type Box, type BrokenAnchorDrift, type C4Level, type C4LevelViewExport, type C4ModelReport, type C4NodeMeta, CUE_ALIASES, type CardinalPort, type CodeProvenanceAnchor, type CodeProvenanceDiagnostic, type CodeProvenanceVerificationReport, type Cue, DIAGRAM_TYPES, type Diagnostic, type DiagnosticIssue, type DiagramAST, type DiagramContext, type DiagramDiffResult, type DiagramQualityMetrics, type DiagramType, type DiffChangeType, EDGE_OPERATORS, type EdgeDecl, type EdgeDiff, type EdgeKind, type EdgeSelector, type ExtractedBeat, type ExtractedGroup, type ExtractedNode, type FlowSegment, type GhostTextSuggestion, type GroupBoundary, type GroupDecl, type GroupDiff, type IntelliCodeItem, type IntelliCodeItemKind, type LayoutDirection, type MarkdyConfig, NODE_ALIASES, NODE_KINDS, type NodeDecl, type NodeDiff, type NodeSelector, type NodeShape, OUTPUT_PRESETS, type OrphanCodeServiceDrift, type OutputPreset, PLAYER_FLAT_KEYS, PLAYER_GROUPS, POPULAR_TECHS, ParseError, type ParseOptions, type ParseResult, type PatternDecl, type PatternRecommendationResult, type PlayerChromeConfig, type PlayerConfig, type PlayerControlsConfig, type PlayerInteractionConfig, type PlayerOverrides, type PlayerPlaybackConfig, type PlayerProgress, type PlayerScope, type Point, type PortLane, type PositionedNode, type QualityCheckItem, type QualityGateOptions, type QualityGateReport, type QualityProfile, type RenderPlan, type RepairPromptBundle, type ResolvedPlayer, type RouteOptions, type RoutedEdge, type RoutedPath, type RuleSeverity, SCENE_KEYS, type SceneMeta, type SemanticProfile, type SequenceActivation, type SequenceMessage, type StyleDecl, type SyntaxDiagnosticReport, type SynthesizedRecipeResult, TECHNICAL_NODE_KINDS, TECHNICAL_NODE_TYPES, THEMES, type TechPreset, type ThemeGeneratorOptions, type ThemeTokens, type TimedCue, type TreeBus, VECTOR_SYMBOLS, VISUAL_PRIMITIVE_TYPES, type VectorSymbol, allocatePortLanes, analyzeAndBuildRepairPrompt, analyzeC4Model, applyPlayerSetting, autoHealArchitectureDrift, buildSmoothSvgPath, canonicalNodeKind, classifyTechnology, compile, compilePlan, compressMarkdyToUrlHash, computeAdaptiveDimensions, computeNodeDimensions, damerauLevenshteinDistance, decompressMarkdyFromUrlHash, detectArchitectureDrift, diagnoseMarkdyCode, diffDiagramASTs, exportC4LevelViews, extractDiagramCodeAnchors, extractDiagramContext, filterC4Hierarchy, findClosestMatch, formatScene, generateC4Storyboard, generateThemeFromBrand, getArchitectureRecipe, getArchitectureSuggestions, getBoxPortPosition, getIntelliCodeCompletions, humanizeId, inferNodeC4Level, listArchitectureRecipes, listAvailableSymbols, listOutputPresets, nodeRole, parse, parseAndCompile, parseCodeAnchor, predictNextLineSuggestion, recommendArchitecturePattern, renderSymbolSvg, repairMarkdyCode, resolveArchitectureConfig, resolveOutputPreset, resolvePlayer, resolveTheme, resolveVectorSymbol, routeOrthogonalEdge, selectOptimalPorts, synthesizeCustomRecipe, validateArchitecture, validateC4Containment, verifyCodeAnchorsWithReader, verifyDiagramQuality };
|
package/dist/index.js
CHANGED
|
@@ -673,7 +673,8 @@ function resolvePlayer(config = {}, overrides = {}) {
|
|
|
673
673
|
const controls = {
|
|
674
674
|
...requestedControls,
|
|
675
675
|
speed: requestedControls.speed && speeds.length > 1,
|
|
676
|
-
resetView: requestedControls.resetView && interactionEnabled
|
|
676
|
+
resetView: requestedControls.resetView && interactionEnabled,
|
|
677
|
+
fit: requestedControls.fit
|
|
677
678
|
};
|
|
678
679
|
const controlsEnabled = Object.values(controls).some(Boolean);
|
|
679
680
|
const rate = overrides.playbackRate ?? playback.rate ?? 1;
|
|
@@ -691,8 +692,8 @@ function resolvePlayer(config = {}, overrides = {}) {
|
|
|
691
692
|
interaction: {
|
|
692
693
|
...gestures,
|
|
693
694
|
enabled: interactionEnabled,
|
|
694
|
-
clickToPlay: interaction.clickToPlay ?? true,
|
|
695
|
-
keyboard: interaction.keyboard ?? false
|
|
695
|
+
clickToPlay: overrides.clickToPlay ?? (interactionOn && (interaction.clickToPlay ?? true)),
|
|
696
|
+
keyboard: interactionOn && (interaction.keyboard ?? false)
|
|
696
697
|
},
|
|
697
698
|
chrome: {
|
|
698
699
|
badge: overrides.copyright ?? chrome.badge ?? true,
|
|
@@ -757,6 +758,7 @@ var SCENE_KEYS = /* @__PURE__ */ new Set([
|
|
|
757
758
|
"duration",
|
|
758
759
|
"direction",
|
|
759
760
|
"layout",
|
|
761
|
+
"rankdir",
|
|
760
762
|
"type",
|
|
761
763
|
...PLAYER_FLAT_KEYS
|
|
762
764
|
]);
|
|
@@ -804,35 +806,123 @@ var NODE_W = 180;
|
|
|
804
806
|
var NODE_H = 76;
|
|
805
807
|
var VENN_NODE_SIZE = 220;
|
|
806
808
|
var GROUP_PAD = 28;
|
|
807
|
-
function
|
|
808
|
-
if (
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
809
|
+
function estimateTextLines(words, charsPerLine) {
|
|
810
|
+
if (words.length === 0) return 0;
|
|
811
|
+
let lines = 1;
|
|
812
|
+
let curLen = 0;
|
|
813
|
+
for (const w of words) {
|
|
814
|
+
if (curLen === 0) {
|
|
815
|
+
curLen = w.length;
|
|
816
|
+
} else if (curLen + 1 + w.length <= charsPerLine) {
|
|
817
|
+
curLen += 1 + w.length;
|
|
818
|
+
} else {
|
|
819
|
+
lines++;
|
|
820
|
+
curLen = w.length;
|
|
821
|
+
}
|
|
813
822
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
823
|
+
return lines;
|
|
824
|
+
}
|
|
825
|
+
function measureLabelLines(rawLabel, words, charsPerLine) {
|
|
826
|
+
if (words.length === 0) return 0;
|
|
827
|
+
if (!rawLabel.includes("\n")) {
|
|
828
|
+
return Math.max(1, estimateTextLines(words, charsPerLine));
|
|
829
|
+
}
|
|
830
|
+
const explicitLines = rawLabel.split("\n");
|
|
831
|
+
let total = 0;
|
|
832
|
+
for (const line of explicitLines) {
|
|
833
|
+
const lineWords = line.trim().split(/\s+/).filter(Boolean);
|
|
834
|
+
total += Math.max(1, estimateTextLines(lineWords, charsPerLine));
|
|
835
|
+
}
|
|
836
|
+
return Math.max(explicitLines.length, total);
|
|
837
|
+
}
|
|
838
|
+
function computeNodeDimensions(decl, baseW = 180, baseH = 76) {
|
|
819
839
|
if (decl.kind === "dot") return { width: 64, height: 64 };
|
|
820
840
|
if (decl.kind === "matrix") return { width: 220, height: 96 };
|
|
821
|
-
const
|
|
822
|
-
const
|
|
841
|
+
const rawLabel = (decl.label || "").trim();
|
|
842
|
+
const labelLen = rawLabel.length;
|
|
843
|
+
const words = rawLabel.split(/\s+/).filter(Boolean);
|
|
823
844
|
const longestWord = Math.max(...words.map((w) => w.length), 0);
|
|
824
|
-
const
|
|
825
|
-
const
|
|
826
|
-
const
|
|
827
|
-
const
|
|
828
|
-
const
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
845
|
+
const tech = decl.props?.tech ?? decl.props?.sub;
|
|
846
|
+
const techStr = tech ? String(tech).trim() : "";
|
|
847
|
+
const techLen = techStr.length;
|
|
848
|
+
const val = decl.props?.value ?? decl.props?.metric;
|
|
849
|
+
const valStr = val ? String(val).trim() : "";
|
|
850
|
+
const valLen = valStr.length;
|
|
851
|
+
const leftSpace = 56;
|
|
852
|
+
const valueW = valLen > 0 ? Math.max(50, valLen * 9.5 + 16) : 0;
|
|
853
|
+
const rightSpace = valueW > 0 ? valueW + 14 : 18;
|
|
854
|
+
const isDiamond = decl.props?.shape === "diamond" || decl.kind === "decision" || decl.kind === "condition";
|
|
855
|
+
const isCircle = decl.props?.shape === "circle";
|
|
856
|
+
const isPill = decl.props?.shape === "pill";
|
|
857
|
+
const effectiveBaseW = isDiamond ? Math.max(baseW, 208) : isCircle ? Math.max(baseW, 140) : baseW;
|
|
858
|
+
const effectiveBaseH = isDiamond ? Math.max(baseH, 88) : isCircle ? Math.max(baseH, 140) : baseH;
|
|
859
|
+
const neededTechW = techLen > 0 ? Math.ceil(techLen * 6.6 + 16) : 0;
|
|
860
|
+
const longestWordW = Math.ceil(longestWord * 8.4 + 4);
|
|
861
|
+
const minColW = Math.max(96, longestWordW, neededTechW);
|
|
862
|
+
if (isCircle) {
|
|
863
|
+
const targetTextW2 = Math.max(minColW, Math.min(220, Math.ceil(labelLen * 7.5)));
|
|
864
|
+
const charsPerLine2 = Math.max(longestWord, Math.floor(targetTextW2 / 8));
|
|
865
|
+
const numLines2 = Math.max(words.length > 0 ? 1 : 0, measureLabelLines(rawLabel, words, charsPerLine2));
|
|
866
|
+
const labelH2 = numLines2 * 16;
|
|
867
|
+
const techH2 = techLen > 0 ? 20 : 0;
|
|
868
|
+
const circleContentH = 28 + labelH2 + techH2;
|
|
869
|
+
const circleContentW = Math.max(targetTextW2, 60);
|
|
870
|
+
const radiusNeeded = Math.sqrt((circleContentW / 2) ** 2 + (circleContentH / 2) ** 2) + 20;
|
|
871
|
+
const diameter = Math.max(effectiveBaseW, effectiveBaseH, Math.ceil(radiusNeeded * 2 / 8) * 8);
|
|
872
|
+
return {
|
|
873
|
+
width: typeof decl.props?.width === "number" ? Math.max(decl.props.width, diameter) : diameter,
|
|
874
|
+
height: typeof decl.props?.height === "number" ? Math.max(decl.props.height, diameter) : diameter
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
if (isDiamond) {
|
|
878
|
+
let targetTextW2;
|
|
879
|
+
if (labelLen <= 12) {
|
|
880
|
+
targetTextW2 = Math.max(minColW, Math.ceil(labelLen * 8));
|
|
881
|
+
} else if (labelLen <= 28) {
|
|
882
|
+
targetTextW2 = Math.max(minColW, Math.min(180, Math.ceil(labelLen / 2 * 8.2 + 8)));
|
|
883
|
+
} else {
|
|
884
|
+
targetTextW2 = Math.max(minColW, Math.min(260, Math.ceil(labelLen / 3 * 8.2 + 12)));
|
|
885
|
+
}
|
|
886
|
+
const charsPerLine2 = Math.max(longestWord, Math.floor(targetTextW2 / 8));
|
|
887
|
+
const numLines2 = Math.max(words.length > 0 ? 1 : 0, measureLabelLines(rawLabel, words, charsPerLine2));
|
|
888
|
+
const labelH2 = numLines2 * 16;
|
|
889
|
+
const techH2 = techLen > 0 ? 20 : 0;
|
|
890
|
+
const diamondContentH = 24 + labelH2 + techH2;
|
|
891
|
+
const neededH2 = Math.max(effectiveBaseH, Math.ceil(diamondContentH / 0.58 / 8) * 8);
|
|
892
|
+
const neededW = Math.max(effectiveBaseW, Math.ceil((targetTextW2 + 32) / 0.52 / 8) * 8);
|
|
893
|
+
return {
|
|
894
|
+
width: typeof decl.props?.width === "number" ? Math.max(decl.props.width, neededW) : neededW,
|
|
895
|
+
height: typeof decl.props?.height === "number" ? Math.max(decl.props.height, neededH2) : neededH2
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
let targetTextW;
|
|
899
|
+
if (labelLen <= 14) {
|
|
900
|
+
targetTextW = Math.max(minColW, Math.ceil(labelLen * 8.4));
|
|
901
|
+
} else if (labelLen <= 30) {
|
|
902
|
+
targetTextW = Math.max(minColW, Math.min(220, Math.ceil(labelLen * 8.4)));
|
|
903
|
+
} else if (labelLen <= 54) {
|
|
904
|
+
targetTextW = Math.max(minColW, Math.min(260, Math.max(180, Math.ceil(labelLen / 2 * 8.4 + 8))));
|
|
905
|
+
} else if (labelLen <= 84) {
|
|
906
|
+
targetTextW = Math.max(minColW, Math.min(300, Math.max(220, Math.ceil(labelLen / 3 * 8.4 + 12))));
|
|
907
|
+
} else {
|
|
908
|
+
targetTextW = Math.max(minColW, Math.min(360, Math.max(250, Math.ceil(labelLen / 4 * 8.4 + 16))));
|
|
909
|
+
}
|
|
910
|
+
const charsPerLine = Math.max(longestWord, Math.floor((targetTextW - 4) / 8.2));
|
|
911
|
+
const numLines = Math.max(words.length > 0 ? 1 : 0, measureLabelLines(rawLabel, words, charsPerLine));
|
|
912
|
+
const labelLineH = 18;
|
|
913
|
+
const labelH = numLines * labelLineH;
|
|
914
|
+
const techH = techLen > 0 ? 22 : 0;
|
|
915
|
+
const padV = 28;
|
|
916
|
+
const neededContentH = Math.max(28, labelH + techH);
|
|
917
|
+
const neededH = neededContentH + padV;
|
|
918
|
+
const height = Math.max(effectiveBaseH, Math.ceil(neededH / 8) * 8);
|
|
919
|
+
const pillPad = isPill ? 24 : 0;
|
|
920
|
+
const calculatedW = leftSpace + targetTextW + rightSpace + pillPad;
|
|
921
|
+
const width = Math.max(effectiveBaseW, Math.ceil(calculatedW / 8) * 8);
|
|
922
|
+
return {
|
|
923
|
+
width: typeof decl.props?.width === "number" ? Math.max(decl.props.width, width) : width,
|
|
924
|
+
height: typeof decl.props?.height === "number" ? Math.max(decl.props.height, height) : height
|
|
925
|
+
};
|
|
836
926
|
}
|
|
837
927
|
function resolveNodeStyle(decl, ast) {
|
|
838
928
|
if (!decl.style) return void 0;
|
|
@@ -853,6 +943,9 @@ var DEFAULTS = {
|
|
|
853
943
|
function diagramType(ast) {
|
|
854
944
|
return ast.meta.type ?? "architecture";
|
|
855
945
|
}
|
|
946
|
+
function effectiveTitleBand(ast) {
|
|
947
|
+
return ast.meta.title && ast.meta.title.trim().length > 0 ? TITLE_BAND : 16;
|
|
948
|
+
}
|
|
856
949
|
function nodeShape(kind, dtype, props) {
|
|
857
950
|
if (typeof props?.shape === "string") {
|
|
858
951
|
const s = props.shape.toLowerCase();
|
|
@@ -1127,6 +1220,11 @@ function layoutRanked(ast, edges, opts) {
|
|
|
1127
1220
|
function layoutTree(ast, edges) {
|
|
1128
1221
|
const nodeIds = Object.keys(ast.nodes);
|
|
1129
1222
|
if (nodeIds.length === 0) return [];
|
|
1223
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1224
|
+
for (const id of nodeIds) {
|
|
1225
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1226
|
+
}
|
|
1227
|
+
const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height), NODE_H);
|
|
1130
1228
|
const children = /* @__PURE__ */ new Map();
|
|
1131
1229
|
const parent = /* @__PURE__ */ new Map();
|
|
1132
1230
|
for (const id of nodeIds) children.set(id, []);
|
|
@@ -1155,18 +1253,22 @@ function layoutTree(ast, edges) {
|
|
|
1155
1253
|
for (const id of nodeIds) if (!depth.has(id)) depth.set(id, 0);
|
|
1156
1254
|
const maxDepth = Math.max(...depth.values(), 0);
|
|
1157
1255
|
const subtreeSpan = /* @__PURE__ */ new Map();
|
|
1158
|
-
|
|
1256
|
+
function leafSpan(id) {
|
|
1257
|
+
const w = nodeDims.get(id)?.width ?? NODE_W;
|
|
1258
|
+
return w + 36;
|
|
1259
|
+
}
|
|
1159
1260
|
function measureSubtree(id) {
|
|
1160
1261
|
const kids = children.get(id) ?? [];
|
|
1262
|
+
const minSpan = leafSpan(id);
|
|
1161
1263
|
if (kids.length === 0) {
|
|
1162
|
-
subtreeSpan.set(id,
|
|
1163
|
-
return
|
|
1264
|
+
subtreeSpan.set(id, minSpan);
|
|
1265
|
+
return minSpan;
|
|
1164
1266
|
}
|
|
1165
1267
|
let total = 0;
|
|
1166
1268
|
for (const kid of kids) {
|
|
1167
1269
|
total += measureSubtree(kid);
|
|
1168
1270
|
}
|
|
1169
|
-
const span = Math.max(
|
|
1271
|
+
const span = Math.max(minSpan, total);
|
|
1170
1272
|
subtreeSpan.set(id, span);
|
|
1171
1273
|
return span;
|
|
1172
1274
|
}
|
|
@@ -1177,34 +1279,37 @@ function layoutTree(ast, edges) {
|
|
|
1177
1279
|
const contentW = ast.meta.width - SAFE * 2;
|
|
1178
1280
|
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
1179
1281
|
const treeStartX = SAFE + Math.max(0, (contentW - totalRootsWidth) / 2);
|
|
1180
|
-
const levelHeight = Math.max(
|
|
1181
|
-
const totalTreeHeight = maxDepth * levelHeight +
|
|
1282
|
+
const levelHeight = Math.max(maxNodeH + 40, Math.min(220, contentH / Math.max(maxDepth + 1, 1)));
|
|
1283
|
+
const totalTreeHeight = maxDepth * levelHeight + maxNodeH;
|
|
1182
1284
|
const treeStartY = TITLE_BAND + Math.max(16, (contentH - totalTreeHeight) / 2);
|
|
1183
1285
|
const nodePositions = /* @__PURE__ */ new Map();
|
|
1184
1286
|
function positionSubtree(id, leftX) {
|
|
1185
1287
|
const d = depth.get(id) ?? 0;
|
|
1186
1288
|
const y = treeStartY + d * levelHeight;
|
|
1187
1289
|
const kids = children.get(id) ?? [];
|
|
1188
|
-
const span = subtreeSpan.get(id) ??
|
|
1290
|
+
const span = subtreeSpan.get(id) ?? leafSpan(id);
|
|
1291
|
+
const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
|
|
1189
1292
|
if (kids.length === 0) {
|
|
1190
1293
|
const centerX = leftX + span / 2;
|
|
1191
|
-
nodePositions.set(id, { x: centerX -
|
|
1294
|
+
nodePositions.set(id, { x: centerX - dims.width / 2, y });
|
|
1192
1295
|
return;
|
|
1193
1296
|
}
|
|
1194
1297
|
let curX = leftX;
|
|
1195
1298
|
for (const kid of kids) {
|
|
1196
|
-
const kidSpan = subtreeSpan.get(kid) ??
|
|
1299
|
+
const kidSpan = subtreeSpan.get(kid) ?? leafSpan(kid);
|
|
1197
1300
|
positionSubtree(kid, curX);
|
|
1198
1301
|
curX += kidSpan;
|
|
1199
1302
|
}
|
|
1200
1303
|
const firstChild = nodePositions.get(kids[0]);
|
|
1201
1304
|
const lastChild = nodePositions.get(kids[kids.length - 1]);
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1305
|
+
const firstDims = nodeDims.get(kids[0]) ?? { width: NODE_W, height: NODE_H };
|
|
1306
|
+
const lastDims = nodeDims.get(kids[kids.length - 1]) ?? { width: NODE_W, height: NODE_H };
|
|
1307
|
+
const parentCenterX = (firstChild.x + firstDims.width / 2 + lastChild.x + lastDims.width / 2) / 2;
|
|
1308
|
+
nodePositions.set(id, { x: parentCenterX - dims.width / 2, y });
|
|
1204
1309
|
}
|
|
1205
1310
|
let currentRootLeft = treeStartX;
|
|
1206
1311
|
for (const r of roots) {
|
|
1207
|
-
const span = subtreeSpan.get(r) ??
|
|
1312
|
+
const span = subtreeSpan.get(r) ?? leafSpan(r);
|
|
1208
1313
|
positionSubtree(r, currentRootLeft);
|
|
1209
1314
|
currentRootLeft += span;
|
|
1210
1315
|
}
|
|
@@ -1212,6 +1317,7 @@ function layoutTree(ast, edges) {
|
|
|
1212
1317
|
for (const id of nodeIds) {
|
|
1213
1318
|
const decl = ast.nodes[id];
|
|
1214
1319
|
const pos = nodePositions.get(id) ?? { x: SAFE, y: TITLE_BAND };
|
|
1320
|
+
const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
|
|
1215
1321
|
nodes.push({
|
|
1216
1322
|
id,
|
|
1217
1323
|
kind: decl.kind,
|
|
@@ -1219,12 +1325,12 @@ function layoutTree(ast, edges) {
|
|
|
1219
1325
|
label: decl.label,
|
|
1220
1326
|
x: snapGrid(pos.x),
|
|
1221
1327
|
y: snapGrid(pos.y),
|
|
1222
|
-
width:
|
|
1223
|
-
height:
|
|
1328
|
+
width: dims.width,
|
|
1329
|
+
height: dims.height,
|
|
1224
1330
|
style: resolveNodeStyle(decl, ast),
|
|
1225
1331
|
props: decl.props,
|
|
1226
1332
|
opacity: 0,
|
|
1227
|
-
shape: "
|
|
1333
|
+
shape: nodeShape(decl.kind, "tree", decl.props),
|
|
1228
1334
|
focal: decl.props.focal === true || decl.props.accent === true
|
|
1229
1335
|
});
|
|
1230
1336
|
}
|
|
@@ -1260,6 +1366,12 @@ function cycleSafeEdges(nodeIds, edges) {
|
|
|
1260
1366
|
function layoutConstellation(ast) {
|
|
1261
1367
|
const nodeIds = Object.keys(ast.nodes);
|
|
1262
1368
|
if (nodeIds.length === 0) return [];
|
|
1369
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1370
|
+
for (const id of nodeIds) {
|
|
1371
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1372
|
+
}
|
|
1373
|
+
const maxNodeW = Math.max(...nodeIds.map((id) => nodeDims.get(id).width), NODE_W);
|
|
1374
|
+
const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height), NODE_H);
|
|
1263
1375
|
const incoming = /* @__PURE__ */ new Set();
|
|
1264
1376
|
for (const edge of ast.edges) {
|
|
1265
1377
|
if (edge.kind !== "response" && edge.from !== edge.to && ast.nodes[edge.to]) incoming.add(edge.to);
|
|
@@ -1267,19 +1379,18 @@ function layoutConstellation(ast) {
|
|
|
1267
1379
|
const focalId = nodeIds.find((id) => ast.nodes[id].props.focal === true || ast.nodes[id].props.accent === true) ?? nodeIds.find((id) => !incoming.has(id)) ?? nodeIds[0];
|
|
1268
1380
|
const contentW = ast.meta.width - SAFE * 2;
|
|
1269
1381
|
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
1270
|
-
const centerX = SAFE + contentW / 2 - NODE_W / 2;
|
|
1271
|
-
const centerY = TITLE_BAND + contentH / 2 - NODE_H / 2;
|
|
1272
1382
|
const orbitIds = nodeIds.filter((id) => id !== focalId);
|
|
1273
|
-
const radiusX = Math.max(140, contentW / 2 -
|
|
1274
|
-
const radiusY = Math.max(120, contentH / 2 -
|
|
1383
|
+
const radiusX = Math.max(140, contentW / 2 - maxNodeW / 2 - SAFE);
|
|
1384
|
+
const radiusY = Math.max(120, contentH / 2 - maxNodeH / 2 - SAFE);
|
|
1275
1385
|
const nodes = [];
|
|
1276
1386
|
for (const id of nodeIds) {
|
|
1277
1387
|
const decl = ast.nodes[id];
|
|
1388
|
+
const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
|
|
1278
1389
|
const isFocal = id === focalId;
|
|
1279
1390
|
const index = orbitIds.indexOf(id);
|
|
1280
1391
|
const angle = orbitIds.length > 0 ? -Math.PI / 2 + index * Math.PI * 2 / orbitIds.length : 0;
|
|
1281
|
-
const x = isFocal ?
|
|
1282
|
-
const y = isFocal ?
|
|
1392
|
+
const x = isFocal ? SAFE + contentW / 2 - dims.width / 2 : SAFE + contentW / 2 + Math.cos(angle) * radiusX - dims.width / 2;
|
|
1393
|
+
const y = isFocal ? TITLE_BAND + contentH / 2 - dims.height / 2 : TITLE_BAND + contentH / 2 + Math.sin(angle) * radiusY - dims.height / 2;
|
|
1283
1394
|
nodes.push({
|
|
1284
1395
|
id,
|
|
1285
1396
|
kind: decl.kind,
|
|
@@ -1287,8 +1398,8 @@ function layoutConstellation(ast) {
|
|
|
1287
1398
|
label: decl.label,
|
|
1288
1399
|
x: snapGrid(x),
|
|
1289
1400
|
y: snapGrid(y),
|
|
1290
|
-
width:
|
|
1291
|
-
height:
|
|
1401
|
+
width: dims.width,
|
|
1402
|
+
height: dims.height,
|
|
1292
1403
|
style: resolveNodeStyle(decl, ast),
|
|
1293
1404
|
props: decl.props,
|
|
1294
1405
|
opacity: 0,
|
|
@@ -1301,6 +1412,12 @@ function layoutConstellation(ast) {
|
|
|
1301
1412
|
function layoutLoop(ast) {
|
|
1302
1413
|
const nodeIds = Object.keys(ast.nodes);
|
|
1303
1414
|
if (nodeIds.length === 0) return [];
|
|
1415
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1416
|
+
for (const id of nodeIds) {
|
|
1417
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1418
|
+
}
|
|
1419
|
+
const maxNodeW = Math.max(...nodeIds.map((id) => nodeDims.get(id).width), NODE_W);
|
|
1420
|
+
const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height), NODE_H);
|
|
1304
1421
|
const hubId = nodeIds.find((id) => {
|
|
1305
1422
|
const decl = ast.nodes[id];
|
|
1306
1423
|
return decl.kind === "hub" || decl.props.hub === true || /^(hub|state|memory|shared_memory|core)$/i.test(id);
|
|
@@ -1310,22 +1427,25 @@ function layoutLoop(ast) {
|
|
|
1310
1427
|
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
1311
1428
|
const centerX = SAFE + contentW / 2;
|
|
1312
1429
|
const centerY = TITLE_BAND + contentH / 2;
|
|
1313
|
-
const radiusX = Math.min(contentW * 0.4, Math.max(220, (contentW -
|
|
1314
|
-
const radiusY = Math.min(contentH * 0.38, Math.max(150, (contentH -
|
|
1430
|
+
const radiusX = Math.min(contentW * 0.4, Math.max(220, (contentW - maxNodeW) / 2 - 32));
|
|
1431
|
+
const radiusY = Math.min(contentH * 0.38, Math.max(150, (contentH - maxNodeH) / 2 - 32));
|
|
1315
1432
|
const nodes = [];
|
|
1316
1433
|
for (const id of nodeIds) {
|
|
1317
1434
|
const decl = ast.nodes[id];
|
|
1318
1435
|
const isHub = id === hubId;
|
|
1436
|
+
const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
|
|
1437
|
+
const w = isHub ? Math.max(dims.width, snapGrid(NODE_W * 1.25)) : dims.width;
|
|
1438
|
+
const h = isHub ? Math.max(dims.height, snapGrid(NODE_H * 1.15)) : dims.height;
|
|
1319
1439
|
let x;
|
|
1320
1440
|
let y;
|
|
1321
1441
|
if (isHub) {
|
|
1322
|
-
x = centerX -
|
|
1323
|
-
y = centerY -
|
|
1442
|
+
x = centerX - w / 2;
|
|
1443
|
+
y = centerY - h / 2;
|
|
1324
1444
|
} else {
|
|
1325
1445
|
const idx = stationIds.indexOf(id);
|
|
1326
1446
|
const angle = -Math.PI / 2 + idx * 2 * Math.PI / Math.max(stationIds.length, 1);
|
|
1327
|
-
x = centerX + Math.cos(angle) * radiusX -
|
|
1328
|
-
y = centerY + Math.sin(angle) * radiusY -
|
|
1447
|
+
x = centerX + Math.cos(angle) * radiusX - w / 2;
|
|
1448
|
+
y = centerY + Math.sin(angle) * radiusY - h / 2;
|
|
1329
1449
|
}
|
|
1330
1450
|
const focal = isHub || decl.props.focal === true || decl.props.accent === true;
|
|
1331
1451
|
nodes.push({
|
|
@@ -1335,8 +1455,8 @@ function layoutLoop(ast) {
|
|
|
1335
1455
|
label: decl.label,
|
|
1336
1456
|
x: snapGrid(x),
|
|
1337
1457
|
y: snapGrid(y),
|
|
1338
|
-
width:
|
|
1339
|
-
height:
|
|
1458
|
+
width: w,
|
|
1459
|
+
height: h,
|
|
1340
1460
|
style: resolveNodeStyle(decl, ast),
|
|
1341
1461
|
props: decl.props,
|
|
1342
1462
|
opacity: 0,
|
|
@@ -1357,6 +1477,10 @@ function layoutMedallion(ast, edges) {
|
|
|
1357
1477
|
if (combined.includes("bi") || combined.includes("dash") || combined.includes("model") || combined.includes("app") || combined.includes("consumer") || combined.includes("user") || combined.includes("client")) return 4;
|
|
1358
1478
|
return 0;
|
|
1359
1479
|
}
|
|
1480
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1481
|
+
for (const id of nodeIds) {
|
|
1482
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1483
|
+
}
|
|
1360
1484
|
const tiers = /* @__PURE__ */ new Map();
|
|
1361
1485
|
for (let i = 0; i <= 4; i++) tiers.set(i, []);
|
|
1362
1486
|
for (const id of nodeIds) {
|
|
@@ -1368,29 +1492,35 @@ function layoutMedallion(ast, edges) {
|
|
|
1368
1492
|
const tierCount = activeTiers.length || 1;
|
|
1369
1493
|
const contentW = ast.meta.width - SAFE * 2;
|
|
1370
1494
|
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
1371
|
-
const
|
|
1372
|
-
const
|
|
1495
|
+
const maxNodeW = Math.max(...nodeIds.map((id) => nodeDims.get(id).width));
|
|
1496
|
+
const availableColGap = tierCount > 1 ? (contentW - maxNodeW) / (tierCount - 1) : 0;
|
|
1497
|
+
const colGap = Math.max(maxNodeW + 36, Math.min(360, availableColGap));
|
|
1498
|
+
const totalW = (tierCount - 1) * colGap + maxNodeW;
|
|
1373
1499
|
const startX = SAFE + Math.max(0, (contentW - totalW) / 2);
|
|
1374
1500
|
const nodes = [];
|
|
1375
1501
|
activeTiers.forEach(([_, ids], colIdx) => {
|
|
1376
1502
|
const colCount = ids.length;
|
|
1503
|
+
const colTierMaxW = Math.max(...ids.map((id) => nodeDims.get(id).width));
|
|
1504
|
+
const colTierMaxH = Math.max(...ids.map((id) => nodeDims.get(id).height));
|
|
1377
1505
|
const colX = startX + colIdx * colGap;
|
|
1378
|
-
const rowSpacing = Math.max(
|
|
1379
|
-
const colH = (colCount - 1) * rowSpacing +
|
|
1506
|
+
const rowSpacing = Math.max(colTierMaxH + 32, contentH / (colCount + 1));
|
|
1507
|
+
const colH = (colCount - 1) * rowSpacing + colTierMaxH;
|
|
1380
1508
|
const startY = TITLE_BAND + Math.max(0, (contentH - colH) / 2);
|
|
1381
1509
|
ids.forEach((id, rowIdx) => {
|
|
1382
1510
|
const decl = ast.nodes[id];
|
|
1383
|
-
const
|
|
1511
|
+
const dims = nodeDims.get(id);
|
|
1512
|
+
const rowY = startY + rowIdx * rowSpacing + (colTierMaxH - dims.height) / 2;
|
|
1513
|
+
const nodeX = colX + (colTierMaxW - dims.width) / 2;
|
|
1384
1514
|
const focal = decl.props.focal === true || decl.props.accent === true;
|
|
1385
1515
|
nodes.push({
|
|
1386
1516
|
id,
|
|
1387
1517
|
kind: decl.kind,
|
|
1388
1518
|
role: nodeRole(decl.kind),
|
|
1389
1519
|
label: decl.label,
|
|
1390
|
-
x: snapGrid(
|
|
1520
|
+
x: snapGrid(nodeX),
|
|
1391
1521
|
y: snapGrid(rowY),
|
|
1392
|
-
width:
|
|
1393
|
-
height:
|
|
1522
|
+
width: dims.width,
|
|
1523
|
+
height: dims.height,
|
|
1394
1524
|
style: resolveNodeStyle(decl, ast),
|
|
1395
1525
|
props: decl.props,
|
|
1396
1526
|
opacity: 0,
|
|
@@ -1404,6 +1534,10 @@ function layoutMedallion(ast, edges) {
|
|
|
1404
1534
|
function layoutQuadrant(ast) {
|
|
1405
1535
|
const nodeIds = Object.keys(ast.nodes);
|
|
1406
1536
|
if (nodeIds.length === 0) return [];
|
|
1537
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1538
|
+
for (const id of nodeIds) {
|
|
1539
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1540
|
+
}
|
|
1407
1541
|
const contentW = ast.meta.width - SAFE * 2;
|
|
1408
1542
|
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
1409
1543
|
const centerX = SAFE + contentW / 2;
|
|
@@ -1437,12 +1571,15 @@ function layoutQuadrant(ast) {
|
|
|
1437
1571
|
};
|
|
1438
1572
|
for (const [qNum, ids] of quadNodes) {
|
|
1439
1573
|
const center = quadCenters[qNum];
|
|
1574
|
+
const offsetCount = ids.length;
|
|
1575
|
+
const maxQHeight = ids.length > 0 ? Math.max(...ids.map((id) => nodeDims.get(id).height)) : NODE_H;
|
|
1576
|
+
const rowStep = maxQHeight + 20;
|
|
1440
1577
|
ids.forEach((id, idx) => {
|
|
1441
1578
|
const decl = ast.nodes[id];
|
|
1442
|
-
const
|
|
1443
|
-
const rowOffset = (idx - (offsetCount - 1) / 2) *
|
|
1444
|
-
const x = center.x -
|
|
1445
|
-
const y = center.y + rowOffset -
|
|
1579
|
+
const dims = nodeDims.get(id);
|
|
1580
|
+
const rowOffset = (idx - (offsetCount - 1) / 2) * rowStep;
|
|
1581
|
+
const x = center.x - dims.width / 2;
|
|
1582
|
+
const y = center.y + rowOffset - dims.height / 2;
|
|
1446
1583
|
const focal = decl.props.focal === true || decl.props.accent === true;
|
|
1447
1584
|
nodes.push({
|
|
1448
1585
|
id,
|
|
@@ -1451,8 +1588,8 @@ function layoutQuadrant(ast) {
|
|
|
1451
1588
|
label: decl.label,
|
|
1452
1589
|
x: snapGrid(x),
|
|
1453
1590
|
y: snapGrid(y),
|
|
1454
|
-
width:
|
|
1455
|
-
height:
|
|
1591
|
+
width: dims.width,
|
|
1592
|
+
height: dims.height,
|
|
1456
1593
|
style: resolveNodeStyle(decl, ast),
|
|
1457
1594
|
props: decl.props,
|
|
1458
1595
|
opacity: 0,
|
|
@@ -1466,6 +1603,10 @@ function layoutQuadrant(ast) {
|
|
|
1466
1603
|
function layoutSwimlane(ast, edges) {
|
|
1467
1604
|
const nodeIds = Object.keys(ast.nodes);
|
|
1468
1605
|
if (nodeIds.length === 0) return [];
|
|
1606
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1607
|
+
for (const id of nodeIds) {
|
|
1608
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1609
|
+
}
|
|
1469
1610
|
const lanes = /* @__PURE__ */ new Map();
|
|
1470
1611
|
const groupKeys = Object.keys(ast.groups);
|
|
1471
1612
|
if (groupKeys.length > 0) {
|
|
@@ -1488,14 +1629,16 @@ function layoutSwimlane(ast, edges) {
|
|
|
1488
1629
|
const laneHeight = contentH / laneCount;
|
|
1489
1630
|
const nodes = [];
|
|
1490
1631
|
activeLanes.forEach(([_, ids], laneIdx) => {
|
|
1491
|
-
const laneY = TITLE_BAND + laneIdx * laneHeight + (laneHeight - NODE_H) / 2;
|
|
1492
1632
|
const count = ids.length;
|
|
1493
|
-
const
|
|
1494
|
-
const
|
|
1633
|
+
const maxLaneW = count > 0 ? Math.max(...ids.map((id) => nodeDims.get(id).width)) : NODE_W;
|
|
1634
|
+
const colSpacing = Math.max(maxLaneW + 40, Math.min(320, contentW / Math.max(count + 1, 2)));
|
|
1635
|
+
const totalW = (count - 1) * colSpacing + maxLaneW;
|
|
1495
1636
|
const startX = SAFE + Math.max(0, (contentW - totalW) / 2);
|
|
1496
1637
|
ids.forEach((id, colIdx) => {
|
|
1497
1638
|
const decl = ast.nodes[id];
|
|
1498
|
-
const
|
|
1639
|
+
const dims = nodeDims.get(id);
|
|
1640
|
+
const colX = startX + colIdx * colSpacing + (maxLaneW - dims.width) / 2;
|
|
1641
|
+
const laneY = TITLE_BAND + laneIdx * laneHeight + (laneHeight - dims.height) / 2;
|
|
1499
1642
|
const focal = decl.props.focal === true || decl.props.accent === true;
|
|
1500
1643
|
nodes.push({
|
|
1501
1644
|
id,
|
|
@@ -1504,8 +1647,8 @@ function layoutSwimlane(ast, edges) {
|
|
|
1504
1647
|
label: decl.label,
|
|
1505
1648
|
x: snapGrid(colX),
|
|
1506
1649
|
y: snapGrid(laneY),
|
|
1507
|
-
width:
|
|
1508
|
-
height:
|
|
1650
|
+
width: dims.width,
|
|
1651
|
+
height: dims.height,
|
|
1509
1652
|
style: resolveNodeStyle(decl, ast),
|
|
1510
1653
|
props: decl.props,
|
|
1511
1654
|
opacity: 0,
|
|
@@ -1519,6 +1662,10 @@ function layoutSwimlane(ast, edges) {
|
|
|
1519
1662
|
function layoutPyramid(ast) {
|
|
1520
1663
|
const nodeIds = Object.keys(ast.nodes);
|
|
1521
1664
|
if (nodeIds.length === 0) return [];
|
|
1665
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1666
|
+
for (const id of nodeIds) {
|
|
1667
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1668
|
+
}
|
|
1522
1669
|
const ranks = /* @__PURE__ */ new Map();
|
|
1523
1670
|
nodeIds.forEach((id, idx) => {
|
|
1524
1671
|
const decl = ast.nodes[id];
|
|
@@ -1531,20 +1678,26 @@ function layoutPyramid(ast) {
|
|
|
1531
1678
|
const contentW = ast.meta.width - SAFE * 2;
|
|
1532
1679
|
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
1533
1680
|
const centerX = SAFE + contentW / 2;
|
|
1534
|
-
const
|
|
1681
|
+
const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height));
|
|
1682
|
+
const tierH = Math.max(NODE_H, maxNodeH);
|
|
1683
|
+
const totalH = tierCount * tierH + (tierCount - 1) * 24;
|
|
1535
1684
|
const startY = TITLE_BAND + Math.max(16, (contentH - totalH) / 2);
|
|
1536
1685
|
const nodes = [];
|
|
1537
1686
|
sortedTiers.forEach(([_, ids], tierIdx) => {
|
|
1538
|
-
const tierY = startY + tierIdx * (
|
|
1687
|
+
const tierY = startY + tierIdx * (tierH + 24);
|
|
1539
1688
|
const baseFraction = 0.32 + 0.44 * tierIdx / Math.max(tierCount - 1, 1);
|
|
1540
1689
|
const tierWidth = contentW * baseFraction;
|
|
1541
1690
|
const count = ids.length;
|
|
1542
|
-
const
|
|
1691
|
+
const maxTierW = Math.max(...ids.map((id) => nodeDims.get(id).width));
|
|
1692
|
+
const nodeW = Math.max(maxTierW, Math.min(tierWidth / count - 12, 480));
|
|
1543
1693
|
const totalTierW = count * nodeW + (count - 1) * 16;
|
|
1544
1694
|
const tierStartX = centerX - totalTierW / 2;
|
|
1545
1695
|
ids.forEach((id, idx) => {
|
|
1546
1696
|
const decl = ast.nodes[id];
|
|
1547
|
-
const
|
|
1697
|
+
const dims = nodeDims.get(id);
|
|
1698
|
+
const finalW = Math.max(nodeW, dims.width);
|
|
1699
|
+
const x = tierStartX + idx * (finalW + 16);
|
|
1700
|
+
const y = tierY + (tierH - dims.height) / 2;
|
|
1548
1701
|
const focal = decl.props.focal === true || decl.props.accent === true;
|
|
1549
1702
|
nodes.push({
|
|
1550
1703
|
id,
|
|
@@ -1553,8 +1706,8 @@ function layoutPyramid(ast) {
|
|
|
1553
1706
|
label: decl.label,
|
|
1554
1707
|
x: snapGrid(x),
|
|
1555
1708
|
y: snapGrid(tierY),
|
|
1556
|
-
width: snapGrid(
|
|
1557
|
-
height:
|
|
1709
|
+
width: snapGrid(finalW),
|
|
1710
|
+
height: dims.height,
|
|
1558
1711
|
style: resolveNodeStyle(decl, ast),
|
|
1559
1712
|
props: decl.props,
|
|
1560
1713
|
opacity: 0,
|
|
@@ -1568,6 +1721,10 @@ function layoutPyramid(ast) {
|
|
|
1568
1721
|
function layoutTimeline(ast) {
|
|
1569
1722
|
const nodeIds = Object.keys(ast.nodes);
|
|
1570
1723
|
if (nodeIds.length === 0) return [];
|
|
1724
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1725
|
+
for (const id of nodeIds) {
|
|
1726
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1727
|
+
}
|
|
1571
1728
|
const contentW = ast.meta.width - SAFE * 2;
|
|
1572
1729
|
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
1573
1730
|
const baselineY = TITLE_BAND + contentH / 2;
|
|
@@ -1575,9 +1732,10 @@ function layoutTimeline(ast) {
|
|
|
1575
1732
|
const nodes = [];
|
|
1576
1733
|
nodeIds.forEach((id, idx) => {
|
|
1577
1734
|
const decl = ast.nodes[id];
|
|
1578
|
-
const
|
|
1735
|
+
const dims = nodeDims.get(id);
|
|
1736
|
+
const x = SAFE + spacing * idx + (spacing - dims.width) / 2;
|
|
1579
1737
|
const above = idx % 2 === 0;
|
|
1580
|
-
const y = above ? baselineY -
|
|
1738
|
+
const y = above ? baselineY - dims.height - 24 : baselineY + 24;
|
|
1581
1739
|
const focal = decl.props.focal === true || decl.props.accent === true;
|
|
1582
1740
|
nodes.push({
|
|
1583
1741
|
id,
|
|
@@ -1586,8 +1744,8 @@ function layoutTimeline(ast) {
|
|
|
1586
1744
|
label: decl.label,
|
|
1587
1745
|
x: snapGrid(x),
|
|
1588
1746
|
y: snapGrid(y),
|
|
1589
|
-
width:
|
|
1590
|
-
height:
|
|
1747
|
+
width: dims.width,
|
|
1748
|
+
height: dims.height,
|
|
1591
1749
|
style: resolveNodeStyle(decl, ast),
|
|
1592
1750
|
props: decl.props,
|
|
1593
1751
|
opacity: 0,
|
|
@@ -1600,16 +1758,23 @@ function layoutTimeline(ast) {
|
|
|
1600
1758
|
function layoutGantt(ast) {
|
|
1601
1759
|
const nodeIds = Object.keys(ast.nodes);
|
|
1602
1760
|
if (nodeIds.length === 0) return [];
|
|
1761
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1762
|
+
for (const id of nodeIds) {
|
|
1763
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1764
|
+
}
|
|
1765
|
+
const topBand = effectiveTitleBand(ast);
|
|
1603
1766
|
const contentW = ast.meta.width - SAFE * 2;
|
|
1604
|
-
const contentH = ast.meta.height - SAFE -
|
|
1767
|
+
const contentH = ast.meta.height - SAFE - topBand - SAFE;
|
|
1605
1768
|
const N = nodeIds.length;
|
|
1606
|
-
const
|
|
1607
|
-
const
|
|
1769
|
+
const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height));
|
|
1770
|
+
const baseBarH = Math.max(44, maxNodeH);
|
|
1771
|
+
const rowH = Math.max(baseBarH + 16, Math.min(100, (contentH - 24) / Math.max(N, 1)));
|
|
1608
1772
|
const totalH = N * rowH;
|
|
1609
|
-
const startY =
|
|
1773
|
+
const startY = topBand + Math.max(16, (contentH - totalH) / 2);
|
|
1610
1774
|
const nodes = [];
|
|
1611
1775
|
nodeIds.forEach((id, idx) => {
|
|
1612
1776
|
const decl = ast.nodes[id];
|
|
1777
|
+
const dims = nodeDims.get(id);
|
|
1613
1778
|
const phase = typeof decl.props.phase === "number" ? decl.props.phase : 0;
|
|
1614
1779
|
const span = typeof decl.props.span === "number" ? decl.props.span : 1;
|
|
1615
1780
|
const totalPhases = Math.max(...nodeIds.map((nid) => {
|
|
@@ -1619,7 +1784,8 @@ function layoutGantt(ast) {
|
|
|
1619
1784
|
}), 1);
|
|
1620
1785
|
const unitW = contentW / totalPhases;
|
|
1621
1786
|
const x = SAFE + phase * unitW;
|
|
1622
|
-
const w = Math.max(span * unitW - 8,
|
|
1787
|
+
const w = Math.max(span * unitW - 8, dims.width);
|
|
1788
|
+
const barH = dims.height;
|
|
1623
1789
|
const y = startY + idx * rowH + (rowH - barH) / 2;
|
|
1624
1790
|
const focal = decl.props.focal === true || decl.props.accent === true;
|
|
1625
1791
|
nodes.push({
|
|
@@ -1630,7 +1796,7 @@ function layoutGantt(ast) {
|
|
|
1630
1796
|
x: snapGrid(x),
|
|
1631
1797
|
y: snapGrid(y),
|
|
1632
1798
|
width: snapGrid(w),
|
|
1633
|
-
height: barH,
|
|
1799
|
+
height: snapGrid(barH),
|
|
1634
1800
|
style: resolveNodeStyle(decl, ast),
|
|
1635
1801
|
props: decl.props,
|
|
1636
1802
|
opacity: 0,
|
|
@@ -1643,23 +1809,31 @@ function layoutGantt(ast) {
|
|
|
1643
1809
|
function layoutVenn(ast) {
|
|
1644
1810
|
const nodeIds = Object.keys(ast.nodes);
|
|
1645
1811
|
if (nodeIds.length === 0) return [];
|
|
1812
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1813
|
+
for (const id of nodeIds) {
|
|
1814
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1815
|
+
}
|
|
1646
1816
|
const contentW = ast.meta.width - SAFE * 2;
|
|
1647
1817
|
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
1648
1818
|
const centerX = SAFE + contentW / 2;
|
|
1649
1819
|
const centerY = TITLE_BAND + contentH / 2;
|
|
1650
1820
|
const N = nodeIds.length;
|
|
1651
|
-
const
|
|
1821
|
+
const maxNodeDim = Math.max(...nodeIds.map((id) => Math.max(nodeDims.get(id).width, nodeDims.get(id).height)));
|
|
1822
|
+
const vennSize = Math.max(VENN_NODE_SIZE, maxNodeDim);
|
|
1823
|
+
const radius = Math.max(vennSize * 0.7, Math.min(contentW, contentH) * 0.24);
|
|
1652
1824
|
const nodes = [];
|
|
1653
1825
|
nodeIds.forEach((id, idx) => {
|
|
1654
1826
|
const decl = ast.nodes[id];
|
|
1827
|
+
const dims = nodeDims.get(id);
|
|
1828
|
+
const circleSize = Math.max(vennSize, dims.width, dims.height);
|
|
1655
1829
|
let x, y;
|
|
1656
1830
|
if (N === 2) {
|
|
1657
|
-
x = centerX + (idx === 0 ? -radius * 0.7 : radius * 0.7) -
|
|
1658
|
-
y = centerY -
|
|
1831
|
+
x = centerX + (idx === 0 ? -radius * 0.7 : radius * 0.7) - circleSize / 2;
|
|
1832
|
+
y = centerY - circleSize / 2;
|
|
1659
1833
|
} else {
|
|
1660
1834
|
const angle = -Math.PI / 2 + idx * 2 * Math.PI / N;
|
|
1661
|
-
x = centerX + radius * 0.85 * Math.cos(angle) -
|
|
1662
|
-
y = centerY + radius * 0.85 * Math.sin(angle) -
|
|
1835
|
+
x = centerX + radius * 0.85 * Math.cos(angle) - circleSize / 2;
|
|
1836
|
+
y = centerY + radius * 0.85 * Math.sin(angle) - circleSize / 2;
|
|
1663
1837
|
}
|
|
1664
1838
|
const focal = decl.props.focal === true || decl.props.accent === true;
|
|
1665
1839
|
nodes.push({
|
|
@@ -1669,8 +1843,8 @@ function layoutVenn(ast) {
|
|
|
1669
1843
|
label: decl.label,
|
|
1670
1844
|
x: snapGrid(x),
|
|
1671
1845
|
y: snapGrid(y),
|
|
1672
|
-
width:
|
|
1673
|
-
height:
|
|
1846
|
+
width: circleSize,
|
|
1847
|
+
height: circleSize,
|
|
1674
1848
|
style: resolveNodeStyle(decl, ast),
|
|
1675
1849
|
props: decl.props,
|
|
1676
1850
|
opacity: 0,
|
|
@@ -1683,18 +1857,26 @@ function layoutVenn(ast) {
|
|
|
1683
1857
|
function layoutLayers(ast) {
|
|
1684
1858
|
const nodeIds = Object.keys(ast.nodes);
|
|
1685
1859
|
if (nodeIds.length === 0) return [];
|
|
1860
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1861
|
+
for (const id of nodeIds) {
|
|
1862
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1863
|
+
}
|
|
1686
1864
|
const contentW = ast.meta.width - SAFE * 2;
|
|
1687
1865
|
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
1688
1866
|
const N = nodeIds.length;
|
|
1689
|
-
const
|
|
1867
|
+
const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height));
|
|
1868
|
+
const layerH = Math.max(maxNodeH, Math.min(Math.max((contentH - (N - 1) * 14) / N, 52), 96));
|
|
1690
1869
|
const totalH = N * layerH + (N - 1) * 14;
|
|
1691
|
-
const startY = TITLE_BAND + (contentH - totalH) / 2;
|
|
1692
|
-
const
|
|
1870
|
+
const startY = TITLE_BAND + Math.max(16, (contentH - totalH) / 2);
|
|
1871
|
+
const maxNodeW = Math.max(...nodeIds.map((id) => nodeDims.get(id).width));
|
|
1872
|
+
const layerW = Math.max(maxNodeW, Math.min(contentW, 880));
|
|
1693
1873
|
const startX = SAFE + (contentW - layerW) / 2;
|
|
1694
1874
|
const nodes = [];
|
|
1695
1875
|
nodeIds.forEach((id, idx) => {
|
|
1696
1876
|
const decl = ast.nodes[id];
|
|
1877
|
+
const dims = nodeDims.get(id);
|
|
1697
1878
|
const y = startY + idx * (layerH + 14);
|
|
1879
|
+
const nodeH = Math.max(layerH, dims.height);
|
|
1698
1880
|
const focal = decl.props.focal === true || decl.props.accent === true;
|
|
1699
1881
|
nodes.push({
|
|
1700
1882
|
id,
|
|
@@ -1704,7 +1886,7 @@ function layoutLayers(ast) {
|
|
|
1704
1886
|
x: snapGrid(startX),
|
|
1705
1887
|
y: snapGrid(y),
|
|
1706
1888
|
width: snapGrid(layerW),
|
|
1707
|
-
height: snapGrid(
|
|
1889
|
+
height: snapGrid(nodeH),
|
|
1708
1890
|
style: resolveNodeStyle(decl, ast),
|
|
1709
1891
|
props: decl.props,
|
|
1710
1892
|
opacity: 0,
|
|
@@ -1717,6 +1899,10 @@ function layoutLayers(ast) {
|
|
|
1717
1899
|
function layoutNested(ast) {
|
|
1718
1900
|
const nodeIds = Object.keys(ast.nodes);
|
|
1719
1901
|
if (nodeIds.length === 0) return [];
|
|
1902
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1903
|
+
for (const id of nodeIds) {
|
|
1904
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1905
|
+
}
|
|
1720
1906
|
const topHeadroom = 20;
|
|
1721
1907
|
const startY = TITLE_BAND + topHeadroom;
|
|
1722
1908
|
const contentW = ast.meta.width - SAFE * 2;
|
|
@@ -1729,18 +1915,19 @@ function layoutNested(ast) {
|
|
|
1729
1915
|
const nodes = [];
|
|
1730
1916
|
nodeIds.forEach((id, idx) => {
|
|
1731
1917
|
const decl = ast.nodes[id];
|
|
1918
|
+
const dims = nodeDims.get(id);
|
|
1732
1919
|
const isCore = idx === N - 1;
|
|
1733
1920
|
let x, y, w, h;
|
|
1734
1921
|
if (isCore) {
|
|
1735
|
-
w = Math.min(320, contentW - idx * padX * 2);
|
|
1736
|
-
h = 84;
|
|
1922
|
+
w = Math.max(dims.width, Math.min(320, contentW - idx * padX * 2));
|
|
1923
|
+
h = Math.max(dims.height, 84);
|
|
1737
1924
|
x = centerX - w / 2;
|
|
1738
1925
|
y = centerY - h / 2 + (N > 1 ? 16 : 0);
|
|
1739
1926
|
} else {
|
|
1740
1927
|
x = SAFE + idx * padX;
|
|
1741
1928
|
y = startY + idx * padY;
|
|
1742
|
-
w = contentW - idx * padX * 2;
|
|
1743
|
-
h = contentH - idx * padY * 2;
|
|
1929
|
+
w = Math.max(dims.width, contentW - idx * padX * 2);
|
|
1930
|
+
h = Math.max(dims.height, contentH - idx * padY * 2);
|
|
1744
1931
|
}
|
|
1745
1932
|
const focal = decl.props.focal === true || decl.props.accent === true || isCore;
|
|
1746
1933
|
nodes.push({
|
|
@@ -1764,6 +1951,10 @@ function layoutNested(ast) {
|
|
|
1764
1951
|
function layoutRadar(ast) {
|
|
1765
1952
|
const nodeIds = Object.keys(ast.nodes);
|
|
1766
1953
|
if (nodeIds.length === 0) return [];
|
|
1954
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1955
|
+
for (const id of nodeIds) {
|
|
1956
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1957
|
+
}
|
|
1767
1958
|
const contentW = ast.meta.width - SAFE * 2;
|
|
1768
1959
|
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
1769
1960
|
const centerX = SAFE + contentW / 2;
|
|
@@ -1773,9 +1964,10 @@ function layoutRadar(ast) {
|
|
|
1773
1964
|
const nodes = [];
|
|
1774
1965
|
nodeIds.forEach((id, idx) => {
|
|
1775
1966
|
const decl = ast.nodes[id];
|
|
1967
|
+
const dims = nodeDims.get(id);
|
|
1776
1968
|
const angle = -Math.PI / 2 + idx * 2 * Math.PI / N;
|
|
1777
|
-
const x = centerX + radius * Math.cos(angle) -
|
|
1778
|
-
const y = centerY + radius * Math.sin(angle) -
|
|
1969
|
+
const x = centerX + radius * Math.cos(angle) - dims.width / 2;
|
|
1970
|
+
const y = centerY + radius * Math.sin(angle) - dims.height / 2;
|
|
1779
1971
|
const focal = decl.props.focal === true || decl.props.accent === true;
|
|
1780
1972
|
nodes.push({
|
|
1781
1973
|
id,
|
|
@@ -1784,8 +1976,8 @@ function layoutRadar(ast) {
|
|
|
1784
1976
|
label: decl.label,
|
|
1785
1977
|
x: snapGrid(x),
|
|
1786
1978
|
y: snapGrid(y),
|
|
1787
|
-
width:
|
|
1788
|
-
height:
|
|
1979
|
+
width: dims.width,
|
|
1980
|
+
height: dims.height,
|
|
1789
1981
|
style: resolveNodeStyle(decl, ast),
|
|
1790
1982
|
props: decl.props,
|
|
1791
1983
|
opacity: 0,
|
|
@@ -1830,9 +2022,9 @@ function layoutNodes(ast, edges) {
|
|
|
1830
2022
|
case "nested":
|
|
1831
2023
|
return layoutNested(ast);
|
|
1832
2024
|
case "state":
|
|
1833
|
-
return layoutRanked(ast, cycleSafeEdges(Object.keys(ast.nodes), edges), { forceVertical:
|
|
2025
|
+
return layoutRanked(ast, cycleSafeEdges(Object.keys(ast.nodes), edges), { forceVertical: ast.meta.direction === "TB" || ast.meta.direction === "BT" });
|
|
1834
2026
|
default:
|
|
1835
|
-
return layoutRanked(ast, edges, { forceVertical:
|
|
2027
|
+
return layoutRanked(ast, edges, { forceVertical: ast.meta.direction === "TB" || ast.meta.direction === "BT" });
|
|
1836
2028
|
}
|
|
1837
2029
|
}
|
|
1838
2030
|
function computeGroupBoundaries(ast, nodes) {
|
|
@@ -1940,21 +2132,33 @@ function scheduleBeats(ast, edges) {
|
|
|
1940
2132
|
}
|
|
1941
2133
|
}
|
|
1942
2134
|
}
|
|
1943
|
-
const
|
|
1944
|
-
|
|
1945
|
-
const
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
}
|
|
1955
|
-
t += DEFAULTS.show + DEFAULTS.cueGap;
|
|
2135
|
+
const explicitlyShownNodes = /* @__PURE__ */ new Set();
|
|
2136
|
+
for (const b of ast.beats) {
|
|
2137
|
+
for (const c of b.cues) {
|
|
2138
|
+
if (c.kind === "show") {
|
|
2139
|
+
resolveTargets(c.targets, ast, groupMap, edgeIds).forEach((id) => explicitlyShownNodes.add(id));
|
|
2140
|
+
} else if (c.kind === "parallel") {
|
|
2141
|
+
for (const pc of c.cues) {
|
|
2142
|
+
if (pc.kind === "show") {
|
|
2143
|
+
resolveTargets(pc.targets, ast, groupMap, edgeIds).forEach((id) => explicitlyShownNodes.add(id));
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
1956
2147
|
}
|
|
1957
2148
|
}
|
|
2149
|
+
const isSequence = ast.meta.type === "sequence";
|
|
2150
|
+
const nodesToReveal = !isSequence && flowNodeIds.size > 0 ? Object.keys(ast.nodes).filter((id) => !flowNodeIds.has(id) && !explicitlyShownNodes.has(id)) : !hasShowCue ? Object.keys(ast.nodes) : [];
|
|
2151
|
+
if (nodesToReveal.length > 0) {
|
|
2152
|
+
cues.push({
|
|
2153
|
+
start: 0,
|
|
2154
|
+
duration: DEFAULTS.show,
|
|
2155
|
+
kind: "show",
|
|
2156
|
+
targets: nodesToReveal,
|
|
2157
|
+
params: { stagger: DEFAULTS.stagger },
|
|
2158
|
+
beat: "__intro"
|
|
2159
|
+
});
|
|
2160
|
+
t += DEFAULTS.show + DEFAULTS.cueGap;
|
|
2161
|
+
}
|
|
1958
2162
|
for (const beat of ast.beats) {
|
|
1959
2163
|
const beatStart = t;
|
|
1960
2164
|
const scheduled = [];
|
|
@@ -2024,8 +2228,10 @@ function scheduleBeats(ast, edges) {
|
|
|
2024
2228
|
}
|
|
2025
2229
|
function buildSequencePlan(ast, cues) {
|
|
2026
2230
|
if (diagramType(ast) !== "sequence") return { messages: [], activations: [] };
|
|
2231
|
+
const nodeDims = Object.values(ast.nodes).map((n) => computeNodeDimensions(n));
|
|
2232
|
+
const maxNodeH = nodeDims.length > 0 ? Math.max(...nodeDims.map((d) => d.height)) : NODE_H;
|
|
2027
2233
|
const flowCues = cues.filter((cue) => cue.kind === "flow" && cue.segments?.[0]);
|
|
2028
|
-
const firstY = TITLE_BAND +
|
|
2234
|
+
const firstY = TITLE_BAND + maxNodeH + 56;
|
|
2029
2235
|
const lastY = ast.meta.height - SAFE - 48;
|
|
2030
2236
|
const availH = Math.max(120, lastY - firstY);
|
|
2031
2237
|
const step = flowCues.length > 1 ? Math.max(52, Math.min(90, availH / flowCues.length)) : 0;
|
|
@@ -2074,19 +2280,44 @@ function computeAdaptiveDimensions(ast, edges) {
|
|
|
2074
2280
|
const direction = ast.meta.direction ?? "LR";
|
|
2075
2281
|
const isVertical = direction === "TB" || direction === "BT";
|
|
2076
2282
|
const groupCount = Object.keys(ast.groups).length;
|
|
2077
|
-
|
|
2078
|
-
|
|
2283
|
+
const nodeDims = nodeIds.map((id) => computeNodeDimensions(ast.nodes[id]));
|
|
2284
|
+
const maxNodeW = nodeDims.length > 0 ? Math.max(...nodeDims.map((d) => d.width)) : NODE_W;
|
|
2285
|
+
const maxNodeH = nodeDims.length > 0 ? Math.max(...nodeDims.map((d) => d.height)) : NODE_H;
|
|
2286
|
+
const hasTitle = Boolean(ast.meta.title && ast.meta.title.trim().length > 0);
|
|
2287
|
+
const titleBand = hasTitle ? TITLE_BAND : 0;
|
|
2288
|
+
const hasBeatCaptions = ast.beats.some((b) => b.label && b.label.trim().length > 0);
|
|
2289
|
+
const bottomBand = hasBeatCaptions ? 44 : 0;
|
|
2290
|
+
const titleW = hasTitle ? Math.max(320, ast.meta.title.trim().length * 11 + SAFE * 2) : 0;
|
|
2291
|
+
let autoW = 1024;
|
|
2292
|
+
let autoH = 640;
|
|
2079
2293
|
if (nodeCount === 0) {
|
|
2080
|
-
autoW =
|
|
2081
|
-
autoH =
|
|
2294
|
+
autoW = 960;
|
|
2295
|
+
autoH = 540;
|
|
2082
2296
|
} else if (dtype === "sequence") {
|
|
2083
2297
|
const flowCues = ast.beats.flatMap((b) => b.cues).filter((c) => c.kind === "flow");
|
|
2084
2298
|
const count = Math.max(nodeCount, 1);
|
|
2085
2299
|
const flowCount = flowCues.length;
|
|
2086
|
-
const
|
|
2087
|
-
const
|
|
2088
|
-
|
|
2089
|
-
|
|
2300
|
+
const colW = Math.max(200, maxNodeW + 36);
|
|
2301
|
+
const requiredW = SAFE * 2 + Math.max(count * colW, titleW);
|
|
2302
|
+
const requiredH = titleBand + maxNodeH + 48 + Math.max(flowCount * 68, 160) + SAFE + bottomBand;
|
|
2303
|
+
autoW = Math.max(480, Math.min(2560, requiredW));
|
|
2304
|
+
autoH = Math.max(320, Math.min(1800, requiredH));
|
|
2305
|
+
} else if (dtype === "medallion") {
|
|
2306
|
+
const tierSet = /* @__PURE__ */ new Set();
|
|
2307
|
+
for (const id of nodeIds) {
|
|
2308
|
+
const combined = `${id} ${ast.nodes[id].kind}`.toLowerCase();
|
|
2309
|
+
if (combined.includes("bronze") || combined.includes("raw") || combined.includes("landing")) tierSet.add(1);
|
|
2310
|
+
else if (combined.includes("silver") || combined.includes("clean") || combined.includes("curated") || combined.includes("conformed")) tierSet.add(2);
|
|
2311
|
+
else if (combined.includes("gold") || combined.includes("agg") || combined.includes("mart") || combined.includes("analytics")) tierSet.add(3);
|
|
2312
|
+
else if (combined.includes("bi") || combined.includes("dash") || combined.includes("model") || combined.includes("app") || combined.includes("consumer") || combined.includes("user") || combined.includes("client")) tierSet.add(4);
|
|
2313
|
+
else tierSet.add(0);
|
|
2314
|
+
}
|
|
2315
|
+
const tierCount = Math.max(tierSet.size, 4);
|
|
2316
|
+
const tierW = Math.max(260, maxNodeW + 48);
|
|
2317
|
+
const requiredW = Math.max(SAFE * 2 + tierCount * tierW, titleW);
|
|
2318
|
+
const requiredH = titleBand + SAFE * 2 + Math.max(440, maxNodeH * 3 + 120) + bottomBand;
|
|
2319
|
+
autoW = Math.max(640, Math.min(2800, requiredW));
|
|
2320
|
+
autoH = Math.max(400, Math.min(1600, requiredH));
|
|
2090
2321
|
} else if (dtype === "tree") {
|
|
2091
2322
|
let measureSubtree2 = function(id) {
|
|
2092
2323
|
const kids = children.get(id) ?? [];
|
|
@@ -2127,15 +2358,16 @@ function computeAdaptiveDimensions(ast, edges) {
|
|
|
2127
2358
|
}
|
|
2128
2359
|
for (const id of nodeIds) if (!depth.has(id)) depth.set(id, 0);
|
|
2129
2360
|
const maxDepth = Math.max(...depth.values(), 0);
|
|
2130
|
-
const minLeafWidth = NODE_W +
|
|
2361
|
+
const minLeafWidth = Math.max(NODE_W + 36, maxNodeW + 36);
|
|
2131
2362
|
const subtreeSpan = /* @__PURE__ */ new Map();
|
|
2132
2363
|
let totalRootsWidth = 0;
|
|
2133
2364
|
const activeRoots = roots.length > 0 ? roots : nodeIds;
|
|
2134
2365
|
for (const r of activeRoots) totalRootsWidth += measureSubtree2(r);
|
|
2135
|
-
const
|
|
2136
|
-
const
|
|
2137
|
-
|
|
2138
|
-
|
|
2366
|
+
const levelH = Math.max(130, maxNodeH + 54);
|
|
2367
|
+
const requiredW = Math.max(SAFE * 2 + totalRootsWidth, titleW);
|
|
2368
|
+
const requiredH = titleBand + SAFE * 2 + (maxDepth + 1) * levelH + bottomBand;
|
|
2369
|
+
autoW = Math.max(480, Math.min(2560, requiredW));
|
|
2370
|
+
autoH = Math.max(320, Math.min(1800, requiredH));
|
|
2139
2371
|
} else if (dtype === "swimlane") {
|
|
2140
2372
|
const laneCount = Math.max(groupCount, 1);
|
|
2141
2373
|
const maxInLane = Math.max(
|
|
@@ -2143,37 +2375,56 @@ function computeAdaptiveDimensions(ast, edges) {
|
|
|
2143
2375
|
Math.ceil(nodeCount / laneCount),
|
|
2144
2376
|
1
|
|
2145
2377
|
);
|
|
2146
|
-
const
|
|
2147
|
-
const
|
|
2148
|
-
|
|
2149
|
-
|
|
2378
|
+
const stepX = Math.max(200, maxNodeW + 36);
|
|
2379
|
+
const laneH = Math.max(130, maxNodeH + 54);
|
|
2380
|
+
const requiredW = Math.max(SAFE * 2 + 140 + maxInLane * stepX, titleW);
|
|
2381
|
+
const requiredH = titleBand + SAFE * 2 + laneCount * laneH + bottomBand;
|
|
2382
|
+
autoW = Math.max(480, Math.min(2560, requiredW));
|
|
2383
|
+
autoH = Math.max(320, Math.min(1800, requiredH));
|
|
2150
2384
|
} else if (dtype === "pyramid") {
|
|
2151
2385
|
const tierCount = Math.max(nodeCount, 1);
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2386
|
+
const tierH = Math.max(100, maxNodeH + 28);
|
|
2387
|
+
autoW = Math.max(480, Math.min(2e3, Math.max(SAFE * 2 + maxNodeW * 2 + 100, titleW)));
|
|
2388
|
+
const requiredH = titleBand + SAFE * 2 + tierCount * tierH + bottomBand;
|
|
2389
|
+
autoH = Math.max(320, Math.min(1440, requiredH));
|
|
2155
2390
|
} else if (dtype === "layers") {
|
|
2156
2391
|
const layerCount = Math.max(nodeCount, 1);
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2392
|
+
const layerH = Math.max(90, maxNodeH + 18);
|
|
2393
|
+
autoW = Math.max(480, Math.min(2e3, Math.max(SAFE * 2 + maxNodeW + 160, titleW)));
|
|
2394
|
+
const requiredH = titleBand + SAFE * 2 + layerCount * layerH + bottomBand;
|
|
2395
|
+
autoH = Math.max(320, Math.min(1440, requiredH));
|
|
2160
2396
|
} else if (dtype === "timeline" || dtype === "gantt") {
|
|
2161
2397
|
const milestones = Math.max(nodeCount, 1);
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2398
|
+
let totalPhases = milestones;
|
|
2399
|
+
if (dtype === "gantt") {
|
|
2400
|
+
totalPhases = Math.max(...nodeIds.map((nid) => {
|
|
2401
|
+
const p = ast.nodes[nid].props.phase;
|
|
2402
|
+
const s = ast.nodes[nid].props.span;
|
|
2403
|
+
return (typeof p === "number" ? p : 0) + (typeof s === "number" ? s : 1);
|
|
2404
|
+
}), milestones);
|
|
2405
|
+
}
|
|
2406
|
+
const milestoneW = Math.max(220, maxNodeW + 36);
|
|
2407
|
+
const requiredW = Math.max(
|
|
2408
|
+
SAFE * 2 + (dtype === "gantt" ? totalPhases * Math.max(160, maxNodeW + 16) : milestones * milestoneW),
|
|
2409
|
+
titleW
|
|
2410
|
+
);
|
|
2411
|
+
const requiredH = dtype === "gantt" ? titleBand + SAFE * 2 + milestones * Math.max(76, maxNodeH + 16) + 40 + bottomBand : titleBand + SAFE * 2 + Math.max(260, maxNodeH * 2 + 80) + bottomBand;
|
|
2412
|
+
autoW = Math.max(480, Math.min(2560, requiredW));
|
|
2413
|
+
autoH = Math.max(320, Math.min(1600, requiredH));
|
|
2166
2414
|
} else if (dtype === "loop" || dtype === "flywheel" || dtype === "radar" || dtype === "venn" || dtype === "constellation") {
|
|
2167
2415
|
const N = Math.max(nodeCount, 3);
|
|
2168
|
-
const minRadius =
|
|
2169
|
-
const
|
|
2170
|
-
const
|
|
2171
|
-
const
|
|
2172
|
-
|
|
2173
|
-
|
|
2416
|
+
const minRadius = Math.max(160, maxNodeW * 0.75);
|
|
2417
|
+
const nodeFootprint = Math.max(160, maxNodeW + 16);
|
|
2418
|
+
const radiusNeeded = Math.max(minRadius, N * nodeFootprint / (2 * Math.PI));
|
|
2419
|
+
const requiredW = Math.max(SAFE * 2 + radiusNeeded * 2 + maxNodeW + 40, titleW);
|
|
2420
|
+
const requiredH = titleBand + SAFE * 2 + radiusNeeded * 2 + maxNodeH + 40 + bottomBand;
|
|
2421
|
+
autoW = Math.max(480, Math.min(2400, requiredW));
|
|
2422
|
+
autoH = Math.max(360, Math.min(1800, requiredH));
|
|
2174
2423
|
} else if (dtype === "quadrant") {
|
|
2175
|
-
|
|
2176
|
-
|
|
2424
|
+
const requiredW = Math.max(480, SAFE * 2 + maxNodeW * 2.5, titleW);
|
|
2425
|
+
const requiredH = titleBand + SAFE * 2 + maxNodeH * 3 + bottomBand;
|
|
2426
|
+
autoW = Math.min(2400, requiredW);
|
|
2427
|
+
autoH = Math.min(1600, requiredH);
|
|
2177
2428
|
} else {
|
|
2178
2429
|
const forceVertical = dtype === "flowchart" && isVertical || isVertical;
|
|
2179
2430
|
const ranks = assignRanks(nodeIds, effectiveEdges, forceVertical ? "TB" : "LR");
|
|
@@ -2186,47 +2437,26 @@ function computeAdaptiveDimensions(ast, edges) {
|
|
|
2186
2437
|
const rankCount = Math.max(...byRank.keys(), 0) + 1;
|
|
2187
2438
|
const maxInRank = Math.max(...[...byRank.values()].map((v) => v.length), 1);
|
|
2188
2439
|
const groupPaddingBonus = groupCount > 0 ? GROUP_PAD * 2 : 0;
|
|
2440
|
+
const maxRankW = maxNodeW;
|
|
2441
|
+
const maxRankH = maxNodeH;
|
|
2189
2442
|
if (forceVertical) {
|
|
2190
|
-
const
|
|
2191
|
-
const
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
} else {
|
|
2199
|
-
autoW = Math.max(960, Math.min(2400, requiredW));
|
|
2200
|
-
autoH = Math.max(720, Math.min(2e3, requiredH));
|
|
2201
|
-
}
|
|
2443
|
+
const colSpacing = 44;
|
|
2444
|
+
const rowGap = 56;
|
|
2445
|
+
const contentNeededW = maxInRank * maxRankW + Math.max(0, maxInRank - 1) * colSpacing + groupPaddingBonus;
|
|
2446
|
+
const requiredW = SAFE * 2 + contentNeededW;
|
|
2447
|
+
autoW = Math.max(480, Math.min(2400, Math.max(requiredW, titleW)));
|
|
2448
|
+
const contentNeededH = rankCount * maxRankH + Math.max(0, rankCount - 1) * rowGap + groupPaddingBonus;
|
|
2449
|
+
const requiredH = SAFE * 2 + titleBand + bottomBand + contentNeededH;
|
|
2450
|
+
autoH = Math.max(360, Math.min(3200, requiredH));
|
|
2202
2451
|
} else {
|
|
2203
|
-
const
|
|
2204
|
-
const
|
|
2205
|
-
const
|
|
2206
|
-
const
|
|
2207
|
-
const
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
if (maxInRank === 1 && !hasTitle) {
|
|
2212
|
-
autoW = Math.max(1024, Math.min(2560, requiredW));
|
|
2213
|
-
autoH = Math.max(hasBeatCaptions ? 240 : 208, Math.min(360, requiredH + 24));
|
|
2214
|
-
} else if (maxInRank === 2 && !hasTitle) {
|
|
2215
|
-
autoW = Math.max(1024, Math.min(2560, requiredW));
|
|
2216
|
-
autoH = Math.max(hasBeatCaptions ? 368 : 336, Math.min(480, requiredH + 24));
|
|
2217
|
-
} else if (maxInRank === 3 && !hasTitle) {
|
|
2218
|
-
autoW = Math.max(1152, Math.min(2560, requiredW));
|
|
2219
|
-
autoH = Math.max(hasBeatCaptions ? 480 : 448, Math.min(640, requiredH + 24));
|
|
2220
|
-
} else if (nodeCount <= 2 && rankCount <= 2) {
|
|
2221
|
-
autoW = 1024;
|
|
2222
|
-
autoH = hasTitle ? 576 : 384;
|
|
2223
|
-
} else if (nodeCount <= 4 && rankCount <= 3 && maxInRank <= 2) {
|
|
2224
|
-
autoW = 1152;
|
|
2225
|
-
autoH = hasTitle ? 648 : 448;
|
|
2226
|
-
} else {
|
|
2227
|
-
autoW = Math.max(1152, Math.min(2560, requiredW));
|
|
2228
|
-
autoH = Math.max(hasTitle ? 648 : maxInRank <= 2 ? 448 : 576, Math.min(1600, requiredH));
|
|
2229
|
-
}
|
|
2452
|
+
const colGap = 56;
|
|
2453
|
+
const rowGap = 44;
|
|
2454
|
+
const contentNeededW = rankCount * maxRankW + Math.max(0, rankCount - 1) * colGap + groupPaddingBonus;
|
|
2455
|
+
const requiredW = SAFE * 2 + contentNeededW;
|
|
2456
|
+
const contentNeededH = maxInRank * maxRankH + Math.max(0, maxInRank - 1) * rowGap + groupPaddingBonus;
|
|
2457
|
+
const requiredH = SAFE * 2 + titleBand + bottomBand + contentNeededH;
|
|
2458
|
+
autoW = Math.max(480, Math.min(2560, Math.max(requiredW, titleW)));
|
|
2459
|
+
autoH = Math.max(240, Math.min(1800, requiredH));
|
|
2230
2460
|
}
|
|
2231
2461
|
}
|
|
2232
2462
|
return {
|
|
@@ -3365,7 +3595,8 @@ function parse(source, opts = {}) {
|
|
|
3365
3595
|
fps: 60,
|
|
3366
3596
|
theme: "paper",
|
|
3367
3597
|
explicitTheme: false,
|
|
3368
|
-
direction: "LR"
|
|
3598
|
+
direction: "LR",
|
|
3599
|
+
explicitDirection: false
|
|
3369
3600
|
};
|
|
3370
3601
|
const styles = {};
|
|
3371
3602
|
const nodes = {};
|
|
@@ -3466,10 +3697,11 @@ function parse(source, opts = {}) {
|
|
|
3466
3697
|
meta.title = title;
|
|
3467
3698
|
remainder = str.rest;
|
|
3468
3699
|
}
|
|
3469
|
-
const inlineLayout = remainder.match(/\
|
|
3700
|
+
const inlineLayout = remainder.match(/\b(?:layout|direction|rankdir)\s+(LR|RL|TB|BT)\b/i);
|
|
3470
3701
|
if (inlineLayout) {
|
|
3471
3702
|
meta.direction = inlineLayout[1].toUpperCase();
|
|
3472
|
-
|
|
3703
|
+
meta.explicitDirection = true;
|
|
3704
|
+
remainder = remainder.replace(/\b(?:layout|direction|rankdir)\s+(LR|RL|TB|BT)\b/i, " ");
|
|
3473
3705
|
}
|
|
3474
3706
|
const props = parseProps(remainder);
|
|
3475
3707
|
for (const [k, v] of Object.entries(props)) {
|
|
@@ -3490,8 +3722,10 @@ function parse(source, opts = {}) {
|
|
|
3490
3722
|
const val = String(v).toLowerCase();
|
|
3491
3723
|
meta.theme = val;
|
|
3492
3724
|
meta.explicitTheme = val !== "auto";
|
|
3493
|
-
} else if (k === "direction" || k === "layout"
|
|
3494
|
-
|
|
3725
|
+
} else if (k === "direction" || k === "layout" || k === "rankdir") {
|
|
3726
|
+
meta.direction = String(v).toUpperCase();
|
|
3727
|
+
meta.explicitDirection = true;
|
|
3728
|
+
} else if (PLAYER_FLAT_KEY_SET.has(k)) {
|
|
3495
3729
|
const error = applyPlayerSetting(meta.player ??= {}, "player", k, String(v));
|
|
3496
3730
|
if (error) diagnostics.push({ severity: "warning", message: error, line: lineNo });
|
|
3497
3731
|
} else if (k === "type") {
|
|
@@ -3500,8 +3734,9 @@ function parse(source, opts = {}) {
|
|
|
3500
3734
|
diagnostics.push({ severity: "warning", message: `unknown diagram type '${v}'`, line: lineNo });
|
|
3501
3735
|
} else {
|
|
3502
3736
|
meta.type = t;
|
|
3503
|
-
if (t === "flowchart" && !props.direction && !props.layout && !inlineLayout) {
|
|
3737
|
+
if (t === "flowchart" && !props.direction && !props.layout && !props.rankdir && !inlineLayout) {
|
|
3504
3738
|
meta.direction = "TB";
|
|
3739
|
+
meta.explicitDirection = true;
|
|
3505
3740
|
}
|
|
3506
3741
|
}
|
|
3507
3742
|
}
|
|
@@ -3528,6 +3763,7 @@ function parse(source, opts = {}) {
|
|
|
3528
3763
|
}
|
|
3529
3764
|
if (/^layout\s+(LR|RL|TB|BT)\b/i.test(line)) {
|
|
3530
3765
|
meta.direction = line.split(/\s+/)[1].toUpperCase();
|
|
3766
|
+
meta.explicitDirection = true;
|
|
3531
3767
|
i++;
|
|
3532
3768
|
continue;
|
|
3533
3769
|
}
|
|
@@ -5659,7 +5895,7 @@ function diagnoseMarkdyCode(code, options = {}) {
|
|
|
5659
5895
|
});
|
|
5660
5896
|
}
|
|
5661
5897
|
}
|
|
5662
|
-
const layoutMatch = trimmed.match(/\
|
|
5898
|
+
const layoutMatch = trimmed.match(/\b(?:layout|direction|rankdir)\s*=?\s*(\w+)/i);
|
|
5663
5899
|
if (layoutMatch) {
|
|
5664
5900
|
const layoutVal = layoutMatch[1].toUpperCase();
|
|
5665
5901
|
if (!LAYOUT_DIRECTIONS.includes(layoutVal)) {
|
|
@@ -8725,6 +8961,7 @@ export {
|
|
|
8725
8961
|
compilePlan,
|
|
8726
8962
|
compressMarkdyToUrlHash,
|
|
8727
8963
|
computeAdaptiveDimensions,
|
|
8964
|
+
computeNodeDimensions,
|
|
8728
8965
|
damerauLevenshteinDistance,
|
|
8729
8966
|
decompressMarkdyFromUrlHash,
|
|
8730
8967
|
detectArchitectureDrift,
|
package/package.json
CHANGED