@markdy/core 1.3.0 → 1.3.2

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,
@@ -804,35 +805,123 @@ var NODE_W = 180;
804
805
  var NODE_H = 76;
805
806
  var VENN_NODE_SIZE = 220;
806
807
  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
- };
808
+ function estimateTextLines(words, charsPerLine) {
809
+ if (words.length === 0) return 0;
810
+ let lines = 1;
811
+ let curLen = 0;
812
+ for (const w of words) {
813
+ if (curLen === 0) {
814
+ curLen = w.length;
815
+ } else if (curLen + 1 + w.length <= charsPerLine) {
816
+ curLen += 1 + w.length;
817
+ } else {
818
+ lines++;
819
+ curLen = w.length;
820
+ }
813
821
  }
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;
822
+ return lines;
823
+ }
824
+ function measureLabelLines(rawLabel, words, charsPerLine) {
825
+ if (words.length === 0) return 0;
826
+ if (!rawLabel.includes("\n")) {
827
+ return Math.max(1, estimateTextLines(words, charsPerLine));
828
+ }
829
+ const explicitLines = rawLabel.split("\n");
830
+ let total = 0;
831
+ for (const line of explicitLines) {
832
+ const lineWords = line.trim().split(/\s+/).filter(Boolean);
833
+ total += Math.max(1, estimateTextLines(lineWords, charsPerLine));
834
+ }
835
+ return Math.max(explicitLines.length, total);
836
+ }
837
+ function computeNodeDimensions(decl, baseW = 180, baseH = 76) {
819
838
  if (decl.kind === "dot") return { width: 64, height: 64 };
820
839
  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);
840
+ const rawLabel = (decl.label || "").trim();
841
+ const labelLen = rawLabel.length;
842
+ const words = rawLabel.split(/\s+/).filter(Boolean);
823
843
  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 };
844
+ const tech = decl.props?.tech ?? decl.props?.sub;
845
+ const techStr = tech ? String(tech).trim() : "";
846
+ const techLen = techStr.length;
847
+ const val = decl.props?.value ?? decl.props?.metric;
848
+ const valStr = val ? String(val).trim() : "";
849
+ const valLen = valStr.length;
850
+ const leftSpace = 56;
851
+ const valueW = valLen > 0 ? Math.max(50, valLen * 9.5 + 16) : 0;
852
+ const rightSpace = valueW > 0 ? valueW + 14 : 18;
853
+ const isDiamond = decl.props?.shape === "diamond" || decl.kind === "decision" || decl.kind === "condition";
854
+ const isCircle = decl.props?.shape === "circle";
855
+ const isPill = decl.props?.shape === "pill";
856
+ const effectiveBaseW = isDiamond ? Math.max(baseW, 208) : isCircle ? Math.max(baseW, 140) : baseW;
857
+ const effectiveBaseH = isDiamond ? Math.max(baseH, 88) : isCircle ? Math.max(baseH, 140) : baseH;
858
+ const neededTechW = techLen > 0 ? Math.ceil(techLen * 6.6 + 16) : 0;
859
+ const longestWordW = Math.ceil(longestWord * 8.4 + 4);
860
+ const minColW = Math.max(96, longestWordW, neededTechW);
861
+ if (isCircle) {
862
+ const targetTextW2 = Math.max(minColW, Math.min(220, Math.ceil(labelLen * 7.5)));
863
+ const charsPerLine2 = Math.max(longestWord, Math.floor(targetTextW2 / 8));
864
+ const numLines2 = Math.max(words.length > 0 ? 1 : 0, measureLabelLines(rawLabel, words, charsPerLine2));
865
+ const labelH2 = numLines2 * 16;
866
+ const techH2 = techLen > 0 ? 20 : 0;
867
+ const circleContentH = 28 + labelH2 + techH2;
868
+ const circleContentW = Math.max(targetTextW2, 60);
869
+ const radiusNeeded = Math.sqrt((circleContentW / 2) ** 2 + (circleContentH / 2) ** 2) + 20;
870
+ const diameter = Math.max(effectiveBaseW, effectiveBaseH, Math.ceil(radiusNeeded * 2 / 8) * 8);
871
+ return {
872
+ width: typeof decl.props?.width === "number" ? Math.max(decl.props.width, diameter) : diameter,
873
+ height: typeof decl.props?.height === "number" ? Math.max(decl.props.height, diameter) : diameter
874
+ };
875
+ }
876
+ if (isDiamond) {
877
+ let targetTextW2;
878
+ if (labelLen <= 12) {
879
+ targetTextW2 = Math.max(minColW, Math.ceil(labelLen * 8));
880
+ } else if (labelLen <= 28) {
881
+ targetTextW2 = Math.max(minColW, Math.min(180, Math.ceil(labelLen / 2 * 8.2 + 8)));
882
+ } else {
883
+ targetTextW2 = Math.max(minColW, Math.min(260, Math.ceil(labelLen / 3 * 8.2 + 12)));
884
+ }
885
+ const charsPerLine2 = Math.max(longestWord, Math.floor(targetTextW2 / 8));
886
+ const numLines2 = Math.max(words.length > 0 ? 1 : 0, measureLabelLines(rawLabel, words, charsPerLine2));
887
+ const labelH2 = numLines2 * 16;
888
+ const techH2 = techLen > 0 ? 20 : 0;
889
+ const diamondContentH = 24 + labelH2 + techH2;
890
+ const neededH2 = Math.max(effectiveBaseH, Math.ceil(diamondContentH / 0.58 / 8) * 8);
891
+ const neededW = Math.max(effectiveBaseW, Math.ceil((targetTextW2 + 32) / 0.52 / 8) * 8);
892
+ return {
893
+ width: typeof decl.props?.width === "number" ? Math.max(decl.props.width, neededW) : neededW,
894
+ height: typeof decl.props?.height === "number" ? Math.max(decl.props.height, neededH2) : neededH2
895
+ };
896
+ }
897
+ let targetTextW;
898
+ if (labelLen <= 14) {
899
+ targetTextW = Math.max(minColW, Math.ceil(labelLen * 8.4));
900
+ } else if (labelLen <= 30) {
901
+ targetTextW = Math.max(minColW, Math.min(220, Math.ceil(labelLen * 8.4)));
902
+ } else if (labelLen <= 54) {
903
+ targetTextW = Math.max(minColW, Math.min(260, Math.max(180, Math.ceil(labelLen / 2 * 8.4 + 8))));
904
+ } else if (labelLen <= 84) {
905
+ targetTextW = Math.max(minColW, Math.min(300, Math.max(220, Math.ceil(labelLen / 3 * 8.4 + 12))));
906
+ } else {
907
+ targetTextW = Math.max(minColW, Math.min(360, Math.max(250, Math.ceil(labelLen / 4 * 8.4 + 16))));
908
+ }
909
+ const charsPerLine = Math.max(longestWord, Math.floor((targetTextW - 4) / 8.2));
910
+ const numLines = Math.max(words.length > 0 ? 1 : 0, measureLabelLines(rawLabel, words, charsPerLine));
911
+ const labelLineH = 18;
912
+ const labelH = numLines * labelLineH;
913
+ const techH = techLen > 0 ? 22 : 0;
914
+ const padV = 28;
915
+ const neededContentH = Math.max(28, labelH + techH);
916
+ const neededH = neededContentH + padV;
917
+ const height = Math.max(effectiveBaseH, Math.ceil(neededH / 8) * 8);
918
+ const pillPad = isPill ? 24 : 0;
919
+ const calculatedW = leftSpace + targetTextW + rightSpace + pillPad;
920
+ const width = Math.max(effectiveBaseW, Math.ceil(calculatedW / 8) * 8);
921
+ return {
922
+ width: typeof decl.props?.width === "number" ? Math.max(decl.props.width, width) : width,
923
+ height: typeof decl.props?.height === "number" ? Math.max(decl.props.height, height) : height
924
+ };
836
925
  }
837
926
  function resolveNodeStyle(decl, ast) {
838
927
  if (!decl.style) return void 0;
@@ -853,6 +942,9 @@ var DEFAULTS = {
853
942
  function diagramType(ast) {
854
943
  return ast.meta.type ?? "architecture";
855
944
  }
945
+ function effectiveTitleBand(ast) {
946
+ return ast.meta.title && ast.meta.title.trim().length > 0 ? TITLE_BAND : 16;
947
+ }
856
948
  function nodeShape(kind, dtype, props) {
857
949
  if (typeof props?.shape === "string") {
858
950
  const s = props.shape.toLowerCase();
@@ -1127,6 +1219,11 @@ function layoutRanked(ast, edges, opts) {
1127
1219
  function layoutTree(ast, edges) {
1128
1220
  const nodeIds = Object.keys(ast.nodes);
1129
1221
  if (nodeIds.length === 0) return [];
1222
+ const nodeDims = /* @__PURE__ */ new Map();
1223
+ for (const id of nodeIds) {
1224
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1225
+ }
1226
+ const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height), NODE_H);
1130
1227
  const children = /* @__PURE__ */ new Map();
1131
1228
  const parent = /* @__PURE__ */ new Map();
1132
1229
  for (const id of nodeIds) children.set(id, []);
@@ -1155,18 +1252,22 @@ function layoutTree(ast, edges) {
1155
1252
  for (const id of nodeIds) if (!depth.has(id)) depth.set(id, 0);
1156
1253
  const maxDepth = Math.max(...depth.values(), 0);
1157
1254
  const subtreeSpan = /* @__PURE__ */ new Map();
1158
- const minLeafWidth = NODE_W + 48;
1255
+ function leafSpan(id) {
1256
+ const w = nodeDims.get(id)?.width ?? NODE_W;
1257
+ return w + 36;
1258
+ }
1159
1259
  function measureSubtree(id) {
1160
1260
  const kids = children.get(id) ?? [];
1261
+ const minSpan = leafSpan(id);
1161
1262
  if (kids.length === 0) {
1162
- subtreeSpan.set(id, minLeafWidth);
1163
- return minLeafWidth;
1263
+ subtreeSpan.set(id, minSpan);
1264
+ return minSpan;
1164
1265
  }
1165
1266
  let total = 0;
1166
1267
  for (const kid of kids) {
1167
1268
  total += measureSubtree(kid);
1168
1269
  }
1169
- const span = Math.max(minLeafWidth, total);
1270
+ const span = Math.max(minSpan, total);
1170
1271
  subtreeSpan.set(id, span);
1171
1272
  return span;
1172
1273
  }
@@ -1177,34 +1278,37 @@ function layoutTree(ast, edges) {
1177
1278
  const contentW = ast.meta.width - SAFE * 2;
1178
1279
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1179
1280
  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;
1281
+ const levelHeight = Math.max(maxNodeH + 40, Math.min(220, contentH / Math.max(maxDepth + 1, 1)));
1282
+ const totalTreeHeight = maxDepth * levelHeight + maxNodeH;
1182
1283
  const treeStartY = TITLE_BAND + Math.max(16, (contentH - totalTreeHeight) / 2);
1183
1284
  const nodePositions = /* @__PURE__ */ new Map();
1184
1285
  function positionSubtree(id, leftX) {
1185
1286
  const d = depth.get(id) ?? 0;
1186
1287
  const y = treeStartY + d * levelHeight;
1187
1288
  const kids = children.get(id) ?? [];
1188
- const span = subtreeSpan.get(id) ?? minLeafWidth;
1289
+ const span = subtreeSpan.get(id) ?? leafSpan(id);
1290
+ const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
1189
1291
  if (kids.length === 0) {
1190
1292
  const centerX = leftX + span / 2;
1191
- nodePositions.set(id, { x: centerX - NODE_W / 2, y });
1293
+ nodePositions.set(id, { x: centerX - dims.width / 2, y });
1192
1294
  return;
1193
1295
  }
1194
1296
  let curX = leftX;
1195
1297
  for (const kid of kids) {
1196
- const kidSpan = subtreeSpan.get(kid) ?? minLeafWidth;
1298
+ const kidSpan = subtreeSpan.get(kid) ?? leafSpan(kid);
1197
1299
  positionSubtree(kid, curX);
1198
1300
  curX += kidSpan;
1199
1301
  }
1200
1302
  const firstChild = nodePositions.get(kids[0]);
1201
1303
  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 });
1304
+ const firstDims = nodeDims.get(kids[0]) ?? { width: NODE_W, height: NODE_H };
1305
+ const lastDims = nodeDims.get(kids[kids.length - 1]) ?? { width: NODE_W, height: NODE_H };
1306
+ const parentCenterX = (firstChild.x + firstDims.width / 2 + lastChild.x + lastDims.width / 2) / 2;
1307
+ nodePositions.set(id, { x: parentCenterX - dims.width / 2, y });
1204
1308
  }
1205
1309
  let currentRootLeft = treeStartX;
1206
1310
  for (const r of roots) {
1207
- const span = subtreeSpan.get(r) ?? minLeafWidth;
1311
+ const span = subtreeSpan.get(r) ?? leafSpan(r);
1208
1312
  positionSubtree(r, currentRootLeft);
1209
1313
  currentRootLeft += span;
1210
1314
  }
@@ -1212,6 +1316,7 @@ function layoutTree(ast, edges) {
1212
1316
  for (const id of nodeIds) {
1213
1317
  const decl = ast.nodes[id];
1214
1318
  const pos = nodePositions.get(id) ?? { x: SAFE, y: TITLE_BAND };
1319
+ const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
1215
1320
  nodes.push({
1216
1321
  id,
1217
1322
  kind: decl.kind,
@@ -1219,12 +1324,12 @@ function layoutTree(ast, edges) {
1219
1324
  label: decl.label,
1220
1325
  x: snapGrid(pos.x),
1221
1326
  y: snapGrid(pos.y),
1222
- width: NODE_W,
1223
- height: NODE_H,
1327
+ width: dims.width,
1328
+ height: dims.height,
1224
1329
  style: resolveNodeStyle(decl, ast),
1225
1330
  props: decl.props,
1226
1331
  opacity: 0,
1227
- shape: "card",
1332
+ shape: nodeShape(decl.kind, "tree", decl.props),
1228
1333
  focal: decl.props.focal === true || decl.props.accent === true
1229
1334
  });
1230
1335
  }
@@ -1260,6 +1365,12 @@ function cycleSafeEdges(nodeIds, edges) {
1260
1365
  function layoutConstellation(ast) {
1261
1366
  const nodeIds = Object.keys(ast.nodes);
1262
1367
  if (nodeIds.length === 0) return [];
1368
+ const nodeDims = /* @__PURE__ */ new Map();
1369
+ for (const id of nodeIds) {
1370
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1371
+ }
1372
+ const maxNodeW = Math.max(...nodeIds.map((id) => nodeDims.get(id).width), NODE_W);
1373
+ const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height), NODE_H);
1263
1374
  const incoming = /* @__PURE__ */ new Set();
1264
1375
  for (const edge of ast.edges) {
1265
1376
  if (edge.kind !== "response" && edge.from !== edge.to && ast.nodes[edge.to]) incoming.add(edge.to);
@@ -1267,19 +1378,18 @@ function layoutConstellation(ast) {
1267
1378
  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
1379
  const contentW = ast.meta.width - SAFE * 2;
1269
1380
  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
1381
  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);
1382
+ const radiusX = Math.max(140, contentW / 2 - maxNodeW / 2 - SAFE);
1383
+ const radiusY = Math.max(120, contentH / 2 - maxNodeH / 2 - SAFE);
1275
1384
  const nodes = [];
1276
1385
  for (const id of nodeIds) {
1277
1386
  const decl = ast.nodes[id];
1387
+ const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
1278
1388
  const isFocal = id === focalId;
1279
1389
  const index = orbitIds.indexOf(id);
1280
1390
  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;
1391
+ const x = isFocal ? SAFE + contentW / 2 - dims.width / 2 : SAFE + contentW / 2 + Math.cos(angle) * radiusX - dims.width / 2;
1392
+ const y = isFocal ? TITLE_BAND + contentH / 2 - dims.height / 2 : TITLE_BAND + contentH / 2 + Math.sin(angle) * radiusY - dims.height / 2;
1283
1393
  nodes.push({
1284
1394
  id,
1285
1395
  kind: decl.kind,
@@ -1287,8 +1397,8 @@ function layoutConstellation(ast) {
1287
1397
  label: decl.label,
1288
1398
  x: snapGrid(x),
1289
1399
  y: snapGrid(y),
1290
- width: NODE_W,
1291
- height: NODE_H,
1400
+ width: dims.width,
1401
+ height: dims.height,
1292
1402
  style: resolveNodeStyle(decl, ast),
1293
1403
  props: decl.props,
1294
1404
  opacity: 0,
@@ -1301,6 +1411,12 @@ function layoutConstellation(ast) {
1301
1411
  function layoutLoop(ast) {
1302
1412
  const nodeIds = Object.keys(ast.nodes);
1303
1413
  if (nodeIds.length === 0) return [];
1414
+ const nodeDims = /* @__PURE__ */ new Map();
1415
+ for (const id of nodeIds) {
1416
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1417
+ }
1418
+ const maxNodeW = Math.max(...nodeIds.map((id) => nodeDims.get(id).width), NODE_W);
1419
+ const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height), NODE_H);
1304
1420
  const hubId = nodeIds.find((id) => {
1305
1421
  const decl = ast.nodes[id];
1306
1422
  return decl.kind === "hub" || decl.props.hub === true || /^(hub|state|memory|shared_memory|core)$/i.test(id);
@@ -1310,22 +1426,25 @@ function layoutLoop(ast) {
1310
1426
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1311
1427
  const centerX = SAFE + contentW / 2;
1312
1428
  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));
1429
+ const radiusX = Math.min(contentW * 0.4, Math.max(220, (contentW - maxNodeW) / 2 - 32));
1430
+ const radiusY = Math.min(contentH * 0.38, Math.max(150, (contentH - maxNodeH) / 2 - 32));
1315
1431
  const nodes = [];
1316
1432
  for (const id of nodeIds) {
1317
1433
  const decl = ast.nodes[id];
1318
1434
  const isHub = id === hubId;
1435
+ const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
1436
+ const w = isHub ? Math.max(dims.width, snapGrid(NODE_W * 1.25)) : dims.width;
1437
+ const h = isHub ? Math.max(dims.height, snapGrid(NODE_H * 1.15)) : dims.height;
1319
1438
  let x;
1320
1439
  let y;
1321
1440
  if (isHub) {
1322
- x = centerX - NODE_W * 1.25 / 2;
1323
- y = centerY - NODE_H * 1.15 / 2;
1441
+ x = centerX - w / 2;
1442
+ y = centerY - h / 2;
1324
1443
  } else {
1325
1444
  const idx = stationIds.indexOf(id);
1326
1445
  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;
1446
+ x = centerX + Math.cos(angle) * radiusX - w / 2;
1447
+ y = centerY + Math.sin(angle) * radiusY - h / 2;
1329
1448
  }
1330
1449
  const focal = isHub || decl.props.focal === true || decl.props.accent === true;
1331
1450
  nodes.push({
@@ -1335,8 +1454,8 @@ function layoutLoop(ast) {
1335
1454
  label: decl.label,
1336
1455
  x: snapGrid(x),
1337
1456
  y: snapGrid(y),
1338
- width: isHub ? snapGrid(NODE_W * 1.25) : NODE_W,
1339
- height: isHub ? snapGrid(NODE_H * 1.15) : NODE_H,
1457
+ width: w,
1458
+ height: h,
1340
1459
  style: resolveNodeStyle(decl, ast),
1341
1460
  props: decl.props,
1342
1461
  opacity: 0,
@@ -1357,6 +1476,10 @@ function layoutMedallion(ast, edges) {
1357
1476
  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
1477
  return 0;
1359
1478
  }
1479
+ const nodeDims = /* @__PURE__ */ new Map();
1480
+ for (const id of nodeIds) {
1481
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1482
+ }
1360
1483
  const tiers = /* @__PURE__ */ new Map();
1361
1484
  for (let i = 0; i <= 4; i++) tiers.set(i, []);
1362
1485
  for (const id of nodeIds) {
@@ -1368,29 +1491,35 @@ function layoutMedallion(ast, edges) {
1368
1491
  const tierCount = activeTiers.length || 1;
1369
1492
  const contentW = ast.meta.width - SAFE * 2;
1370
1493
  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;
1494
+ const maxNodeW = Math.max(...nodeIds.map((id) => nodeDims.get(id).width));
1495
+ const availableColGap = tierCount > 1 ? (contentW - maxNodeW) / (tierCount - 1) : 0;
1496
+ const colGap = Math.max(maxNodeW + 36, Math.min(360, availableColGap));
1497
+ const totalW = (tierCount - 1) * colGap + maxNodeW;
1373
1498
  const startX = SAFE + Math.max(0, (contentW - totalW) / 2);
1374
1499
  const nodes = [];
1375
1500
  activeTiers.forEach(([_, ids], colIdx) => {
1376
1501
  const colCount = ids.length;
1502
+ const colTierMaxW = Math.max(...ids.map((id) => nodeDims.get(id).width));
1503
+ const colTierMaxH = Math.max(...ids.map((id) => nodeDims.get(id).height));
1377
1504
  const colX = startX + colIdx * colGap;
1378
- const rowSpacing = Math.max(NODE_H + 32, contentH / (colCount + 1));
1379
- const colH = (colCount - 1) * rowSpacing + NODE_H;
1505
+ const rowSpacing = Math.max(colTierMaxH + 32, contentH / (colCount + 1));
1506
+ const colH = (colCount - 1) * rowSpacing + colTierMaxH;
1380
1507
  const startY = TITLE_BAND + Math.max(0, (contentH - colH) / 2);
1381
1508
  ids.forEach((id, rowIdx) => {
1382
1509
  const decl = ast.nodes[id];
1383
- const rowY = startY + rowIdx * rowSpacing;
1510
+ const dims = nodeDims.get(id);
1511
+ const rowY = startY + rowIdx * rowSpacing + (colTierMaxH - dims.height) / 2;
1512
+ const nodeX = colX + (colTierMaxW - dims.width) / 2;
1384
1513
  const focal = decl.props.focal === true || decl.props.accent === true;
1385
1514
  nodes.push({
1386
1515
  id,
1387
1516
  kind: decl.kind,
1388
1517
  role: nodeRole(decl.kind),
1389
1518
  label: decl.label,
1390
- x: snapGrid(colX),
1519
+ x: snapGrid(nodeX),
1391
1520
  y: snapGrid(rowY),
1392
- width: NODE_W,
1393
- height: NODE_H,
1521
+ width: dims.width,
1522
+ height: dims.height,
1394
1523
  style: resolveNodeStyle(decl, ast),
1395
1524
  props: decl.props,
1396
1525
  opacity: 0,
@@ -1404,6 +1533,10 @@ function layoutMedallion(ast, edges) {
1404
1533
  function layoutQuadrant(ast) {
1405
1534
  const nodeIds = Object.keys(ast.nodes);
1406
1535
  if (nodeIds.length === 0) return [];
1536
+ const nodeDims = /* @__PURE__ */ new Map();
1537
+ for (const id of nodeIds) {
1538
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1539
+ }
1407
1540
  const contentW = ast.meta.width - SAFE * 2;
1408
1541
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1409
1542
  const centerX = SAFE + contentW / 2;
@@ -1437,12 +1570,15 @@ function layoutQuadrant(ast) {
1437
1570
  };
1438
1571
  for (const [qNum, ids] of quadNodes) {
1439
1572
  const center = quadCenters[qNum];
1573
+ const offsetCount = ids.length;
1574
+ const maxQHeight = ids.length > 0 ? Math.max(...ids.map((id) => nodeDims.get(id).height)) : NODE_H;
1575
+ const rowStep = maxQHeight + 20;
1440
1576
  ids.forEach((id, idx) => {
1441
1577
  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;
1578
+ const dims = nodeDims.get(id);
1579
+ const rowOffset = (idx - (offsetCount - 1) / 2) * rowStep;
1580
+ const x = center.x - dims.width / 2;
1581
+ const y = center.y + rowOffset - dims.height / 2;
1446
1582
  const focal = decl.props.focal === true || decl.props.accent === true;
1447
1583
  nodes.push({
1448
1584
  id,
@@ -1451,8 +1587,8 @@ function layoutQuadrant(ast) {
1451
1587
  label: decl.label,
1452
1588
  x: snapGrid(x),
1453
1589
  y: snapGrid(y),
1454
- width: NODE_W,
1455
- height: NODE_H,
1590
+ width: dims.width,
1591
+ height: dims.height,
1456
1592
  style: resolveNodeStyle(decl, ast),
1457
1593
  props: decl.props,
1458
1594
  opacity: 0,
@@ -1466,6 +1602,10 @@ function layoutQuadrant(ast) {
1466
1602
  function layoutSwimlane(ast, edges) {
1467
1603
  const nodeIds = Object.keys(ast.nodes);
1468
1604
  if (nodeIds.length === 0) return [];
1605
+ const nodeDims = /* @__PURE__ */ new Map();
1606
+ for (const id of nodeIds) {
1607
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1608
+ }
1469
1609
  const lanes = /* @__PURE__ */ new Map();
1470
1610
  const groupKeys = Object.keys(ast.groups);
1471
1611
  if (groupKeys.length > 0) {
@@ -1488,14 +1628,16 @@ function layoutSwimlane(ast, edges) {
1488
1628
  const laneHeight = contentH / laneCount;
1489
1629
  const nodes = [];
1490
1630
  activeLanes.forEach(([_, ids], laneIdx) => {
1491
- const laneY = TITLE_BAND + laneIdx * laneHeight + (laneHeight - NODE_H) / 2;
1492
1631
  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;
1632
+ const maxLaneW = count > 0 ? Math.max(...ids.map((id) => nodeDims.get(id).width)) : NODE_W;
1633
+ const colSpacing = Math.max(maxLaneW + 40, Math.min(320, contentW / Math.max(count + 1, 2)));
1634
+ const totalW = (count - 1) * colSpacing + maxLaneW;
1495
1635
  const startX = SAFE + Math.max(0, (contentW - totalW) / 2);
1496
1636
  ids.forEach((id, colIdx) => {
1497
1637
  const decl = ast.nodes[id];
1498
- const colX = startX + colIdx * colSpacing;
1638
+ const dims = nodeDims.get(id);
1639
+ const colX = startX + colIdx * colSpacing + (maxLaneW - dims.width) / 2;
1640
+ const laneY = TITLE_BAND + laneIdx * laneHeight + (laneHeight - dims.height) / 2;
1499
1641
  const focal = decl.props.focal === true || decl.props.accent === true;
1500
1642
  nodes.push({
1501
1643
  id,
@@ -1504,8 +1646,8 @@ function layoutSwimlane(ast, edges) {
1504
1646
  label: decl.label,
1505
1647
  x: snapGrid(colX),
1506
1648
  y: snapGrid(laneY),
1507
- width: NODE_W,
1508
- height: NODE_H,
1649
+ width: dims.width,
1650
+ height: dims.height,
1509
1651
  style: resolveNodeStyle(decl, ast),
1510
1652
  props: decl.props,
1511
1653
  opacity: 0,
@@ -1519,6 +1661,10 @@ function layoutSwimlane(ast, edges) {
1519
1661
  function layoutPyramid(ast) {
1520
1662
  const nodeIds = Object.keys(ast.nodes);
1521
1663
  if (nodeIds.length === 0) return [];
1664
+ const nodeDims = /* @__PURE__ */ new Map();
1665
+ for (const id of nodeIds) {
1666
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1667
+ }
1522
1668
  const ranks = /* @__PURE__ */ new Map();
1523
1669
  nodeIds.forEach((id, idx) => {
1524
1670
  const decl = ast.nodes[id];
@@ -1531,20 +1677,26 @@ function layoutPyramid(ast) {
1531
1677
  const contentW = ast.meta.width - SAFE * 2;
1532
1678
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1533
1679
  const centerX = SAFE + contentW / 2;
1534
- const totalH = tierCount * NODE_H + (tierCount - 1) * 24;
1680
+ const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height));
1681
+ const tierH = Math.max(NODE_H, maxNodeH);
1682
+ const totalH = tierCount * tierH + (tierCount - 1) * 24;
1535
1683
  const startY = TITLE_BAND + Math.max(16, (contentH - totalH) / 2);
1536
1684
  const nodes = [];
1537
1685
  sortedTiers.forEach(([_, ids], tierIdx) => {
1538
- const tierY = startY + tierIdx * (NODE_H + 24);
1686
+ const tierY = startY + tierIdx * (tierH + 24);
1539
1687
  const baseFraction = 0.32 + 0.44 * tierIdx / Math.max(tierCount - 1, 1);
1540
1688
  const tierWidth = contentW * baseFraction;
1541
1689
  const count = ids.length;
1542
- const nodeW = Math.max(NODE_W, Math.min(tierWidth / count - 12, 480));
1690
+ const maxTierW = Math.max(...ids.map((id) => nodeDims.get(id).width));
1691
+ const nodeW = Math.max(maxTierW, Math.min(tierWidth / count - 12, 480));
1543
1692
  const totalTierW = count * nodeW + (count - 1) * 16;
1544
1693
  const tierStartX = centerX - totalTierW / 2;
1545
1694
  ids.forEach((id, idx) => {
1546
1695
  const decl = ast.nodes[id];
1547
- const x = tierStartX + idx * (nodeW + 16);
1696
+ const dims = nodeDims.get(id);
1697
+ const finalW = Math.max(nodeW, dims.width);
1698
+ const x = tierStartX + idx * (finalW + 16);
1699
+ const y = tierY + (tierH - dims.height) / 2;
1548
1700
  const focal = decl.props.focal === true || decl.props.accent === true;
1549
1701
  nodes.push({
1550
1702
  id,
@@ -1553,8 +1705,8 @@ function layoutPyramid(ast) {
1553
1705
  label: decl.label,
1554
1706
  x: snapGrid(x),
1555
1707
  y: snapGrid(tierY),
1556
- width: snapGrid(nodeW),
1557
- height: NODE_H,
1708
+ width: snapGrid(finalW),
1709
+ height: dims.height,
1558
1710
  style: resolveNodeStyle(decl, ast),
1559
1711
  props: decl.props,
1560
1712
  opacity: 0,
@@ -1568,6 +1720,10 @@ function layoutPyramid(ast) {
1568
1720
  function layoutTimeline(ast) {
1569
1721
  const nodeIds = Object.keys(ast.nodes);
1570
1722
  if (nodeIds.length === 0) return [];
1723
+ const nodeDims = /* @__PURE__ */ new Map();
1724
+ for (const id of nodeIds) {
1725
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1726
+ }
1571
1727
  const contentW = ast.meta.width - SAFE * 2;
1572
1728
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1573
1729
  const baselineY = TITLE_BAND + contentH / 2;
@@ -1575,9 +1731,10 @@ function layoutTimeline(ast) {
1575
1731
  const nodes = [];
1576
1732
  nodeIds.forEach((id, idx) => {
1577
1733
  const decl = ast.nodes[id];
1578
- const x = SAFE + spacing * idx + (spacing - NODE_W) / 2;
1734
+ const dims = nodeDims.get(id);
1735
+ const x = SAFE + spacing * idx + (spacing - dims.width) / 2;
1579
1736
  const above = idx % 2 === 0;
1580
- const y = above ? baselineY - NODE_H - 24 : baselineY + 24;
1737
+ const y = above ? baselineY - dims.height - 24 : baselineY + 24;
1581
1738
  const focal = decl.props.focal === true || decl.props.accent === true;
1582
1739
  nodes.push({
1583
1740
  id,
@@ -1586,8 +1743,8 @@ function layoutTimeline(ast) {
1586
1743
  label: decl.label,
1587
1744
  x: snapGrid(x),
1588
1745
  y: snapGrid(y),
1589
- width: NODE_W,
1590
- height: NODE_H,
1746
+ width: dims.width,
1747
+ height: dims.height,
1591
1748
  style: resolveNodeStyle(decl, ast),
1592
1749
  props: decl.props,
1593
1750
  opacity: 0,
@@ -1600,16 +1757,23 @@ function layoutTimeline(ast) {
1600
1757
  function layoutGantt(ast) {
1601
1758
  const nodeIds = Object.keys(ast.nodes);
1602
1759
  if (nodeIds.length === 0) return [];
1760
+ const nodeDims = /* @__PURE__ */ new Map();
1761
+ for (const id of nodeIds) {
1762
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1763
+ }
1764
+ const topBand = effectiveTitleBand(ast);
1603
1765
  const contentW = ast.meta.width - SAFE * 2;
1604
- const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1766
+ const contentH = ast.meta.height - SAFE - topBand - SAFE;
1605
1767
  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);
1768
+ const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height));
1769
+ const baseBarH = Math.max(44, maxNodeH);
1770
+ const rowH = Math.max(baseBarH + 16, Math.min(100, (contentH - 24) / Math.max(N, 1)));
1608
1771
  const totalH = N * rowH;
1609
- const startY = TITLE_BAND + Math.max(16, (contentH - totalH) / 2);
1772
+ const startY = topBand + Math.max(16, (contentH - totalH) / 2);
1610
1773
  const nodes = [];
1611
1774
  nodeIds.forEach((id, idx) => {
1612
1775
  const decl = ast.nodes[id];
1776
+ const dims = nodeDims.get(id);
1613
1777
  const phase = typeof decl.props.phase === "number" ? decl.props.phase : 0;
1614
1778
  const span = typeof decl.props.span === "number" ? decl.props.span : 1;
1615
1779
  const totalPhases = Math.max(...nodeIds.map((nid) => {
@@ -1619,7 +1783,8 @@ function layoutGantt(ast) {
1619
1783
  }), 1);
1620
1784
  const unitW = contentW / totalPhases;
1621
1785
  const x = SAFE + phase * unitW;
1622
- const w = Math.max(span * unitW - 8, NODE_W);
1786
+ const w = Math.max(span * unitW - 8, dims.width);
1787
+ const barH = dims.height;
1623
1788
  const y = startY + idx * rowH + (rowH - barH) / 2;
1624
1789
  const focal = decl.props.focal === true || decl.props.accent === true;
1625
1790
  nodes.push({
@@ -1630,7 +1795,7 @@ function layoutGantt(ast) {
1630
1795
  x: snapGrid(x),
1631
1796
  y: snapGrid(y),
1632
1797
  width: snapGrid(w),
1633
- height: barH,
1798
+ height: snapGrid(barH),
1634
1799
  style: resolveNodeStyle(decl, ast),
1635
1800
  props: decl.props,
1636
1801
  opacity: 0,
@@ -1643,23 +1808,31 @@ function layoutGantt(ast) {
1643
1808
  function layoutVenn(ast) {
1644
1809
  const nodeIds = Object.keys(ast.nodes);
1645
1810
  if (nodeIds.length === 0) return [];
1811
+ const nodeDims = /* @__PURE__ */ new Map();
1812
+ for (const id of nodeIds) {
1813
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1814
+ }
1646
1815
  const contentW = ast.meta.width - SAFE * 2;
1647
1816
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1648
1817
  const centerX = SAFE + contentW / 2;
1649
1818
  const centerY = TITLE_BAND + contentH / 2;
1650
1819
  const N = nodeIds.length;
1651
- const radius = Math.min(contentW, contentH) * 0.24;
1820
+ const maxNodeDim = Math.max(...nodeIds.map((id) => Math.max(nodeDims.get(id).width, nodeDims.get(id).height)));
1821
+ const vennSize = Math.max(VENN_NODE_SIZE, maxNodeDim);
1822
+ const radius = Math.max(vennSize * 0.7, Math.min(contentW, contentH) * 0.24);
1652
1823
  const nodes = [];
1653
1824
  nodeIds.forEach((id, idx) => {
1654
1825
  const decl = ast.nodes[id];
1826
+ const dims = nodeDims.get(id);
1827
+ const circleSize = Math.max(vennSize, dims.width, dims.height);
1655
1828
  let x, y;
1656
1829
  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;
1830
+ x = centerX + (idx === 0 ? -radius * 0.7 : radius * 0.7) - circleSize / 2;
1831
+ y = centerY - circleSize / 2;
1659
1832
  } else {
1660
1833
  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;
1834
+ x = centerX + radius * 0.85 * Math.cos(angle) - circleSize / 2;
1835
+ y = centerY + radius * 0.85 * Math.sin(angle) - circleSize / 2;
1663
1836
  }
1664
1837
  const focal = decl.props.focal === true || decl.props.accent === true;
1665
1838
  nodes.push({
@@ -1669,8 +1842,8 @@ function layoutVenn(ast) {
1669
1842
  label: decl.label,
1670
1843
  x: snapGrid(x),
1671
1844
  y: snapGrid(y),
1672
- width: VENN_NODE_SIZE,
1673
- height: VENN_NODE_SIZE,
1845
+ width: circleSize,
1846
+ height: circleSize,
1674
1847
  style: resolveNodeStyle(decl, ast),
1675
1848
  props: decl.props,
1676
1849
  opacity: 0,
@@ -1683,18 +1856,26 @@ function layoutVenn(ast) {
1683
1856
  function layoutLayers(ast) {
1684
1857
  const nodeIds = Object.keys(ast.nodes);
1685
1858
  if (nodeIds.length === 0) return [];
1859
+ const nodeDims = /* @__PURE__ */ new Map();
1860
+ for (const id of nodeIds) {
1861
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1862
+ }
1686
1863
  const contentW = ast.meta.width - SAFE * 2;
1687
1864
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1688
1865
  const N = nodeIds.length;
1689
- const layerH = Math.min(Math.max((contentH - (N - 1) * 14) / N, 52), 68);
1866
+ const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height));
1867
+ const layerH = Math.max(maxNodeH, Math.min(Math.max((contentH - (N - 1) * 14) / N, 52), 96));
1690
1868
  const totalH = N * layerH + (N - 1) * 14;
1691
- const startY = TITLE_BAND + (contentH - totalH) / 2;
1692
- const layerW = Math.min(contentW, 880);
1869
+ const startY = TITLE_BAND + Math.max(16, (contentH - totalH) / 2);
1870
+ const maxNodeW = Math.max(...nodeIds.map((id) => nodeDims.get(id).width));
1871
+ const layerW = Math.max(maxNodeW, Math.min(contentW, 880));
1693
1872
  const startX = SAFE + (contentW - layerW) / 2;
1694
1873
  const nodes = [];
1695
1874
  nodeIds.forEach((id, idx) => {
1696
1875
  const decl = ast.nodes[id];
1876
+ const dims = nodeDims.get(id);
1697
1877
  const y = startY + idx * (layerH + 14);
1878
+ const nodeH = Math.max(layerH, dims.height);
1698
1879
  const focal = decl.props.focal === true || decl.props.accent === true;
1699
1880
  nodes.push({
1700
1881
  id,
@@ -1704,7 +1885,7 @@ function layoutLayers(ast) {
1704
1885
  x: snapGrid(startX),
1705
1886
  y: snapGrid(y),
1706
1887
  width: snapGrid(layerW),
1707
- height: snapGrid(layerH),
1888
+ height: snapGrid(nodeH),
1708
1889
  style: resolveNodeStyle(decl, ast),
1709
1890
  props: decl.props,
1710
1891
  opacity: 0,
@@ -1717,6 +1898,10 @@ function layoutLayers(ast) {
1717
1898
  function layoutNested(ast) {
1718
1899
  const nodeIds = Object.keys(ast.nodes);
1719
1900
  if (nodeIds.length === 0) return [];
1901
+ const nodeDims = /* @__PURE__ */ new Map();
1902
+ for (const id of nodeIds) {
1903
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1904
+ }
1720
1905
  const topHeadroom = 20;
1721
1906
  const startY = TITLE_BAND + topHeadroom;
1722
1907
  const contentW = ast.meta.width - SAFE * 2;
@@ -1729,18 +1914,19 @@ function layoutNested(ast) {
1729
1914
  const nodes = [];
1730
1915
  nodeIds.forEach((id, idx) => {
1731
1916
  const decl = ast.nodes[id];
1917
+ const dims = nodeDims.get(id);
1732
1918
  const isCore = idx === N - 1;
1733
1919
  let x, y, w, h;
1734
1920
  if (isCore) {
1735
- w = Math.min(320, contentW - idx * padX * 2);
1736
- h = 84;
1921
+ w = Math.max(dims.width, Math.min(320, contentW - idx * padX * 2));
1922
+ h = Math.max(dims.height, 84);
1737
1923
  x = centerX - w / 2;
1738
1924
  y = centerY - h / 2 + (N > 1 ? 16 : 0);
1739
1925
  } else {
1740
1926
  x = SAFE + idx * padX;
1741
1927
  y = startY + idx * padY;
1742
- w = contentW - idx * padX * 2;
1743
- h = contentH - idx * padY * 2;
1928
+ w = Math.max(dims.width, contentW - idx * padX * 2);
1929
+ h = Math.max(dims.height, contentH - idx * padY * 2);
1744
1930
  }
1745
1931
  const focal = decl.props.focal === true || decl.props.accent === true || isCore;
1746
1932
  nodes.push({
@@ -1764,6 +1950,10 @@ function layoutNested(ast) {
1764
1950
  function layoutRadar(ast) {
1765
1951
  const nodeIds = Object.keys(ast.nodes);
1766
1952
  if (nodeIds.length === 0) return [];
1953
+ const nodeDims = /* @__PURE__ */ new Map();
1954
+ for (const id of nodeIds) {
1955
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1956
+ }
1767
1957
  const contentW = ast.meta.width - SAFE * 2;
1768
1958
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1769
1959
  const centerX = SAFE + contentW / 2;
@@ -1773,9 +1963,10 @@ function layoutRadar(ast) {
1773
1963
  const nodes = [];
1774
1964
  nodeIds.forEach((id, idx) => {
1775
1965
  const decl = ast.nodes[id];
1966
+ const dims = nodeDims.get(id);
1776
1967
  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;
1968
+ const x = centerX + radius * Math.cos(angle) - dims.width / 2;
1969
+ const y = centerY + radius * Math.sin(angle) - dims.height / 2;
1779
1970
  const focal = decl.props.focal === true || decl.props.accent === true;
1780
1971
  nodes.push({
1781
1972
  id,
@@ -1784,8 +1975,8 @@ function layoutRadar(ast) {
1784
1975
  label: decl.label,
1785
1976
  x: snapGrid(x),
1786
1977
  y: snapGrid(y),
1787
- width: NODE_W,
1788
- height: NODE_H,
1978
+ width: dims.width,
1979
+ height: dims.height,
1789
1980
  style: resolveNodeStyle(decl, ast),
1790
1981
  props: decl.props,
1791
1982
  opacity: 0,
@@ -1830,9 +2021,9 @@ function layoutNodes(ast, edges) {
1830
2021
  case "nested":
1831
2022
  return layoutNested(ast);
1832
2023
  case "state":
1833
- return layoutRanked(ast, cycleSafeEdges(Object.keys(ast.nodes), edges), { forceVertical: false });
2024
+ return layoutRanked(ast, cycleSafeEdges(Object.keys(ast.nodes), edges), { forceVertical: ast.meta.direction === "TB" || ast.meta.direction === "BT" });
1834
2025
  default:
1835
- return layoutRanked(ast, edges, { forceVertical: false });
2026
+ return layoutRanked(ast, edges, { forceVertical: ast.meta.direction === "TB" || ast.meta.direction === "BT" });
1836
2027
  }
1837
2028
  }
1838
2029
  function computeGroupBoundaries(ast, nodes) {
@@ -1917,13 +2108,51 @@ function scheduleBeats(ast, edges) {
1917
2108
  let edgeCounter = edges.length;
1918
2109
  const edgeIds = edges.map((e) => e.id);
1919
2110
  const groupMap = Object.fromEntries(Object.entries(ast.groups).map(([k, g]) => [k, g.members]));
1920
- const hasIntro = ast.beats.some((b) => b.cues.some((c) => c.kind === "show"));
1921
- if (!hasIntro && Object.keys(ast.nodes).length > 0) {
2111
+ const hasShowCue = ast.beats.some(
2112
+ (b) => b.cues.some((c) => c.kind === "show" || c.kind === "parallel" && c.cues.some((pc) => pc.kind === "show"))
2113
+ );
2114
+ const flowNodeIds = /* @__PURE__ */ new Set();
2115
+ for (const b of ast.beats) {
2116
+ for (const c of b.cues) {
2117
+ if (c.kind === "flow") {
2118
+ for (const seg of c.segments) {
2119
+ flowNodeIds.add(seg.from);
2120
+ flowNodeIds.add(seg.to);
2121
+ }
2122
+ } else if (c.kind === "parallel") {
2123
+ for (const pc of c.cues) {
2124
+ if (pc.kind === "flow") {
2125
+ for (const seg of pc.segments) {
2126
+ flowNodeIds.add(seg.from);
2127
+ flowNodeIds.add(seg.to);
2128
+ }
2129
+ }
2130
+ }
2131
+ }
2132
+ }
2133
+ }
2134
+ const explicitlyShownNodes = /* @__PURE__ */ new Set();
2135
+ for (const b of ast.beats) {
2136
+ for (const c of b.cues) {
2137
+ if (c.kind === "show") {
2138
+ resolveTargets(c.targets, ast, groupMap, edgeIds).forEach((id) => explicitlyShownNodes.add(id));
2139
+ } else if (c.kind === "parallel") {
2140
+ for (const pc of c.cues) {
2141
+ if (pc.kind === "show") {
2142
+ resolveTargets(pc.targets, ast, groupMap, edgeIds).forEach((id) => explicitlyShownNodes.add(id));
2143
+ }
2144
+ }
2145
+ }
2146
+ }
2147
+ }
2148
+ const isSequence = ast.meta.type === "sequence";
2149
+ const nodesToReveal = !isSequence && flowNodeIds.size > 0 ? Object.keys(ast.nodes).filter((id) => !flowNodeIds.has(id) && !explicitlyShownNodes.has(id)) : !hasShowCue ? Object.keys(ast.nodes) : [];
2150
+ if (nodesToReveal.length > 0) {
1922
2151
  cues.push({
1923
2152
  start: 0,
1924
2153
  duration: DEFAULTS.show,
1925
2154
  kind: "show",
1926
- targets: Object.keys(ast.nodes),
2155
+ targets: nodesToReveal,
1927
2156
  params: { stagger: DEFAULTS.stagger },
1928
2157
  beat: "__intro"
1929
2158
  });
@@ -1998,8 +2227,10 @@ function scheduleBeats(ast, edges) {
1998
2227
  }
1999
2228
  function buildSequencePlan(ast, cues) {
2000
2229
  if (diagramType(ast) !== "sequence") return { messages: [], activations: [] };
2230
+ const nodeDims = Object.values(ast.nodes).map((n) => computeNodeDimensions(n));
2231
+ const maxNodeH = nodeDims.length > 0 ? Math.max(...nodeDims.map((d) => d.height)) : NODE_H;
2001
2232
  const flowCues = cues.filter((cue) => cue.kind === "flow" && cue.segments?.[0]);
2002
- const firstY = TITLE_BAND + NODE_H + 56;
2233
+ const firstY = TITLE_BAND + maxNodeH + 56;
2003
2234
  const lastY = ast.meta.height - SAFE - 48;
2004
2235
  const availH = Math.max(120, lastY - firstY);
2005
2236
  const step = flowCues.length > 1 ? Math.max(52, Math.min(90, availH / flowCues.length)) : 0;
@@ -2048,19 +2279,44 @@ function computeAdaptiveDimensions(ast, edges) {
2048
2279
  const direction = ast.meta.direction ?? "LR";
2049
2280
  const isVertical = direction === "TB" || direction === "BT";
2050
2281
  const groupCount = Object.keys(ast.groups).length;
2051
- let autoW = 1280;
2052
- let autoH = 720;
2282
+ const nodeDims = nodeIds.map((id) => computeNodeDimensions(ast.nodes[id]));
2283
+ const maxNodeW = nodeDims.length > 0 ? Math.max(...nodeDims.map((d) => d.width)) : NODE_W;
2284
+ const maxNodeH = nodeDims.length > 0 ? Math.max(...nodeDims.map((d) => d.height)) : NODE_H;
2285
+ const hasTitle = Boolean(ast.meta.title && ast.meta.title.trim().length > 0);
2286
+ const titleBand = hasTitle ? TITLE_BAND : 0;
2287
+ const hasBeatCaptions = ast.beats.some((b) => b.label && b.label.trim().length > 0);
2288
+ const bottomBand = hasBeatCaptions ? 44 : 0;
2289
+ const titleW = hasTitle ? Math.max(320, ast.meta.title.trim().length * 11 + SAFE * 2) : 0;
2290
+ let autoW = 1024;
2291
+ let autoH = 640;
2053
2292
  if (nodeCount === 0) {
2054
- autoW = 1280;
2055
- autoH = 720;
2293
+ autoW = 960;
2294
+ autoH = 540;
2056
2295
  } else if (dtype === "sequence") {
2057
2296
  const flowCues = ast.beats.flatMap((b) => b.cues).filter((c) => c.kind === "flow");
2058
2297
  const count = Math.max(nodeCount, 1);
2059
2298
  const flowCount = flowCues.length;
2060
- const requiredW = SAFE * 2 + Math.max(count * 220, 800);
2061
- const requiredH = TITLE_BAND + NODE_H + 56 + Math.max(flowCount * 76, 280) + SAFE + 48;
2062
- autoW = Math.max(1088, Math.min(2560, requiredW));
2063
- autoH = Math.max(640, Math.min(1800, requiredH));
2299
+ const colW = Math.max(200, maxNodeW + 36);
2300
+ const requiredW = SAFE * 2 + Math.max(count * colW, titleW);
2301
+ const requiredH = titleBand + maxNodeH + 48 + Math.max(flowCount * 68, 160) + SAFE + bottomBand;
2302
+ autoW = Math.max(480, Math.min(2560, requiredW));
2303
+ autoH = Math.max(320, Math.min(1800, requiredH));
2304
+ } else if (dtype === "medallion") {
2305
+ const tierSet = /* @__PURE__ */ new Set();
2306
+ for (const id of nodeIds) {
2307
+ const combined = `${id} ${ast.nodes[id].kind}`.toLowerCase();
2308
+ if (combined.includes("bronze") || combined.includes("raw") || combined.includes("landing")) tierSet.add(1);
2309
+ else if (combined.includes("silver") || combined.includes("clean") || combined.includes("curated") || combined.includes("conformed")) tierSet.add(2);
2310
+ else if (combined.includes("gold") || combined.includes("agg") || combined.includes("mart") || combined.includes("analytics")) tierSet.add(3);
2311
+ 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);
2312
+ else tierSet.add(0);
2313
+ }
2314
+ const tierCount = Math.max(tierSet.size, 4);
2315
+ const tierW = Math.max(260, maxNodeW + 48);
2316
+ const requiredW = Math.max(SAFE * 2 + tierCount * tierW, titleW);
2317
+ const requiredH = titleBand + SAFE * 2 + Math.max(440, maxNodeH * 3 + 120) + bottomBand;
2318
+ autoW = Math.max(640, Math.min(2800, requiredW));
2319
+ autoH = Math.max(400, Math.min(1600, requiredH));
2064
2320
  } else if (dtype === "tree") {
2065
2321
  let measureSubtree2 = function(id) {
2066
2322
  const kids = children.get(id) ?? [];
@@ -2101,15 +2357,16 @@ function computeAdaptiveDimensions(ast, edges) {
2101
2357
  }
2102
2358
  for (const id of nodeIds) if (!depth.has(id)) depth.set(id, 0);
2103
2359
  const maxDepth = Math.max(...depth.values(), 0);
2104
- const minLeafWidth = NODE_W + 48;
2360
+ const minLeafWidth = Math.max(NODE_W + 36, maxNodeW + 36);
2105
2361
  const subtreeSpan = /* @__PURE__ */ new Map();
2106
2362
  let totalRootsWidth = 0;
2107
2363
  const activeRoots = roots.length > 0 ? roots : nodeIds;
2108
2364
  for (const r of activeRoots) totalRootsWidth += measureSubtree2(r);
2109
- const requiredW = SAFE * 2 + totalRootsWidth;
2110
- const requiredH = TITLE_BAND + SAFE * 2 + (maxDepth + 1) * 140;
2111
- autoW = Math.max(1120, Math.min(2560, requiredW));
2112
- autoH = Math.max(640, Math.min(1600, requiredH));
2365
+ const levelH = Math.max(130, maxNodeH + 54);
2366
+ const requiredW = Math.max(SAFE * 2 + totalRootsWidth, titleW);
2367
+ const requiredH = titleBand + SAFE * 2 + (maxDepth + 1) * levelH + bottomBand;
2368
+ autoW = Math.max(480, Math.min(2560, requiredW));
2369
+ autoH = Math.max(320, Math.min(1800, requiredH));
2113
2370
  } else if (dtype === "swimlane") {
2114
2371
  const laneCount = Math.max(groupCount, 1);
2115
2372
  const maxInLane = Math.max(
@@ -2117,37 +2374,56 @@ function computeAdaptiveDimensions(ast, edges) {
2117
2374
  Math.ceil(nodeCount / laneCount),
2118
2375
  1
2119
2376
  );
2120
- const requiredW = SAFE * 2 + 140 + maxInLane * 220;
2121
- const requiredH = TITLE_BAND + SAFE * 2 + laneCount * 140;
2122
- autoW = Math.max(1152, Math.min(2560, requiredW));
2123
- autoH = Math.max(640, Math.min(1600, requiredH));
2377
+ const stepX = Math.max(200, maxNodeW + 36);
2378
+ const laneH = Math.max(130, maxNodeH + 54);
2379
+ const requiredW = Math.max(SAFE * 2 + 140 + maxInLane * stepX, titleW);
2380
+ const requiredH = titleBand + SAFE * 2 + laneCount * laneH + bottomBand;
2381
+ autoW = Math.max(480, Math.min(2560, requiredW));
2382
+ autoH = Math.max(320, Math.min(1800, requiredH));
2124
2383
  } else if (dtype === "pyramid") {
2125
2384
  const tierCount = Math.max(nodeCount, 1);
2126
- autoW = 1280;
2127
- const requiredH = TITLE_BAND + SAFE * 2 + tierCount * 110;
2128
- autoH = Math.max(640, Math.min(1440, requiredH));
2385
+ const tierH = Math.max(100, maxNodeH + 28);
2386
+ autoW = Math.max(480, Math.min(2e3, Math.max(SAFE * 2 + maxNodeW * 2 + 100, titleW)));
2387
+ const requiredH = titleBand + SAFE * 2 + tierCount * tierH + bottomBand;
2388
+ autoH = Math.max(320, Math.min(1440, requiredH));
2129
2389
  } else if (dtype === "layers") {
2130
2390
  const layerCount = Math.max(nodeCount, 1);
2131
- autoW = 1280;
2132
- const requiredH = TITLE_BAND + SAFE * 2 + layerCount * 96;
2133
- autoH = Math.max(640, Math.min(1440, requiredH));
2391
+ const layerH = Math.max(90, maxNodeH + 18);
2392
+ autoW = Math.max(480, Math.min(2e3, Math.max(SAFE * 2 + maxNodeW + 160, titleW)));
2393
+ const requiredH = titleBand + SAFE * 2 + layerCount * layerH + bottomBand;
2394
+ autoH = Math.max(320, Math.min(1440, requiredH));
2134
2395
  } else if (dtype === "timeline" || dtype === "gantt") {
2135
2396
  const milestones = Math.max(nodeCount, 1);
2136
- const requiredW = SAFE * 2 + milestones * 240;
2137
- const requiredH = TITLE_BAND + SAFE * 2 + 380;
2138
- autoW = Math.max(1200, Math.min(2560, requiredW));
2139
- autoH = Math.max(640, Math.min(1200, requiredH));
2397
+ let totalPhases = milestones;
2398
+ if (dtype === "gantt") {
2399
+ totalPhases = Math.max(...nodeIds.map((nid) => {
2400
+ const p = ast.nodes[nid].props.phase;
2401
+ const s = ast.nodes[nid].props.span;
2402
+ return (typeof p === "number" ? p : 0) + (typeof s === "number" ? s : 1);
2403
+ }), milestones);
2404
+ }
2405
+ const milestoneW = Math.max(220, maxNodeW + 36);
2406
+ const requiredW = Math.max(
2407
+ SAFE * 2 + (dtype === "gantt" ? totalPhases * Math.max(160, maxNodeW + 16) : milestones * milestoneW),
2408
+ titleW
2409
+ );
2410
+ 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;
2411
+ autoW = Math.max(480, Math.min(2560, requiredW));
2412
+ autoH = Math.max(320, Math.min(1600, requiredH));
2140
2413
  } else if (dtype === "loop" || dtype === "flywheel" || dtype === "radar" || dtype === "venn" || dtype === "constellation") {
2141
2414
  const N = Math.max(nodeCount, 3);
2142
- const minRadius = 200;
2143
- const radiusNeeded = Math.max(minRadius, N * 180 / (2 * Math.PI));
2144
- const requiredW = SAFE * 2 + radiusNeeded * 2 + 240;
2145
- const requiredH = TITLE_BAND + SAFE * 2 + radiusNeeded * 2 + 140;
2146
- autoW = Math.max(1088, Math.min(2200, requiredW));
2147
- autoH = Math.max(720, Math.min(1600, requiredH));
2415
+ const minRadius = Math.max(160, maxNodeW * 0.75);
2416
+ const nodeFootprint = Math.max(160, maxNodeW + 16);
2417
+ const radiusNeeded = Math.max(minRadius, N * nodeFootprint / (2 * Math.PI));
2418
+ const requiredW = Math.max(SAFE * 2 + radiusNeeded * 2 + maxNodeW + 40, titleW);
2419
+ const requiredH = titleBand + SAFE * 2 + radiusNeeded * 2 + maxNodeH + 40 + bottomBand;
2420
+ autoW = Math.max(480, Math.min(2400, requiredW));
2421
+ autoH = Math.max(360, Math.min(1800, requiredH));
2148
2422
  } else if (dtype === "quadrant") {
2149
- autoW = 1200;
2150
- autoH = 800;
2423
+ const requiredW = Math.max(480, SAFE * 2 + maxNodeW * 2.5, titleW);
2424
+ const requiredH = titleBand + SAFE * 2 + maxNodeH * 3 + bottomBand;
2425
+ autoW = Math.min(2400, requiredW);
2426
+ autoH = Math.min(1600, requiredH);
2151
2427
  } else {
2152
2428
  const forceVertical = dtype === "flowchart" && isVertical || isVertical;
2153
2429
  const ranks = assignRanks(nodeIds, effectiveEdges, forceVertical ? "TB" : "LR");
@@ -2160,47 +2436,26 @@ function computeAdaptiveDimensions(ast, edges) {
2160
2436
  const rankCount = Math.max(...byRank.keys(), 0) + 1;
2161
2437
  const maxInRank = Math.max(...[...byRank.values()].map((v) => v.length), 1);
2162
2438
  const groupPaddingBonus = groupCount > 0 ? GROUP_PAD * 2 : 0;
2439
+ const maxRankW = maxNodeW;
2440
+ const maxRankH = maxNodeH;
2163
2441
  if (forceVertical) {
2164
- const requiredW = SAFE * 2 + maxInRank * NODE_W + (maxInRank - 1) * 44 + groupPaddingBonus;
2165
- const requiredH = SAFE * 2 + TITLE_BAND + rankCount * NODE_H + (rankCount - 1) * 56 + groupPaddingBonus;
2166
- if (nodeCount <= 2) {
2167
- autoW = 960;
2168
- autoH = 640;
2169
- } else if (nodeCount <= 4 && rankCount <= 3) {
2170
- autoW = 1024;
2171
- autoH = 720;
2172
- } else {
2173
- autoW = Math.max(960, Math.min(2400, requiredW));
2174
- autoH = Math.max(720, Math.min(2e3, requiredH));
2175
- }
2442
+ const colSpacing = 44;
2443
+ const rowGap = 56;
2444
+ const contentNeededW = maxInRank * maxRankW + Math.max(0, maxInRank - 1) * colSpacing + groupPaddingBonus;
2445
+ const requiredW = SAFE * 2 + contentNeededW;
2446
+ autoW = Math.max(480, Math.min(2400, Math.max(requiredW, titleW)));
2447
+ const contentNeededH = rankCount * maxRankH + Math.max(0, rankCount - 1) * rowGap + groupPaddingBonus;
2448
+ const requiredH = SAFE * 2 + titleBand + bottomBand + contentNeededH;
2449
+ autoH = Math.max(360, Math.min(3200, requiredH));
2176
2450
  } else {
2177
- const hasTitle = Boolean(ast.meta.title && ast.meta.title.trim().length > 0);
2178
- const hasBeatCaptions = ast.beats.some((b) => b.label && b.label.trim().length > 0);
2179
- const titleBand = hasTitle ? TITLE_BAND : 0;
2180
- const bottomBand = hasBeatCaptions ? 44 : 0;
2181
- const maxRankW = Math.max(...nodeIds.map((id) => computeNodeDimensions(ast.nodes[id]).width));
2182
- const maxRankH = Math.max(...nodeIds.map((id) => computeNodeDimensions(ast.nodes[id]).height));
2183
- const requiredW = SAFE * 2 + rankCount * maxRankW + (rankCount - 1) * 56 + groupPaddingBonus;
2184
- const requiredH = SAFE * 2 + titleBand + bottomBand + maxInRank * maxRankH + (maxInRank - 1) * 44 + groupPaddingBonus;
2185
- if (maxInRank === 1 && !hasTitle) {
2186
- autoW = Math.max(1024, Math.min(2560, requiredW));
2187
- autoH = Math.max(hasBeatCaptions ? 240 : 208, Math.min(360, requiredH + 24));
2188
- } else if (maxInRank === 2 && !hasTitle) {
2189
- autoW = Math.max(1024, Math.min(2560, requiredW));
2190
- autoH = Math.max(hasBeatCaptions ? 368 : 336, Math.min(480, requiredH + 24));
2191
- } else if (maxInRank === 3 && !hasTitle) {
2192
- autoW = Math.max(1152, Math.min(2560, requiredW));
2193
- autoH = Math.max(hasBeatCaptions ? 480 : 448, Math.min(640, requiredH + 24));
2194
- } else if (nodeCount <= 2 && rankCount <= 2) {
2195
- autoW = 1024;
2196
- autoH = hasTitle ? 576 : 384;
2197
- } else if (nodeCount <= 4 && rankCount <= 3 && maxInRank <= 2) {
2198
- autoW = 1152;
2199
- autoH = hasTitle ? 648 : 448;
2200
- } else {
2201
- autoW = Math.max(1152, Math.min(2560, requiredW));
2202
- autoH = Math.max(hasTitle ? 648 : maxInRank <= 2 ? 448 : 576, Math.min(1600, requiredH));
2203
- }
2451
+ const colGap = 56;
2452
+ const rowGap = 44;
2453
+ const contentNeededW = rankCount * maxRankW + Math.max(0, rankCount - 1) * colGap + groupPaddingBonus;
2454
+ const requiredW = SAFE * 2 + contentNeededW;
2455
+ const contentNeededH = maxInRank * maxRankH + Math.max(0, maxInRank - 1) * rowGap + groupPaddingBonus;
2456
+ const requiredH = SAFE * 2 + titleBand + bottomBand + contentNeededH;
2457
+ autoW = Math.max(480, Math.min(2560, Math.max(requiredW, titleW)));
2458
+ autoH = Math.max(240, Math.min(1800, requiredH));
2204
2459
  }
2205
2460
  }
2206
2461
  return {
@@ -3339,7 +3594,8 @@ function parse(source, opts = {}) {
3339
3594
  fps: 60,
3340
3595
  theme: "paper",
3341
3596
  explicitTheme: false,
3342
- direction: "LR"
3597
+ direction: "LR",
3598
+ explicitDirection: false
3343
3599
  };
3344
3600
  const styles = {};
3345
3601
  const nodes = {};
@@ -3443,6 +3699,7 @@ function parse(source, opts = {}) {
3443
3699
  const inlineLayout = remainder.match(/\blayout\s+(LR|RL|TB|BT)\b/i);
3444
3700
  if (inlineLayout) {
3445
3701
  meta.direction = inlineLayout[1].toUpperCase();
3702
+ meta.explicitDirection = true;
3446
3703
  remainder = remainder.replace(/\blayout\s+(LR|RL|TB|BT)\b/i, " ");
3447
3704
  }
3448
3705
  const props = parseProps(remainder);
@@ -3464,8 +3721,10 @@ function parse(source, opts = {}) {
3464
3721
  const val = String(v).toLowerCase();
3465
3722
  meta.theme = val;
3466
3723
  meta.explicitTheme = val !== "auto";
3467
- } else if (k === "direction" || k === "layout") meta.direction = String(v).toUpperCase();
3468
- else if (PLAYER_FLAT_KEY_SET.has(k)) {
3724
+ } else if (k === "direction" || k === "layout") {
3725
+ meta.direction = String(v).toUpperCase();
3726
+ meta.explicitDirection = true;
3727
+ } else if (PLAYER_FLAT_KEY_SET.has(k)) {
3469
3728
  const error = applyPlayerSetting(meta.player ??= {}, "player", k, String(v));
3470
3729
  if (error) diagnostics.push({ severity: "warning", message: error, line: lineNo });
3471
3730
  } else if (k === "type") {
@@ -3502,6 +3761,7 @@ function parse(source, opts = {}) {
3502
3761
  }
3503
3762
  if (/^layout\s+(LR|RL|TB|BT)\b/i.test(line)) {
3504
3763
  meta.direction = line.split(/\s+/)[1].toUpperCase();
3764
+ meta.explicitDirection = true;
3505
3765
  i++;
3506
3766
  continue;
3507
3767
  }
@@ -8699,6 +8959,7 @@ export {
8699
8959
  compilePlan,
8700
8960
  compressMarkdyToUrlHash,
8701
8961
  computeAdaptiveDimensions,
8962
+ computeNodeDimensions,
8702
8963
  damerauLevenshteinDistance,
8703
8964
  decompressMarkdyFromUrlHash,
8704
8965
  detectArchitectureDrift,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/core",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
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",