agentic-mermaid 0.1.1 → 0.2.0
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/AGENT_NATIVE.md +155 -91
- package/CHANGELOG.md +163 -4
- package/Instructions_for_agents.md +10 -10
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +31 -38
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/assets/hero.png +0 -0
- package/dist/agent-core.d.ts +463 -0
- package/dist/agent-core.js +397 -0
- package/dist/agent.d.ts +73 -1868
- package/dist/agent.js +336 -37325
- package/dist/agentic-mermaid-mcp.js +7 -38399
- package/dist/am.js +33626 -40446
- package/dist/chunk-2P5FKUQC.js +3485 -0
- package/dist/chunk-2S3VSS6I.js +149 -0
- package/dist/chunk-2SILJDW2.js +37 -0
- package/dist/chunk-BJYMBT4I.js +61963 -0
- package/dist/chunk-FDM43XU7.js +615 -0
- package/dist/chunk-MYPDVZCO.js +4702 -0
- package/dist/index-DWPa2MJs.d.ts +6780 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +310 -33014
- package/dist/mcp-cli-5J47WEY3.js +17 -0
- package/docs/README.md +39 -31
- package/docs/agent-api-cookbook.md +44 -35
- package/docs/agent-mutation-policy.md +1 -1
- package/docs/agent-workflow-examples.md +2 -2
- package/docs/api.md +284 -77
- package/docs/ascii.md +26 -16
- package/docs/code-mode-impact.md +214 -0
- package/docs/comparison.md +23 -11
- package/docs/computational-aesthetics-prototype-plan.md +252 -0
- package/docs/config.md +26 -10
- package/docs/custom-fonts.md +265 -0
- package/docs/custom-style-cookbook.md +130 -9
- package/docs/diagram-families.md +48 -23
- package/docs/features.md +59 -52
- package/docs/fork-differences.md +6 -3
- package/docs/getting-started.md +23 -16
- package/docs/issue-derived-test-cases.md +29 -22
- package/docs/mcp-code-mode-rationale.md +13 -6
- package/docs/mcp-http-transport.md +20 -13
- package/docs/mermaid-layout-complaints.md +66 -52
- package/docs/mutation-testing.md +145 -66
- package/docs/quality.md +23 -14
- package/docs/react.md +2 -2
- package/docs/schemas/style-spec.schema.json +1221 -30
- package/docs/style-authoring.md +241 -38
- package/docs/svg-semantic-contract.md +49 -0
- package/docs/testing-strategy.md +166 -58
- package/docs/theming.md +19 -32
- package/examples/styles/catalog.json +54 -0
- package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
- package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
- package/examples/styles/cupertino-prototype.mmd +10 -0
- package/examples/styles/cupertino-prototype.style.json +63 -0
- package/examples/styles/mid-century-report.style.json +2 -2
- package/examples/styles/star-chart-atlas.style.json +2 -1
- package/examples/styles/transit-route-map.style.json +2 -2
- package/examples/styles/vercel-inspired-prototype.mmd +6 -0
- package/examples/styles/vercel-inspired-prototype.style.json +87 -0
- package/package.json +85 -45
- package/server.json +2 -2
- package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
- package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
- package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
- package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
- package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
- package/TODO.md +0 -684
- package/dist/index-wjRjbsP-.d.ts +0 -1102
- package/docs/assets/improvements/box-start-connector.png +0 -0
- package/docs/assets/improvements/comment-dropped-lint.png +0 -0
- package/docs/assets/improvements/er-content-loss.png +0 -0
- package/docs/assets/improvements/fan-in-trunks.png +0 -0
- package/docs/assets/improvements/gantt-family.png +0 -0
- package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
- package/docs/assets/improvements/gantt-verify-seam.png +0 -0
- package/docs/assets/improvements/subgraph-container-edges.png +0 -0
- package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
- package/docs/assets/improvements/wrapper-init-directive.png +0 -0
- package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
- package/docs/assets/style-cookbook/mid-century-report.png +0 -0
- package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
- package/docs/assets/style-cookbook/transit-route-map.png +0 -0
- package/docs/contributing/adding-diagram-types.md +0 -144
- package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
- package/docs/contributing/diagram-family-citizenship.md +0 -82
- package/docs/contributing/harvesting-upstream-tests.md +0 -126
- package/docs/contributing/lessons-learned.md +0 -276
- package/docs/contributing/releasing.md +0 -82
- package/docs/contributing/visual-review-evidence.md +0 -53
- package/docs/design/families/README.md +0 -26
- package/docs/design/families/architecture-beta.md +0 -48
- package/docs/design/families/flowchart-parser-conformance.md +0 -22
- package/docs/design/families/gantt-research.md +0 -137
- package/docs/design/families/gantt.md +0 -329
- package/docs/design/families/journey-agentic-cards-before.png +0 -0
- package/docs/design/families/journey-experience-curve-implementation.png +0 -0
- package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
- package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
- package/docs/design/families/journey-migration-parity.md +0 -439
- package/docs/design/families/journey-section-overlap-after.png +0 -0
- package/docs/design/families/journey-section-overlap-before.png +0 -0
- package/docs/design/families/journey-usage-research.md +0 -423
- package/docs/design/families/journey.md +0 -153
- package/docs/design/families/xychart.md +0 -85
- package/docs/design/system/README.md +0 -45
- package/docs/design/system/abstraction-audit.md +0 -224
- package/docs/design/system/abstraction-recommendations.md +0 -655
- package/docs/design/system/architecture.mmd +0 -29
- package/docs/design/system/architecture.svg +0 -137
- package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
- package/docs/design/system/layout-rubric.md +0 -138
- package/docs/design/system/route-contracts.md +0 -848
- package/docs/design/system/source-preservation-ladder.md +0 -52
- package/docs/design/system/styles-rollout.md +0 -216
- package/docs/design/system/ugly-layouts.md +0 -119
- package/docs/layout-characterization/README.md +0 -245
- package/docs/layout-characterization/contact-sheet-families.md +0 -498
- package/docs/layout-characterization/contact-sheet.md +0 -638
- package/docs/layout-characterization/properties.md +0 -281
- package/docs/layout-characterization/visual-quality.md +0 -160
- package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
- package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
- package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
- package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
- package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/journey.svg +0 -98
- package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
- package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
- package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
- package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
- package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
- package/docs/project/divergences.md +0 -98
- package/docs/project/dogfooding-docs-strategy.md +0 -66
- package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
- package/docs/project/lessons-learned.md +0 -1019
- package/docs/project/remove-role-styling-plan.md +0 -176
- package/examples/agent-improve-auth-flow.ts +0 -206
- package/examples/agent-loop.ts +0 -53
- package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
- package/llms.txt +0 -104
- package/skill-evals/README.md +0 -77
- package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
- package/skill-evals/fixtures/class-payment/input.mmd +0 -4
- package/skill-evals/fixtures/er-orders/input.mmd +0 -10
- package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
- package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
- package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
- package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
- package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
- package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
- package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
- package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
- package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
- package/skill-evals/shared-benchmark.json +0 -510
- package/src/agent/analyze.ts +0 -157
- package/src/agent/apply.ts +0 -146
- package/src/agent/architecture-body.ts +0 -458
- package/src/agent/certificates.ts +0 -110
- package/src/agent/class-body.ts +0 -319
- package/src/agent/core.ts +0 -207
- package/src/agent/create.ts +0 -106
- package/src/agent/describe.ts +0 -368
- package/src/agent/er-body.ts +0 -224
- package/src/agent/facts.ts +0 -360
- package/src/agent/families-builtin.ts +0 -920
- package/src/agent/families.ts +0 -232
- package/src/agent/family-layouts.ts +0 -832
- package/src/agent/flowchart-body.ts +0 -372
- package/src/agent/flowchart-unsupported.ts +0 -243
- package/src/agent/font-coverage.ts +0 -288
- package/src/agent/gantt-body.ts +0 -495
- package/src/agent/index.ts +0 -10
- package/src/agent/journey-body.ts +0 -374
- package/src/agent/label-metrics.ts +0 -28
- package/src/agent/layout-to-rendered.ts +0 -63
- package/src/agent/mutate.ts +0 -97
- package/src/agent/mutation-ops.ts +0 -51
- package/src/agent/op-schema.ts +0 -443
- package/src/agent/parse.ts +0 -223
- package/src/agent/pie-body.ts +0 -270
- package/src/agent/png.ts +0 -171
- package/src/agent/quadrant-body.ts +0 -336
- package/src/agent/quality.ts +0 -363
- package/src/agent/readability-audit.ts +0 -92
- package/src/agent/rendered-route-audit.ts +0 -61
- package/src/agent/sequence-body.ts +0 -345
- package/src/agent/serialize.ts +0 -214
- package/src/agent/state-body.ts +0 -589
- package/src/agent/structural-count.ts +0 -150
- package/src/agent/timeline-body.ts +0 -280
- package/src/agent/trace-log.ts +0 -43
- package/src/agent/types.ts +0 -995
- package/src/agent/verify.ts +0 -603
- package/src/agent/xychart-body.ts +0 -472
- package/src/architecture/config.ts +0 -282
- package/src/architecture/layout.ts +0 -496
- package/src/architecture/parser.ts +0 -375
- package/src/architecture/renderer.ts +0 -611
- package/src/architecture/types.ts +0 -113
- package/src/ascii/ansi.ts +0 -423
- package/src/ascii/architecture.ts +0 -210
- package/src/ascii/canvas.ts +0 -443
- package/src/ascii/class-diagram.ts +0 -697
- package/src/ascii/converter.ts +0 -475
- package/src/ascii/draw.ts +0 -1693
- package/src/ascii/edge-bundling.ts +0 -328
- package/src/ascii/edge-routing.ts +0 -330
- package/src/ascii/er-diagram.ts +0 -439
- package/src/ascii/gantt.ts +0 -276
- package/src/ascii/grid.ts +0 -747
- package/src/ascii/index.ts +0 -196
- package/src/ascii/journey.ts +0 -169
- package/src/ascii/meta.ts +0 -244
- package/src/ascii/multiline-utils.ts +0 -80
- package/src/ascii/pathfinder.ts +0 -301
- package/src/ascii/pie.ts +0 -67
- package/src/ascii/quadrant.ts +0 -158
- package/src/ascii/reverse.ts +0 -216
- package/src/ascii/sequence.ts +0 -499
- package/src/ascii/shapes/circle.ts +0 -27
- package/src/ascii/shapes/corners.ts +0 -143
- package/src/ascii/shapes/diamond.ts +0 -27
- package/src/ascii/shapes/hexagon.ts +0 -27
- package/src/ascii/shapes/index.ts +0 -106
- package/src/ascii/shapes/rectangle.ts +0 -168
- package/src/ascii/shapes/rounded.ts +0 -27
- package/src/ascii/shapes/special.ts +0 -332
- package/src/ascii/shapes/stadium.ts +0 -110
- package/src/ascii/shapes/state.ts +0 -192
- package/src/ascii/shapes/types.ts +0 -73
- package/src/ascii/timeline.ts +0 -141
- package/src/ascii/types.ts +0 -312
- package/src/ascii/validate.ts +0 -120
- package/src/ascii/width.ts +0 -81
- package/src/ascii/wrap.ts +0 -131
- package/src/ascii/xychart.ts +0 -919
- package/src/browser.ts +0 -29
- package/src/class/layout.ts +0 -240
- package/src/class/parser.ts +0 -332
- package/src/class/renderer.ts +0 -638
- package/src/class/types.ts +0 -129
- package/src/cli/agent-instructions.ts +0 -1
- package/src/cli/am-bin.ts +0 -6
- package/src/cli/exit-codes.ts +0 -18
- package/src/cli/index.ts +0 -1132
- package/src/cli/init-agent.ts +0 -147
- package/src/cli/run-entrypoint.ts +0 -10
- package/src/color-resolver.ts +0 -152
- package/src/elk-instance.ts +0 -114
- package/src/er/layout.ts +0 -187
- package/src/er/parser.ts +0 -226
- package/src/er/renderer.ts +0 -792
- package/src/er/types.ts +0 -99
- package/src/family-rubric.ts +0 -425
- package/src/font-manifest.ts +0 -31
- package/src/gantt/layout.ts +0 -365
- package/src/gantt/parser.ts +0 -282
- package/src/gantt/pipeline.ts +0 -27
- package/src/gantt/renderer.ts +0 -414
- package/src/gantt/schedule.ts +0 -513
- package/src/gantt/types.ts +0 -255
- package/src/index.ts +0 -368
- package/src/journey/layout.ts +0 -554
- package/src/journey/parse-core.ts +0 -251
- package/src/journey/parser.ts +0 -101
- package/src/journey/renderer.ts +0 -917
- package/src/journey/types.ts +0 -144
- package/src/layout/geometry.ts +0 -182
- package/src/layout/pass.ts +0 -142
- package/src/layout/passes/index.ts +0 -3916
- package/src/layout-engine.ts +0 -1565
- package/src/layout-rubric.ts +0 -659
- package/src/mcp/artifacts.ts +0 -137
- package/src/mcp/dynamic-harness.ts +0 -31
- package/src/mcp/dynamic-modules.d.ts +0 -7
- package/src/mcp/facade.ts +0 -608
- package/src/mcp/harness-runtime.ts +0 -203
- package/src/mcp/hosted-server.ts +0 -468
- package/src/mcp/mcp-bin.ts +0 -3
- package/src/mcp/mcp-cli.ts +0 -82
- package/src/mcp/protocol.ts +0 -13
- package/src/mcp/sandbox.ts +0 -195
- package/src/mcp/sdk-decl.ts +0 -391
- package/src/mcp/server.ts +0 -362
- package/src/mcp/tool-surface.ts +0 -146
- package/src/mermaid-source.ts +0 -655
- package/src/multiline-utils.ts +0 -228
- package/src/parser.ts +0 -1031
- package/src/pie/layout.ts +0 -168
- package/src/pie/parser.ts +0 -136
- package/src/pie/renderer.ts +0 -234
- package/src/pie/types.ts +0 -82
- package/src/quadrant/layout.ts +0 -191
- package/src/quadrant/parser.ts +0 -183
- package/src/quadrant/renderer.ts +0 -322
- package/src/quadrant/types.ts +0 -113
- package/src/render-family-hooks.ts +0 -244
- package/src/renderer.ts +0 -1099
- package/src/route-contracts.ts +0 -2723
- package/src/scene/backend.ts +0 -76
- package/src/scene/fidelity.ts +0 -183
- package/src/scene/hybrid-backend.ts +0 -174
- package/src/scene/ir.ts +0 -181
- package/src/scene/marks.ts +0 -127
- package/src/scene/rough-backend.ts +0 -378
- package/src/scene/seed.ts +0 -46
- package/src/scene/style-registry.ts +0 -556
- package/src/sequence/layout.ts +0 -456
- package/src/sequence/parser.ts +0 -255
- package/src/sequence/renderer.ts +0 -604
- package/src/sequence/types.ts +0 -154
- package/src/shape-clipping.ts +0 -351
- package/src/shared/accessibility-directives.ts +0 -22
- package/src/shared/batched.ts +0 -61
- package/src/shared/color-math.ts +0 -93
- package/src/shared/svg-a11y.ts +0 -29
- package/src/shared/syntax-error.ts +0 -17
- package/src/shared/unicode-ranges.ts +0 -135
- package/src/styles.ts +0 -318
- package/src/svg-paths.ts +0 -28
- package/src/text-metrics.ts +0 -248
- package/src/theme.ts +0 -710
- package/src/timeline/layout.ts +0 -381
- package/src/timeline/parser.ts +0 -215
- package/src/timeline/renderer.ts +0 -538
- package/src/timeline/types.ts +0 -105
- package/src/types.ts +0 -462
- package/src/website-worker-modules.d.ts +0 -18
- package/src/xychart/axis-utils.ts +0 -109
- package/src/xychart/colors.ts +0 -124
- package/src/xychart/config.ts +0 -88
- package/src/xychart/layout.ts +0 -656
- package/src/xychart/parser.ts +0 -504
- package/src/xychart/renderer.ts +0 -640
- package/src/xychart/types.ts +0 -282
package/src/ascii/grid.ts
DELETED
|
@@ -1,747 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// ASCII renderer — grid-based layout
|
|
3
|
-
//
|
|
4
|
-
// Ported from AlexanderGrooff/mermaid-ascii cmd/graph.go + cmd/mapping_node.go.
|
|
5
|
-
// Places nodes on a logical grid, computes column/row sizes,
|
|
6
|
-
// converts grid coordinates to character-level drawing coordinates,
|
|
7
|
-
// and handles subgraph bounding boxes.
|
|
8
|
-
// ============================================================================
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
GridCoord, DrawingCoord, Direction, AsciiGraph, AsciiNode, AsciiSubgraph, AsciiEdge,
|
|
12
|
-
} from './types.ts'
|
|
13
|
-
import { gridKey, gridCoordDirection } from './types.ts'
|
|
14
|
-
import { mkCanvas, setCanvasSizeToGrid, setRoleCanvasSizeToGrid } from './canvas.ts'
|
|
15
|
-
import { determinePath, determineLabelLine } from './edge-routing.ts'
|
|
16
|
-
import { getPath, mergePath } from './pathfinder.ts'
|
|
17
|
-
import { analyzeEdgeBundles, processBundles } from './edge-bundling.ts'
|
|
18
|
-
import { drawBox } from './draw.ts'
|
|
19
|
-
import { maxLineWidth, lineCount } from './multiline-utils.ts'
|
|
20
|
-
import { getShapeDimensions } from './shapes/index.ts'
|
|
21
|
-
|
|
22
|
-
// ============================================================================
|
|
23
|
-
// Grid coordinate → drawing coordinate conversion
|
|
24
|
-
// ============================================================================
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Convert a grid coordinate to a drawing (character) coordinate.
|
|
28
|
-
* Sums column widths up to the target column, and row heights up to the target row,
|
|
29
|
-
* then centers within the cell.
|
|
30
|
-
*/
|
|
31
|
-
export function gridToDrawingCoord(
|
|
32
|
-
graph: AsciiGraph,
|
|
33
|
-
c: GridCoord,
|
|
34
|
-
dir?: Direction,
|
|
35
|
-
): DrawingCoord {
|
|
36
|
-
const target: GridCoord = dir
|
|
37
|
-
? { x: c.x + dir.x, y: c.y + dir.y }
|
|
38
|
-
: c
|
|
39
|
-
|
|
40
|
-
let x = 0
|
|
41
|
-
for (let col = 0; col < target.x; col++) {
|
|
42
|
-
x += graph.columnWidth.get(col) ?? 0
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let y = 0
|
|
46
|
-
for (let row = 0; row < target.y; row++) {
|
|
47
|
-
y += graph.rowHeight.get(row) ?? 0
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const colW = graph.columnWidth.get(target.x) ?? 0
|
|
51
|
-
const rowH = graph.rowHeight.get(target.y) ?? 0
|
|
52
|
-
return {
|
|
53
|
-
x: x + Math.floor(colW / 2) + graph.offsetX,
|
|
54
|
-
y: y + Math.floor(rowH / 2) + graph.offsetY,
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Convert a path of grid coords to drawing coords. */
|
|
59
|
-
export function lineToDrawing(graph: AsciiGraph, line: GridCoord[]): DrawingCoord[] {
|
|
60
|
-
return line.map(c => gridToDrawingCoord(graph, c))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ============================================================================
|
|
64
|
-
// Node placement on the grid
|
|
65
|
-
// ============================================================================
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Reserve a 3x3 block in the grid for a node.
|
|
69
|
-
* If the requested position is occupied, recursively shift by 4 grid units
|
|
70
|
-
* (in the perpendicular direction based on effective direction) until a free spot is found.
|
|
71
|
-
*
|
|
72
|
-
* @param effectiveDir - Optional direction override. If not provided, uses the node's
|
|
73
|
-
* effective direction (subgraph direction if in a subgraph with override,
|
|
74
|
-
* otherwise graph direction).
|
|
75
|
-
*/
|
|
76
|
-
export function reserveSpotInGrid(
|
|
77
|
-
graph: AsciiGraph,
|
|
78
|
-
node: AsciiNode,
|
|
79
|
-
requested: GridCoord,
|
|
80
|
-
effectiveDir?: 'LR' | 'TD',
|
|
81
|
-
): GridCoord {
|
|
82
|
-
// Determine direction for collision handling
|
|
83
|
-
const dir = effectiveDir ?? getEffectiveDirection(graph, node)
|
|
84
|
-
|
|
85
|
-
if (graph.grid.has(gridKey(requested))) {
|
|
86
|
-
// Collision — shift perpendicular to main flow direction
|
|
87
|
-
if (dir === 'LR') {
|
|
88
|
-
return reserveSpotInGrid(graph, node, { x: requested.x, y: requested.y + 4 }, dir)
|
|
89
|
-
} else {
|
|
90
|
-
return reserveSpotInGrid(graph, node, { x: requested.x + 4, y: requested.y }, dir)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Reserve the 3x3 block
|
|
95
|
-
for (let dx = 0; dx < 3; dx++) {
|
|
96
|
-
for (let dy = 0; dy < 3; dy++) {
|
|
97
|
-
const reserved: GridCoord = { x: requested.x + dx, y: requested.y + dy }
|
|
98
|
-
graph.grid.set(gridKey(reserved), node)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
node.gridCoord = requested
|
|
103
|
-
return requested
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ============================================================================
|
|
107
|
-
// Column width / row height computation
|
|
108
|
-
// ============================================================================
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Set column widths and row heights for a node's 3x3 grid block.
|
|
112
|
-
* Each node occupies 3 columns (border, content, border) and 3 rows.
|
|
113
|
-
* Uses shape-aware dimensions to properly size non-rectangular shapes.
|
|
114
|
-
*/
|
|
115
|
-
export function setColumnWidth(graph: AsciiGraph, node: AsciiNode): void {
|
|
116
|
-
const gc = node.gridCoord!
|
|
117
|
-
const padding = graph.config.boxBorderPadding
|
|
118
|
-
|
|
119
|
-
// Get shape-aware dimensions
|
|
120
|
-
const shapeDims = getShapeDimensions(node.shape, node.displayLabel, {
|
|
121
|
-
useAscii: graph.config.useAscii,
|
|
122
|
-
padding,
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
// Use shape-provided grid dimensions
|
|
126
|
-
const colWidths = shapeDims.gridColumns
|
|
127
|
-
const rowHeights = shapeDims.gridRows
|
|
128
|
-
|
|
129
|
-
for (let idx = 0; idx < colWidths.length; idx++) {
|
|
130
|
-
const xCoord = gc.x + idx
|
|
131
|
-
const current = graph.columnWidth.get(xCoord) ?? 0
|
|
132
|
-
graph.columnWidth.set(xCoord, Math.max(current, colWidths[idx]!))
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
for (let idx = 0; idx < rowHeights.length; idx++) {
|
|
136
|
-
const yCoord = gc.y + idx
|
|
137
|
-
const current = graph.rowHeight.get(yCoord) ?? 0
|
|
138
|
-
graph.rowHeight.set(yCoord, Math.max(current, rowHeights[idx]!))
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Padding column/row before the node (spacing between nodes)
|
|
142
|
-
if (gc.x > 0) {
|
|
143
|
-
const current = graph.columnWidth.get(gc.x - 1) ?? 0
|
|
144
|
-
graph.columnWidth.set(gc.x - 1, Math.max(current, graph.config.paddingX))
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (gc.y > 0) {
|
|
148
|
-
let basePadding = graph.config.paddingY
|
|
149
|
-
// Extra vertical padding for nodes with incoming edges from outside their subgraph
|
|
150
|
-
if (hasIncomingEdgeFromOutsideSubgraph(graph, node)) {
|
|
151
|
-
const subgraphOverhead = 4
|
|
152
|
-
basePadding += subgraphOverhead
|
|
153
|
-
}
|
|
154
|
-
const current = graph.rowHeight.get(gc.y - 1) ?? 0
|
|
155
|
-
graph.rowHeight.set(gc.y - 1, Math.max(current, basePadding))
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Ensure grid has width/height entries for all cells along an edge path. */
|
|
160
|
-
export function increaseGridSizeForPath(graph: AsciiGraph, path: GridCoord[]): void {
|
|
161
|
-
for (const c of path) {
|
|
162
|
-
if (!graph.columnWidth.has(c.x)) {
|
|
163
|
-
graph.columnWidth.set(c.x, Math.floor(graph.config.paddingX / 2))
|
|
164
|
-
}
|
|
165
|
-
if (!graph.rowHeight.has(c.y)) {
|
|
166
|
-
graph.rowHeight.set(c.y, Math.floor(graph.config.paddingY / 2))
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ============================================================================
|
|
172
|
-
// Subgraph helpers
|
|
173
|
-
// ============================================================================
|
|
174
|
-
|
|
175
|
-
function isNodeInAnySubgraph(graph: AsciiGraph, node: AsciiNode): boolean {
|
|
176
|
-
return graph.subgraphs.some(sg => sg.nodes.includes(node))
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Get the innermost subgraph that directly contains this node.
|
|
181
|
-
* Returns null if node is not in any subgraph.
|
|
182
|
-
*/
|
|
183
|
-
export function getNodeSubgraph(graph: AsciiGraph, node: AsciiNode): AsciiSubgraph | null {
|
|
184
|
-
// Find the innermost (most deeply nested) subgraph containing the node
|
|
185
|
-
let innermost: AsciiSubgraph | null = null
|
|
186
|
-
for (const sg of graph.subgraphs) {
|
|
187
|
-
if (sg.nodes.includes(node)) {
|
|
188
|
-
// Check if this subgraph is deeper (more nested) than current innermost
|
|
189
|
-
if (!innermost || isAncestorOrSelf(innermost, sg)) {
|
|
190
|
-
innermost = sg
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return innermost
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Check if `candidate` is the same as or an ancestor of `target`. */
|
|
198
|
-
function isAncestorOrSelf(candidate: AsciiSubgraph, target: AsciiSubgraph): boolean {
|
|
199
|
-
let current: AsciiSubgraph | null = target
|
|
200
|
-
while (current !== null) {
|
|
201
|
-
if (current === candidate) return true
|
|
202
|
-
current = current.parent
|
|
203
|
-
}
|
|
204
|
-
return false
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Get the effective direction for a node's layout.
|
|
209
|
-
* Returns the subgraph's direction override if the node is in a subgraph with one,
|
|
210
|
-
* otherwise returns the graph-level direction.
|
|
211
|
-
*/
|
|
212
|
-
export function getEffectiveDirection(graph: AsciiGraph, node: AsciiNode): 'LR' | 'TD' {
|
|
213
|
-
const sg = getNodeSubgraph(graph, node)
|
|
214
|
-
if (sg?.direction) {
|
|
215
|
-
return sg.direction
|
|
216
|
-
}
|
|
217
|
-
return graph.config.graphDirection
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Check if a node has an incoming edge from outside its subgraph
|
|
222
|
-
* AND is the topmost such node in its subgraph.
|
|
223
|
-
* Used to add extra vertical padding for subgraph borders.
|
|
224
|
-
*/
|
|
225
|
-
function hasIncomingEdgeFromOutsideSubgraph(graph: AsciiGraph, node: AsciiNode): boolean {
|
|
226
|
-
const nodeSg = getNodeSubgraph(graph, node)
|
|
227
|
-
if (!nodeSg) return false
|
|
228
|
-
|
|
229
|
-
let hasExternalEdge = false
|
|
230
|
-
for (const edge of graph.edges) {
|
|
231
|
-
if (edge.to === node) {
|
|
232
|
-
const sourceSg = getNodeSubgraph(graph, edge.from)
|
|
233
|
-
if (sourceSg !== nodeSg) {
|
|
234
|
-
hasExternalEdge = true
|
|
235
|
-
break
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (!hasExternalEdge) return false
|
|
241
|
-
|
|
242
|
-
// Only return true for the topmost node with an external incoming edge
|
|
243
|
-
for (const otherNode of nodeSg.nodes) {
|
|
244
|
-
if (otherNode === node || !otherNode.gridCoord) continue
|
|
245
|
-
let otherHasExternal = false
|
|
246
|
-
for (const edge of graph.edges) {
|
|
247
|
-
if (edge.to === otherNode) {
|
|
248
|
-
const sourceSg = getNodeSubgraph(graph, edge.from)
|
|
249
|
-
if (sourceSg !== nodeSg) {
|
|
250
|
-
otherHasExternal = true
|
|
251
|
-
break
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
if (otherHasExternal && otherNode.gridCoord.y < node.gridCoord!.y) {
|
|
256
|
-
return false
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return true
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ============================================================================
|
|
264
|
-
// Subgraph bounding boxes
|
|
265
|
-
// ============================================================================
|
|
266
|
-
|
|
267
|
-
function calculateSubgraphBoundingBox(graph: AsciiGraph, sg: AsciiSubgraph): void {
|
|
268
|
-
if (sg.nodes.length === 0) return
|
|
269
|
-
|
|
270
|
-
let minX = 1_000_000
|
|
271
|
-
let minY = 1_000_000
|
|
272
|
-
let maxX = -1_000_000
|
|
273
|
-
let maxY = -1_000_000
|
|
274
|
-
|
|
275
|
-
// Include children's bounding boxes
|
|
276
|
-
for (const child of sg.children) {
|
|
277
|
-
calculateSubgraphBoundingBox(graph, child)
|
|
278
|
-
if (child.nodes.length > 0) {
|
|
279
|
-
minX = Math.min(minX, child.minX)
|
|
280
|
-
minY = Math.min(minY, child.minY)
|
|
281
|
-
maxX = Math.max(maxX, child.maxX)
|
|
282
|
-
maxY = Math.max(maxY, child.maxY)
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Include node positions
|
|
287
|
-
for (const node of sg.nodes) {
|
|
288
|
-
if (!node.drawingCoord || !node.drawing) continue
|
|
289
|
-
const nodeMinX = node.drawingCoord.x
|
|
290
|
-
const nodeMinY = node.drawingCoord.y
|
|
291
|
-
const nodeMaxX = nodeMinX + node.drawing.length - 1
|
|
292
|
-
const nodeMaxY = nodeMinY + node.drawing[0]!.length - 1
|
|
293
|
-
minX = Math.min(minX, nodeMinX)
|
|
294
|
-
minY = Math.min(minY, nodeMinY)
|
|
295
|
-
maxX = Math.max(maxX, nodeMaxX)
|
|
296
|
-
maxY = Math.max(maxY, nodeMaxY)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const subgraphPadding = 2
|
|
300
|
-
const subgraphLabelSpace = 2
|
|
301
|
-
sg.minX = minX - subgraphPadding
|
|
302
|
-
sg.minY = minY - subgraphPadding - subgraphLabelSpace
|
|
303
|
-
sg.maxX = maxX + subgraphPadding
|
|
304
|
-
sg.maxY = maxY + subgraphPadding
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** Ensure non-overlapping root subgraphs have minimum spacing. */
|
|
308
|
-
function ensureSubgraphSpacing(graph: AsciiGraph): void {
|
|
309
|
-
const minSpacing = 1
|
|
310
|
-
const rootSubgraphs = graph.subgraphs.filter(sg => sg.parent === null && sg.nodes.length > 0)
|
|
311
|
-
|
|
312
|
-
for (let i = 0; i < rootSubgraphs.length; i++) {
|
|
313
|
-
for (let j = i + 1; j < rootSubgraphs.length; j++) {
|
|
314
|
-
const sg1 = rootSubgraphs[i]!
|
|
315
|
-
const sg2 = rootSubgraphs[j]!
|
|
316
|
-
|
|
317
|
-
// Horizontal overlap → adjust vertical
|
|
318
|
-
if (sg1.minX < sg2.maxX && sg1.maxX > sg2.minX) {
|
|
319
|
-
if (sg1.maxY >= sg2.minY - minSpacing && sg1.minY < sg2.minY) {
|
|
320
|
-
sg2.minY = sg1.maxY + minSpacing + 1
|
|
321
|
-
} else if (sg2.maxY >= sg1.minY - minSpacing && sg2.minY < sg1.minY) {
|
|
322
|
-
sg1.minY = sg2.maxY + minSpacing + 1
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
// Vertical overlap → adjust horizontal
|
|
326
|
-
if (sg1.minY < sg2.maxY && sg1.maxY > sg2.minY) {
|
|
327
|
-
if (sg1.maxX >= sg2.minX - minSpacing && sg1.minX < sg2.minX) {
|
|
328
|
-
sg2.minX = sg1.maxX + minSpacing + 1
|
|
329
|
-
} else if (sg2.maxX >= sg1.minX - minSpacing && sg2.minX < sg1.minX) {
|
|
330
|
-
sg1.minX = sg2.maxX + minSpacing + 1
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export function calculateSubgraphBoundingBoxes(graph: AsciiGraph): void {
|
|
338
|
-
for (const sg of graph.subgraphs) {
|
|
339
|
-
calculateSubgraphBoundingBox(graph, sg)
|
|
340
|
-
}
|
|
341
|
-
ensureSubgraphSpacing(graph)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Offset all drawing coordinates so subgraph borders don't go negative.
|
|
346
|
-
* If any subgraph has negative min coordinates, shift everything positive.
|
|
347
|
-
*/
|
|
348
|
-
export function offsetDrawingForSubgraphs(graph: AsciiGraph): void {
|
|
349
|
-
if (graph.subgraphs.length === 0) return
|
|
350
|
-
|
|
351
|
-
let minX = 0
|
|
352
|
-
let minY = 0
|
|
353
|
-
for (const sg of graph.subgraphs) {
|
|
354
|
-
minX = Math.min(minX, sg.minX)
|
|
355
|
-
minY = Math.min(minY, sg.minY)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const offsetX = -minX
|
|
359
|
-
const offsetY = -minY
|
|
360
|
-
if (offsetX === 0 && offsetY === 0) return
|
|
361
|
-
|
|
362
|
-
graph.offsetX = offsetX
|
|
363
|
-
graph.offsetY = offsetY
|
|
364
|
-
|
|
365
|
-
for (const sg of graph.subgraphs) {
|
|
366
|
-
sg.minX += offsetX
|
|
367
|
-
sg.minY += offsetY
|
|
368
|
-
sg.maxX += offsetX
|
|
369
|
-
sg.maxY += offsetY
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
for (const node of graph.nodes) {
|
|
373
|
-
if (node.drawingCoord) {
|
|
374
|
-
node.drawingCoord.x += offsetX
|
|
375
|
-
node.drawingCoord.y += offsetY
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// ============================================================================
|
|
381
|
-
// Main layout orchestrator
|
|
382
|
-
// ============================================================================
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* createMapping performs the full grid layout:
|
|
386
|
-
* 1. Place root nodes on the grid
|
|
387
|
-
* 2. Place child nodes level by level
|
|
388
|
-
* 3. Compute column widths and row heights
|
|
389
|
-
* 4. Run A* pathfinding for all edges
|
|
390
|
-
* 5. Determine label placement
|
|
391
|
-
* 6. Convert grid coords → drawing coords
|
|
392
|
-
* 7. Generate node box drawings
|
|
393
|
-
* 8. Calculate subgraph bounding boxes
|
|
394
|
-
*/
|
|
395
|
-
export function createMapping(graph: AsciiGraph): void {
|
|
396
|
-
const dir = graph.config.graphDirection
|
|
397
|
-
// Sparse level tracker — levels grow by 4 per generation, so a fixed-size
|
|
398
|
-
// array would run out on deep chains (a ~25-node chain already reaches
|
|
399
|
-
// level 100) and the resulting `undefined` positions turned into NaN grid
|
|
400
|
-
// coordinates that sent A* pathfinding into an unbounded search (hang).
|
|
401
|
-
const highestPositionPerLevel = new Map<number, number>()
|
|
402
|
-
const levelPos = (level: number): number => highestPositionPerLevel.get(level) ?? 0
|
|
403
|
-
|
|
404
|
-
// Identify root nodes — nodes that aren't the target of any edge
|
|
405
|
-
const nodesFound = new Set<string>()
|
|
406
|
-
const initialRoots: AsciiNode[] = []
|
|
407
|
-
|
|
408
|
-
for (const node of graph.nodes) {
|
|
409
|
-
if (!nodesFound.has(node.name)) {
|
|
410
|
-
initialRoots.push(node)
|
|
411
|
-
}
|
|
412
|
-
nodesFound.add(node.name)
|
|
413
|
-
for (const child of getChildren(graph, node)) {
|
|
414
|
-
nodesFound.add(child.name)
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Filter out subgraph nodes that have incoming edges from external sources.
|
|
419
|
-
// This handles the case where subgraph is declared before external nodes
|
|
420
|
-
// (e.g., `subgraph s; A-->B; end; X-->A` - A shouldn't be a root, X should).
|
|
421
|
-
const rootNodes = initialRoots.filter(node => {
|
|
422
|
-
const nodeSg = getNodeSubgraph(graph, node)
|
|
423
|
-
if (!nodeSg) return true // external nodes: keep as roots
|
|
424
|
-
|
|
425
|
-
// Check if this subgraph node has incoming edges from outside its subgraph
|
|
426
|
-
for (const edge of graph.edges) {
|
|
427
|
-
if (edge.to === node) {
|
|
428
|
-
const sourceSg = getNodeSubgraph(graph, edge.from)
|
|
429
|
-
if (sourceSg !== nodeSg) {
|
|
430
|
-
return false // has external incoming edge → not a root
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return true
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
// In LR mode with both external and subgraph roots, separate them
|
|
438
|
-
// so subgraph roots are placed one level deeper
|
|
439
|
-
let hasExternalRoots = false
|
|
440
|
-
let hasSubgraphRootsWithEdges = false
|
|
441
|
-
for (const node of rootNodes) {
|
|
442
|
-
if (isNodeInAnySubgraph(graph, node)) {
|
|
443
|
-
if (getChildren(graph, node).length > 0) hasSubgraphRootsWithEdges = true
|
|
444
|
-
} else {
|
|
445
|
-
hasExternalRoots = true
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
const shouldSeparate = dir === 'LR' && hasExternalRoots && hasSubgraphRootsWithEdges
|
|
449
|
-
|
|
450
|
-
let externalRootNodes: AsciiNode[]
|
|
451
|
-
let subgraphRootNodes: AsciiNode[] = []
|
|
452
|
-
|
|
453
|
-
if (shouldSeparate) {
|
|
454
|
-
externalRootNodes = rootNodes.filter(n => !isNodeInAnySubgraph(graph, n))
|
|
455
|
-
subgraphRootNodes = rootNodes.filter(n => isNodeInAnySubgraph(graph, n))
|
|
456
|
-
} else {
|
|
457
|
-
externalRootNodes = rootNodes
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Group roots that feed the same first target so they sit contiguously
|
|
461
|
-
// (upstream lukilabs#69): interleaved declarations otherwise scatter each
|
|
462
|
-
// fan-in group across the level and their edge trunks cross. The sort is
|
|
463
|
-
// stable, so declaration order survives within each group and graphs
|
|
464
|
-
// without shared targets are unaffected.
|
|
465
|
-
const rootGroupKey = (n: AsciiNode): string => {
|
|
466
|
-
const kids = getChildren(graph, n)
|
|
467
|
-
return kids.length > 0 ? kids[0]!.name : `__ungrouped__${n.name}`
|
|
468
|
-
}
|
|
469
|
-
const rootGroupOrder = new Map<string, number>()
|
|
470
|
-
for (const n of externalRootNodes) {
|
|
471
|
-
const k = rootGroupKey(n)
|
|
472
|
-
if (!rootGroupOrder.has(k)) rootGroupOrder.set(k, rootGroupOrder.size)
|
|
473
|
-
}
|
|
474
|
-
externalRootNodes = [...externalRootNodes]
|
|
475
|
-
.sort((a, b) => rootGroupOrder.get(rootGroupKey(a))! - rootGroupOrder.get(rootGroupKey(b))!)
|
|
476
|
-
|
|
477
|
-
// Forward in-degree per node, for fan-in target alignment below
|
|
478
|
-
// (fan-in trunk grouping, issues #68/#69). SVG and ASCII now share the
|
|
479
|
-
// classifier from route-contracts.ts; only primary-forward edges contribute
|
|
480
|
-
// to deepest-parent placement. The reciprocal-neighbor fallback is retained
|
|
481
|
-
// for older/incomplete graphs where a routeClass is absent.
|
|
482
|
-
const outNeighbors = new Map<string, Set<string>>()
|
|
483
|
-
for (const edge of graph.edges) {
|
|
484
|
-
let s = outNeighbors.get(edge.from.name)
|
|
485
|
-
if (!s) { s = new Set(); outNeighbors.set(edge.from.name, s) }
|
|
486
|
-
s.add(edge.to.name)
|
|
487
|
-
}
|
|
488
|
-
const inDegree = new Map<string, number>()
|
|
489
|
-
// Forward parents per child (same exclusions as inDegree): used for
|
|
490
|
-
// longest-path layering, so a fan-in target waits for its DEEPEST parent
|
|
491
|
-
// instead of being parked at the level of whichever parent places first
|
|
492
|
-
// (which made later parents' edges run backward — issue #25 criterion 1).
|
|
493
|
-
const forwardParents = new Map<string, Array<typeof graph.nodes[number]>>()
|
|
494
|
-
for (const edge of graph.edges) {
|
|
495
|
-
// Skip self-loops: a node is not its own fan-in source (issues #68/#69).
|
|
496
|
-
if (edge.from.name === edge.to.name) continue
|
|
497
|
-
if (edge.routeClass !== undefined) {
|
|
498
|
-
if (edge.routeClass !== 'primary-forward') continue
|
|
499
|
-
} else if (outNeighbors.get(edge.to.name)?.has(edge.from.name)) {
|
|
500
|
-
// Back-compat fallback: skip 2-cycle back-edges when no shared route
|
|
501
|
-
// classification was attached by the converter.
|
|
502
|
-
continue
|
|
503
|
-
}
|
|
504
|
-
inDegree.set(edge.to.name, (inDegree.get(edge.to.name) ?? 0) + 1)
|
|
505
|
-
if (!forwardParents.has(edge.to.name)) forwardParents.set(edge.to.name, [])
|
|
506
|
-
forwardParents.get(edge.to.name)!.push(edge.from)
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Place external root nodes
|
|
510
|
-
for (const node of externalRootNodes) {
|
|
511
|
-
const requested: GridCoord = dir === 'LR'
|
|
512
|
-
? { x: 0, y: levelPos(0) }
|
|
513
|
-
: { x: levelPos(0), y: 0 }
|
|
514
|
-
reserveSpotInGrid(graph, graph.nodes[node.index]!, requested)
|
|
515
|
-
highestPositionPerLevel.set(0, levelPos(0) + 4)
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Place subgraph root nodes at level 4 (one level in from the edge)
|
|
519
|
-
if (shouldSeparate && subgraphRootNodes.length > 0) {
|
|
520
|
-
const subgraphLevel = 4
|
|
521
|
-
for (const node of subgraphRootNodes) {
|
|
522
|
-
const requested: GridCoord = dir === 'LR'
|
|
523
|
-
? { x: subgraphLevel, y: levelPos(subgraphLevel) }
|
|
524
|
-
: { x: levelPos(subgraphLevel), y: subgraphLevel }
|
|
525
|
-
reserveSpotInGrid(graph, graph.nodes[node.index]!, requested)
|
|
526
|
-
highestPositionPerLevel.set(subgraphLevel, levelPos(subgraphLevel) + 4)
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Place child nodes level by level
|
|
531
|
-
// Use subgraph direction only when both parent and child are in the same subgraph
|
|
532
|
-
// Multi-pass: iterate until all nodes are placed (handles non-topological node order)
|
|
533
|
-
// Note: when shouldSeparate, externalRootNodes + subgraphRootNodes = rootNodes
|
|
534
|
-
// otherwise, externalRootNodes = rootNodes and subgraphRootNodes is empty
|
|
535
|
-
let placedCount = externalRootNodes.length + subgraphRootNodes.length
|
|
536
|
-
// Longest-path layering needs cycle tolerance: when a whole pass places
|
|
537
|
-
// nothing because children are waiting on parents stuck in a cycle, one
|
|
538
|
-
// forced pass falls back to greedy placement, then waiting resumes.
|
|
539
|
-
let force = false
|
|
540
|
-
while (placedCount < graph.nodes.length) {
|
|
541
|
-
const prevCount = placedCount
|
|
542
|
-
for (const node of graph.nodes) {
|
|
543
|
-
if (node.gridCoord === null) continue // skip unplaced nodes
|
|
544
|
-
const gc = node.gridCoord
|
|
545
|
-
|
|
546
|
-
for (const edge of getEdgesFromNode(graph, node)) {
|
|
547
|
-
const child = edge.to
|
|
548
|
-
if (child.gridCoord !== null) continue // already placed
|
|
549
|
-
// Longest-path layering: wait until every forward parent is placed,
|
|
550
|
-
// so the child lands after its deepest parent.
|
|
551
|
-
if (!force && (forwardParents.get(child.name) ?? []).some(p => p.gridCoord === null)) continue
|
|
552
|
-
|
|
553
|
-
// Determine direction for this edge (parent -> child)
|
|
554
|
-
// Use subgraph direction only if both are in the same subgraph with override
|
|
555
|
-
const parentSg = getNodeSubgraph(graph, node)
|
|
556
|
-
const childSg = getNodeSubgraph(graph, child)
|
|
557
|
-
const edgeDir = (parentSg && parentSg === childSg && parentSg.direction)
|
|
558
|
-
? parentSg.direction
|
|
559
|
-
: graph.config.graphDirection
|
|
560
|
-
|
|
561
|
-
// Longest path: the child's level comes from its deepest placed
|
|
562
|
-
// forward parent, not from whichever parent iterates first.
|
|
563
|
-
let parentLevel = edgeDir === 'LR' ? gc.x : gc.y
|
|
564
|
-
if (edgeDir === graph.config.graphDirection) {
|
|
565
|
-
for (const parent of forwardParents.get(child.name) ?? []) {
|
|
566
|
-
if (parent.gridCoord === null) continue
|
|
567
|
-
parentLevel = Math.max(parentLevel, edgeDir === 'LR' ? parent.gridCoord.x : parent.gridCoord.y)
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
const childLevel = parentLevel + 4
|
|
571
|
-
|
|
572
|
-
// Determine position based on direction context
|
|
573
|
-
let highestPosition: number
|
|
574
|
-
if (edgeDir !== graph.config.graphDirection) {
|
|
575
|
-
// Cross-direction: use parent's perpendicular coordinate
|
|
576
|
-
// This keeps children aligned with parent when direction changes
|
|
577
|
-
highestPosition = edgeDir === 'LR' ? gc.y : gc.x
|
|
578
|
-
} else if (edge.fromSubgraph) {
|
|
579
|
-
// Container-to-node edges should emerge from the subgraph's anchor
|
|
580
|
-
// side, not from the next free root slot, otherwise a TD edge from a
|
|
581
|
-
// subgraph to a following node can jump sideways and render backward.
|
|
582
|
-
highestPosition = Math.max(
|
|
583
|
-
levelPos(childLevel),
|
|
584
|
-
edgeDir === 'LR' ? gc.y : gc.x,
|
|
585
|
-
)
|
|
586
|
-
} else if ((inDegree.get(child.name) ?? 0) > 1) {
|
|
587
|
-
// Fan-in target: align with the parent group's perpendicular
|
|
588
|
-
// position (upstream lukilabs#69) instead of the next sequential
|
|
589
|
-
// slot, so each target sits under its own root group and trunk
|
|
590
|
-
// rows of different fan-in groups don't collide.
|
|
591
|
-
highestPosition = Math.max(
|
|
592
|
-
levelPos(childLevel),
|
|
593
|
-
edgeDir === 'LR' ? gc.y : gc.x,
|
|
594
|
-
)
|
|
595
|
-
} else {
|
|
596
|
-
// Same direction: use level tracker
|
|
597
|
-
highestPosition = levelPos(childLevel)
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
const requested: GridCoord = edgeDir === 'LR'
|
|
601
|
-
? { x: childLevel, y: highestPosition }
|
|
602
|
-
: { x: highestPosition, y: childLevel }
|
|
603
|
-
reserveSpotInGrid(graph, graph.nodes[child.index]!, requested, edgeDir)
|
|
604
|
-
|
|
605
|
-
// Only update level tracker for same-direction placements
|
|
606
|
-
if (edgeDir === graph.config.graphDirection) {
|
|
607
|
-
highestPositionPerLevel.set(childLevel, highestPosition + 4)
|
|
608
|
-
}
|
|
609
|
-
placedCount++
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
if (placedCount === prevCount) {
|
|
613
|
-
// Safety: break if even a forced pass made no progress (disconnected
|
|
614
|
-
// nodes). A first stall means children are waiting on a cycle — run
|
|
615
|
-
// one greedy pass to break the deadlock, then resume waiting.
|
|
616
|
-
if (force) break
|
|
617
|
-
force = true
|
|
618
|
-
} else {
|
|
619
|
-
force = false
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Compute column widths and row heights
|
|
624
|
-
for (const node of graph.nodes) {
|
|
625
|
-
setColumnWidth(graph, node)
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Analyze edges for bundling (parallel links like A & B --> C)
|
|
629
|
-
// This groups edges that share sources or targets for cleaner visualization
|
|
630
|
-
graph.bundles = analyzeEdgeBundles(graph)
|
|
631
|
-
|
|
632
|
-
// Route bundled edges through junction points
|
|
633
|
-
processBundles(graph)
|
|
634
|
-
|
|
635
|
-
// Route non-bundled edges via A* and determine label positions
|
|
636
|
-
for (const edge of graph.edges) {
|
|
637
|
-
// Skip edges already routed as part of a bundle. processBundles draws the
|
|
638
|
-
// shared trunk once and writes each member's path; re-running A* here would
|
|
639
|
-
// overwrite that path and break the merged junction. The path.length guard
|
|
640
|
-
// ensures we only skip edges that actually received a routed path.
|
|
641
|
-
if (edge.bundle && edge.path.length > 0) {
|
|
642
|
-
increaseGridSizeForPath(graph, edge.path)
|
|
643
|
-
determineLabelLine(graph, edge)
|
|
644
|
-
continue
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
determinePath(graph, edge)
|
|
648
|
-
increaseGridSizeForPath(graph, edge.path)
|
|
649
|
-
determineLabelLine(graph, edge)
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
shareSiblingEdgeTrunks(graph)
|
|
653
|
-
|
|
654
|
-
// Convert grid coords → drawing coords and generate box drawings
|
|
655
|
-
for (const node of graph.nodes) {
|
|
656
|
-
node.drawingCoord = gridToDrawingCoord(graph, node.gridCoord!)
|
|
657
|
-
node.drawing = drawBox(node, graph)
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// Set canvas size and compute subgraph bounding boxes
|
|
661
|
-
setCanvasSizeToGrid(graph.canvas, graph.columnWidth, graph.rowHeight)
|
|
662
|
-
setRoleCanvasSizeToGrid(graph.roleCanvas, graph.columnWidth, graph.rowHeight)
|
|
663
|
-
calculateSubgraphBoundingBoxes(graph)
|
|
664
|
-
offsetDrawingForSubgraphs(graph)
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// ============================================================================
|
|
668
|
-
// Sibling trunk sharing
|
|
669
|
-
// ============================================================================
|
|
670
|
-
|
|
671
|
-
/**
|
|
672
|
-
* Re-route sibling edges from the same source/start side through the first
|
|
673
|
-
* edge's initial trunk. This fixes labeled fan-outs that cannot use bundle
|
|
674
|
-
* routing: A* otherwise solves each sibling independently and can send later
|
|
675
|
-
* branches on avoidable L-shaped detours (trunk sharing, issues #111/#113).
|
|
676
|
-
*/
|
|
677
|
-
function shareSiblingEdgeTrunks(graph: AsciiGraph): void {
|
|
678
|
-
const groups = new Map<string, AsciiEdge[]>()
|
|
679
|
-
for (const edge of graph.edges) {
|
|
680
|
-
// Bundled edges already share a routed trunk via processBundles; sharing
|
|
681
|
-
// again here would fight that routing (issues #111/#113).
|
|
682
|
-
if (edge.bundle) continue
|
|
683
|
-
const key = `${edge.from.name}:${edge.startDir.x},${edge.startDir.y}`
|
|
684
|
-
const list = groups.get(key) ?? []
|
|
685
|
-
list.push(edge)
|
|
686
|
-
groups.set(key, list)
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
const junctionKeys = new Set(graph.trunkJunctions.map(gridKey))
|
|
690
|
-
|
|
691
|
-
for (const edges of groups.values()) {
|
|
692
|
-
if (edges.length < 2) continue
|
|
693
|
-
// Self-loops have no meaningful trunk to share and would inject a spurious
|
|
694
|
-
// junction; exclude any group containing one (issues #111/#113).
|
|
695
|
-
if (edges.some(e => e.from === e.to)) continue
|
|
696
|
-
|
|
697
|
-
const first = edges[0]!
|
|
698
|
-
const firstPath = first.path
|
|
699
|
-
let branchIdx = -1
|
|
700
|
-
if (firstPath.length >= 3) {
|
|
701
|
-
const dx = firstPath[1]!.x - firstPath[0]!.x
|
|
702
|
-
const dy = firstPath[1]!.y - firstPath[0]!.y
|
|
703
|
-
for (let i = 2; i < firstPath.length; i++) {
|
|
704
|
-
const ndx = firstPath[i]!.x - firstPath[i - 1]!.x
|
|
705
|
-
const ndy = firstPath[i]!.y - firstPath[i - 1]!.y
|
|
706
|
-
if (ndx !== dx || ndy !== dy) {
|
|
707
|
-
branchIdx = i - 1
|
|
708
|
-
break
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
if (branchIdx === -1) continue
|
|
713
|
-
|
|
714
|
-
const trunk = firstPath.slice(0, branchIdx + 1)
|
|
715
|
-
const branchPoint = firstPath[branchIdx]!
|
|
716
|
-
|
|
717
|
-
for (let i = 1; i < edges.length; i++) {
|
|
718
|
-
const edge = edges[i]!
|
|
719
|
-
const target = gridCoordDirection(edge.to.gridCoord!, edge.endDir)
|
|
720
|
-
const route = getPath(graph.grid, branchPoint, target, edge.startDir)
|
|
721
|
-
if (!route) continue
|
|
722
|
-
|
|
723
|
-
edge.path = [...trunk, ...mergePath(route).slice(1)]
|
|
724
|
-
const key = gridKey(branchPoint)
|
|
725
|
-
if (!junctionKeys.has(key)) {
|
|
726
|
-
graph.trunkJunctions.push(branchPoint)
|
|
727
|
-
junctionKeys.add(key)
|
|
728
|
-
}
|
|
729
|
-
increaseGridSizeForPath(graph, edge.path)
|
|
730
|
-
determineLabelLine(graph, edge)
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// ============================================================================
|
|
736
|
-
// Graph traversal helpers
|
|
737
|
-
// ============================================================================
|
|
738
|
-
|
|
739
|
-
/** Get all edges originating from a node. */
|
|
740
|
-
function getEdgesFromNode(graph: AsciiGraph, node: AsciiNode): AsciiGraph['edges'] {
|
|
741
|
-
return graph.edges.filter(e => e.from.name === node.name)
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
/** Get all direct children of a node (targets of outgoing edges). */
|
|
745
|
-
function getChildren(graph: AsciiGraph, node: AsciiNode): AsciiNode[] {
|
|
746
|
-
return getEdgesFromNode(graph, node).map(e => e.to)
|
|
747
|
-
}
|