@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 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 computeNodeDimensions(decl, baseW = 180, baseH = 76) {
808
- if (typeof decl.props?.width === "number") {
809
- return {
810
- width: decl.props.width,
811
- height: typeof decl.props.height === "number" ? decl.props.height : baseH
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
- const labelLen = (decl.label || "").length;
815
- const tech = decl.props?.tech ?? decl.props?.sub;
816
- const techLen = tech ? String(tech).length : 0;
817
- const val = decl.props?.value ?? decl.props?.metric;
818
- const valLen = val ? String(val).length : 0;
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 valueW = valLen > 0 ? Math.max(50, valLen * 9.5 + 16) : 0;
822
- const words = (decl.label || "").trim().split(/\s+/).filter(Boolean);
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 avgLineChars = words.length > 1 ? Math.ceil(labelLen / Math.min(words.length, 2)) : labelLen;
825
- const neededLabelChars = Math.max(longestWord + 2, avgLineChars, Math.min(labelLen, 22));
826
- const maxChars = Math.max(neededLabelChars, techLen);
827
- const neededTextW = Math.max(96, maxChars * 8.4);
828
- const calculatedW = 56 + neededTextW + (valueW > 0 ? valueW + 14 : 0) + 18;
829
- const minW = Math.max(baseW, Math.min(360, calculatedW));
830
- let width = Math.ceil(minW / 8) * 8;
831
- let height = baseH;
832
- if (labelLen > 24 && techLen > 0) {
833
- height = Math.max(height, 84);
834
- }
835
- return { width, height };
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
- const minLeafWidth = NODE_W + 48;
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, minLeafWidth);
1163
- return minLeafWidth;
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(minLeafWidth, total);
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(NODE_H + 48, Math.min(160, contentH / Math.max(maxDepth + 1, 1)));
1181
- const totalTreeHeight = maxDepth * levelHeight + NODE_H;
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) ?? minLeafWidth;
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 - NODE_W / 2, y });
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) ?? minLeafWidth;
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 parentCenterX = (firstChild.x + NODE_W / 2 + lastChild.x + NODE_W / 2) / 2;
1203
- nodePositions.set(id, { x: parentCenterX - NODE_W / 2, y });
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) ?? minLeafWidth;
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: NODE_W,
1223
- height: NODE_H,
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: "card",
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 - NODE_W / 2 - SAFE);
1274
- const radiusY = Math.max(120, contentH / 2 - NODE_H / 2 - SAFE);
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 ? centerX : SAFE + contentW / 2 + Math.cos(angle) * radiusX - NODE_W / 2;
1282
- const y = isFocal ? centerY : TITLE_BAND + contentH / 2 + Math.sin(angle) * radiusY - NODE_H / 2;
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: NODE_W,
1291
- height: NODE_H,
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 - NODE_W) / 2 - 32));
1314
- const radiusY = Math.min(contentH * 0.38, Math.max(150, (contentH - NODE_H) / 2 - 32));
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 - NODE_W * 1.25 / 2;
1323
- y = centerY - NODE_H * 1.15 / 2;
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 - NODE_W / 2;
1328
- y = centerY + Math.sin(angle) * radiusY - NODE_H / 2;
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: isHub ? snapGrid(NODE_W * 1.25) : NODE_W,
1339
- height: isHub ? snapGrid(NODE_H * 1.15) : NODE_H,
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 colGap = Math.max(NODE_W + 48, contentW / (tierCount + 1));
1372
- const totalW = (tierCount - 1) * colGap + NODE_W;
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(NODE_H + 32, contentH / (colCount + 1));
1379
- const colH = (colCount - 1) * rowSpacing + NODE_H;
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 rowY = startY + rowIdx * rowSpacing;
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(colX),
1520
+ x: snapGrid(nodeX),
1391
1521
  y: snapGrid(rowY),
1392
- width: NODE_W,
1393
- height: NODE_H,
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 offsetCount = ids.length;
1443
- const rowOffset = (idx - (offsetCount - 1) / 2) * (NODE_H + 20);
1444
- const x = center.x - NODE_W / 2;
1445
- const y = center.y + rowOffset - NODE_H / 2;
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: NODE_W,
1455
- height: NODE_H,
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 colSpacing = Math.max(NODE_W + 48, Math.min(260, contentW / Math.max(count + 1, 2)));
1494
- const totalW = (count - 1) * colSpacing + NODE_W;
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 colX = startX + colIdx * colSpacing;
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: NODE_W,
1508
- height: NODE_H,
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 totalH = tierCount * NODE_H + (tierCount - 1) * 24;
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 * (NODE_H + 24);
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 nodeW = Math.max(NODE_W, Math.min(tierWidth / count - 12, 480));
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 x = tierStartX + idx * (nodeW + 16);
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(nodeW),
1557
- height: NODE_H,
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 x = SAFE + spacing * idx + (spacing - NODE_W) / 2;
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 - NODE_H - 24 : baselineY + 24;
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: NODE_W,
1590
- height: NODE_H,
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 - TITLE_BAND - SAFE;
1767
+ const contentH = ast.meta.height - SAFE - topBand - SAFE;
1605
1768
  const N = nodeIds.length;
1606
- const rowH = Math.min(68, Math.max(52, (contentH - 24) / Math.max(N, 1)));
1607
- const barH = Math.min(44, rowH - 16);
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 = TITLE_BAND + Math.max(16, (contentH - totalH) / 2);
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, NODE_W);
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 radius = Math.min(contentW, contentH) * 0.24;
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) - VENN_NODE_SIZE / 2;
1658
- y = centerY - VENN_NODE_SIZE / 2;
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) - VENN_NODE_SIZE / 2;
1662
- y = centerY + radius * 0.85 * Math.sin(angle) - VENN_NODE_SIZE / 2;
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: VENN_NODE_SIZE,
1673
- height: VENN_NODE_SIZE,
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 layerH = Math.min(Math.max((contentH - (N - 1) * 14) / N, 52), 68);
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 layerW = Math.min(contentW, 880);
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(layerH),
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) - NODE_W / 2;
1778
- const y = centerY + radius * Math.sin(angle) - NODE_H / 2;
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: NODE_W,
1788
- height: NODE_H,
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: false });
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: false });
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 isSequence = ast.meta.type === "sequence";
1944
- if (!hasShowCue && Object.keys(ast.nodes).length > 0) {
1945
- const nodesToReveal = !isSequence && flowNodeIds.size > 0 ? Object.keys(ast.nodes).filter((id) => !flowNodeIds.has(id)) : Object.keys(ast.nodes);
1946
- if (nodesToReveal.length > 0) {
1947
- cues.push({
1948
- start: 0,
1949
- duration: DEFAULTS.show,
1950
- kind: "show",
1951
- targets: nodesToReveal,
1952
- params: { stagger: DEFAULTS.stagger },
1953
- beat: "__intro"
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 + NODE_H + 56;
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
- let autoW = 1280;
2078
- let autoH = 720;
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 = 1280;
2081
- autoH = 720;
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 requiredW = SAFE * 2 + Math.max(count * 220, 800);
2087
- const requiredH = TITLE_BAND + NODE_H + 56 + Math.max(flowCount * 76, 280) + SAFE + 48;
2088
- autoW = Math.max(1088, Math.min(2560, requiredW));
2089
- autoH = Math.max(640, Math.min(1800, requiredH));
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 + 48;
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 requiredW = SAFE * 2 + totalRootsWidth;
2136
- const requiredH = TITLE_BAND + SAFE * 2 + (maxDepth + 1) * 140;
2137
- autoW = Math.max(1120, Math.min(2560, requiredW));
2138
- autoH = Math.max(640, Math.min(1600, requiredH));
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 requiredW = SAFE * 2 + 140 + maxInLane * 220;
2147
- const requiredH = TITLE_BAND + SAFE * 2 + laneCount * 140;
2148
- autoW = Math.max(1152, Math.min(2560, requiredW));
2149
- autoH = Math.max(640, Math.min(1600, requiredH));
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
- autoW = 1280;
2153
- const requiredH = TITLE_BAND + SAFE * 2 + tierCount * 110;
2154
- autoH = Math.max(640, Math.min(1440, requiredH));
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
- autoW = 1280;
2158
- const requiredH = TITLE_BAND + SAFE * 2 + layerCount * 96;
2159
- autoH = Math.max(640, Math.min(1440, requiredH));
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
- const requiredW = SAFE * 2 + milestones * 240;
2163
- const requiredH = TITLE_BAND + SAFE * 2 + 380;
2164
- autoW = Math.max(1200, Math.min(2560, requiredW));
2165
- autoH = Math.max(640, Math.min(1200, requiredH));
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 = 200;
2169
- const radiusNeeded = Math.max(minRadius, N * 180 / (2 * Math.PI));
2170
- const requiredW = SAFE * 2 + radiusNeeded * 2 + 240;
2171
- const requiredH = TITLE_BAND + SAFE * 2 + radiusNeeded * 2 + 140;
2172
- autoW = Math.max(1088, Math.min(2200, requiredW));
2173
- autoH = Math.max(720, Math.min(1600, requiredH));
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
- autoW = 1200;
2176
- autoH = 800;
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 requiredW = SAFE * 2 + maxInRank * NODE_W + (maxInRank - 1) * 44 + groupPaddingBonus;
2191
- const requiredH = SAFE * 2 + TITLE_BAND + rankCount * NODE_H + (rankCount - 1) * 56 + groupPaddingBonus;
2192
- if (nodeCount <= 2) {
2193
- autoW = 960;
2194
- autoH = 640;
2195
- } else if (nodeCount <= 4 && rankCount <= 3) {
2196
- autoW = 1024;
2197
- autoH = 720;
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 hasTitle = Boolean(ast.meta.title && ast.meta.title.trim().length > 0);
2204
- const hasBeatCaptions = ast.beats.some((b) => b.label && b.label.trim().length > 0);
2205
- const titleBand = hasTitle ? TITLE_BAND : 0;
2206
- const bottomBand = hasBeatCaptions ? 44 : 0;
2207
- const maxRankW = Math.max(...nodeIds.map((id) => computeNodeDimensions(ast.nodes[id]).width));
2208
- const maxRankH = Math.max(...nodeIds.map((id) => computeNodeDimensions(ast.nodes[id]).height));
2209
- const requiredW = SAFE * 2 + rankCount * maxRankW + (rankCount - 1) * 56 + groupPaddingBonus;
2210
- const requiredH = SAFE * 2 + titleBand + bottomBand + maxInRank * maxRankH + (maxInRank - 1) * 44 + groupPaddingBonus;
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(/\blayout\s+(LR|RL|TB|BT)\b/i);
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
- remainder = remainder.replace(/\blayout\s+(LR|RL|TB|BT)\b/i, " ");
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") meta.direction = String(v).toUpperCase();
3494
- else if (PLAYER_FLAT_KEY_SET.has(k)) {
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(/\blayout\s*=?\s*(\w+)/i);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/core",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Diagram-native MarkdyScript parser and compiler (auto-layout, edge routing, cue scheduling) — zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",