@ogabrielluiz/patchflow 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  SIGNAL_OPERATORS,
3
3
  __commonJS,
4
4
  __toESM
5
- } from "./chunk-B7DUSPQH.js";
5
+ } from "./chunk-4VUBNFI4.js";
6
6
 
7
7
  // node_modules/@dagrejs/graphlib/lib/graph.js
8
8
  var require_graph = __commonJS({
@@ -3542,7 +3542,8 @@ var errorMessages = {
3542
3542
  duplicateModule: (name) => `Module "${name}" declared more than once.`,
3543
3543
  didYouMean: (got, suggestion) => `Module "${got}" is not declared. Did you mean "${suggestion}"?`,
3544
3544
  syntaxError: (detail) => `Syntax error: ${detail}`,
3545
- emptyDiagram: () => `Diagram is empty \u2014 no connections found.`
3545
+ emptyDiagram: () => `Diagram is empty \u2014 no connections found.`,
3546
+ ambiguousPortDirection: (port, module) => `Port "${port}" on module "${module}" is used as both input and output. Use distinct names like "In ${port}"/"Out ${port}" to disambiguate.`
3546
3547
  };
3547
3548
  function editDistance(a, b) {
3548
3549
  const la = a.length;
@@ -3852,6 +3853,39 @@ function parse(input) {
3852
3853
  }
3853
3854
  }
3854
3855
  }
3856
+ const portUsageMap = /* @__PURE__ */ new Map();
3857
+ const allConns = [...forward, ...feedback];
3858
+ for (const conn of allConns) {
3859
+ const srcBlock = conn.source.blockId;
3860
+ if (!portUsageMap.has(srcBlock)) portUsageMap.set(srcBlock, /* @__PURE__ */ new Map());
3861
+ const srcPorts = portUsageMap.get(srcBlock);
3862
+ if (!srcPorts.has(conn.source.portId)) srcPorts.set(conn.source.portId, { asSource: [], asTarget: [] });
3863
+ srcPorts.get(conn.source.portId).asSource.push(0);
3864
+ const tgtBlock = conn.target.blockId;
3865
+ if (!portUsageMap.has(tgtBlock)) portUsageMap.set(tgtBlock, /* @__PURE__ */ new Map());
3866
+ const tgtPorts = portUsageMap.get(tgtBlock);
3867
+ if (!tgtPorts.has(conn.target.portId)) tgtPorts.set(conn.target.portId, { asSource: [], asTarget: [] });
3868
+ tgtPorts.get(conn.target.portId).asTarget.push(0);
3869
+ }
3870
+ for (const [blockId, portMap] of portUsageMap) {
3871
+ for (const [portId, usage] of portMap) {
3872
+ if (usage.asSource.length > 0 && usage.asTarget.length > 0) {
3873
+ const block = allBlocks.get(blockId);
3874
+ const blockLabel = block ? block.label : blockId;
3875
+ const srcConn = allConns.find((c) => c.source.blockId === blockId && c.source.portId === portId);
3876
+ const tgtConn = allConns.find((c) => c.target.blockId === blockId && c.target.portId === portId);
3877
+ const portDisplay = srcConn ? srcConn.source.portDisplay : tgtConn ? tgtConn.target.portDisplay : portId;
3878
+ warnings.push({
3879
+ code: "AMBIGUOUS_PORT_DIRECTION",
3880
+ message: errorMessages.ambiguousPortDirection(portDisplay, blockLabel),
3881
+ line: 0,
3882
+ column: 1,
3883
+ length: 1,
3884
+ severity: "warning"
3885
+ });
3886
+ }
3887
+ }
3888
+ }
3855
3889
  const graph = {
3856
3890
  declaredBlocks: declaredBlocksList,
3857
3891
  stubBlocks,
@@ -4068,23 +4102,33 @@ function collectPorts(blocks, connections) {
4068
4102
  }
4069
4103
  return result;
4070
4104
  }
4105
+ function startYForFace(blockY, blockHeight, portCount) {
4106
+ const topMargin = 40;
4107
+ const bottomMargin = 30;
4108
+ const topY = blockY + topMargin;
4109
+ const bottomY = blockY + blockHeight - bottomMargin;
4110
+ const availableHeight = bottomY - topY;
4111
+ const clusterHeight = Math.max(0, (portCount - 1) * 24);
4112
+ return topY + (availableHeight - clusterHeight) / 2;
4113
+ }
4071
4114
  function placePorts(block, ports) {
4072
4115
  const inPorts = ports.filter((p) => p.direction === "in");
4073
4116
  const outPorts = ports.filter((p) => p.direction === "out");
4074
4117
  const layoutPorts = [];
4075
- const startY = block.y + 40 + (block.subLabel ? 18 : 0);
4076
4118
  const spacing = 24;
4119
+ const inStartY = startYForFace(block.y, block.height, inPorts.length);
4077
4120
  inPorts.forEach((p, i) => {
4078
4121
  layoutPorts.push({
4079
4122
  ...p,
4080
- position: { x: block.x, y: startY + i * spacing },
4123
+ position: { x: block.x, y: inStartY + i * spacing },
4081
4124
  signalType: null
4082
4125
  });
4083
4126
  });
4127
+ const outStartY = startYForFace(block.y, block.height, outPorts.length);
4084
4128
  outPorts.forEach((p, i) => {
4085
4129
  layoutPorts.push({
4086
4130
  ...p,
4087
- position: { x: block.x + block.width, y: startY + i * spacing },
4131
+ position: { x: block.x + block.width, y: outStartY + i * spacing },
4088
4132
  signalType: null
4089
4133
  });
4090
4134
  });
@@ -4127,7 +4171,7 @@ function findPortPosition(block, portId, direction) {
4127
4171
  }
4128
4172
  function layout(graph, options = {}) {
4129
4173
  const direction = options.direction ?? "LR";
4130
- const rankSep = options.rankSep ?? 200;
4174
+ const rankSep = options.rankSep ?? 120;
4131
4175
  const nodeSep = options.nodeSep ?? 40;
4132
4176
  const allBlocks = [...graph.declaredBlocks, ...graph.stubBlocks];
4133
4177
  const allConnections = [...graph.connections, ...graph.feedbackEdges];
@@ -4372,7 +4416,28 @@ var SIGNAL_PILL_LABEL = {
4372
4416
  trigger: "trig",
4373
4417
  clock: "clk"
4374
4418
  };
4375
- function buildLabels(theme, blocks) {
4419
+ var UPWARD_CABLE_THRESHOLD = 20;
4420
+ function computeLabelBelowMap(connections) {
4421
+ const map = /* @__PURE__ */ new Map();
4422
+ for (const conn of connections) {
4423
+ const srcKey = `${conn.source.blockId}:${conn.source.portId}:out`;
4424
+ const srcDy = conn.targetPoint.y - conn.sourcePoint.y;
4425
+ if (srcDy < -UPWARD_CABLE_THRESHOLD) {
4426
+ map.set(srcKey, true);
4427
+ } else if (!map.has(srcKey)) {
4428
+ map.set(srcKey, false);
4429
+ }
4430
+ const tgtKey = `${conn.target.blockId}:${conn.target.portId}:in`;
4431
+ const tgtDy = conn.sourcePoint.y - conn.targetPoint.y;
4432
+ if (conn.isFeedback || tgtDy > UPWARD_CABLE_THRESHOLD) {
4433
+ map.set(tgtKey, true);
4434
+ } else if (!map.has(tgtKey)) {
4435
+ map.set(tgtKey, false);
4436
+ }
4437
+ }
4438
+ return map;
4439
+ }
4440
+ function buildLabels(theme, blocks, connections) {
4376
4441
  const parts = [];
4377
4442
  const fontFamily = sanitizeForSvg(theme.port.fontFamily);
4378
4443
  const pillShow = theme.port.pill.show;
@@ -4381,50 +4446,51 @@ function buildLabels(theme, blocks) {
4381
4446
  const pillRadius = theme.port.pill.cornerRadius;
4382
4447
  const pillPadX = 3;
4383
4448
  const pillHeight = 11;
4384
- const pillGap = 6;
4385
4449
  const charWidth = 6.5;
4450
+ const pillOffsetAbove = 20;
4451
+ const nameOffsetAbove = 32;
4452
+ const pillOffsetBelow = 20;
4453
+ const nameOffsetBelow = 32;
4454
+ const belowMap = computeLabelBelowMap(connections);
4455
+ const labelOffsetX = 6;
4386
4456
  for (const block of blocks) {
4387
4457
  for (const port of block.ports) {
4388
4458
  const { x, y } = port.position;
4389
- const isOut = port.direction === "out";
4390
- const labelX = isOut ? x + 14 : x - 14;
4391
- const labelY = y + 3;
4392
- const anchor = isOut ? "start" : "end";
4459
+ const key = `${block.id}:${port.id}:${port.direction}`;
4460
+ const below = belowMap.get(key) === true;
4393
4461
  const display = sanitizeForSvg(port.display);
4394
- const labelWidth = port.display.length * charWidth;
4462
+ const isOutput = port.direction === "out";
4463
+ const textY = below ? y + nameOffsetBelow : y - nameOffsetAbove;
4464
+ const textX = isOutput ? x + labelOffsetX : x - labelOffsetX;
4465
+ const textAnchor = isOutput ? "start" : "end";
4395
4466
  parts.push(
4396
- `<text x="${labelX}" y="${labelY}" font-family="${fontFamily}" font-size="${theme.port.fontSize}" fill="${theme.port.labelColor}" font-weight="600" text-anchor="${anchor}">${display}</text>`
4467
+ `<text x="${textX}" y="${textY}" font-family="${fontFamily}" font-size="${theme.port.fontSize}" fill="${theme.port.labelColor}" font-weight="600" text-anchor="${textAnchor}" dominant-baseline="central">${display}</text>`
4397
4468
  );
4398
4469
  if (pillShow && port.signalType) {
4399
4470
  const pillText = SIGNAL_PILL_LABEL[port.signalType];
4400
4471
  const pillWidth = pillText.length * charWidth + pillPadX * 2;
4401
4472
  const pillColor = theme.cable.colors[port.signalType].stroke;
4402
- let pillX;
4403
- if (isOut) {
4404
- pillX = labelX + labelWidth + pillGap;
4405
- } else {
4406
- pillX = labelX - labelWidth - pillGap - pillWidth;
4407
- }
4408
- const pillY = y - pillHeight / 2;
4409
- const textX = pillX + pillWidth / 2;
4410
- const textY = pillY + pillHeight / 2 + pillFontSize / 2 - 1;
4473
+ const pillCenterY = below ? y + pillOffsetBelow : y - pillOffsetAbove;
4474
+ const pillX = isOutput ? x + labelOffsetX : x - labelOffsetX - pillWidth;
4475
+ const pillY = pillCenterY - pillHeight / 2;
4476
+ const pillTextX = pillX + pillWidth / 2;
4411
4477
  parts.push(
4412
4478
  `<rect class="pf-port-pill" x="${pillX}" y="${pillY}" width="${pillWidth}" height="${pillHeight}" rx="${pillRadius}" fill="${pillColor}" data-signal="${port.signalType}"/>`
4413
4479
  );
4414
4480
  parts.push(
4415
- `<text class="pf-port-pill-text" x="${textX}" y="${textY}" text-anchor="middle" font-family="${fontFamily}" font-size="${pillFontSize}" fill="${pillTextColor}" font-weight="600">${sanitizeForSvg(pillText)}</text>`
4481
+ `<text class="pf-port-pill-text" x="${pillTextX}" y="${pillCenterY}" text-anchor="middle" dominant-baseline="central" font-family="${fontFamily}" font-size="${pillFontSize}" fill="${pillTextColor}" font-weight="600">${sanitizeForSvg(pillText)}</text>`
4416
4482
  );
4417
4483
  }
4418
4484
  }
4419
4485
  }
4420
4486
  return parts.join("");
4421
4487
  }
4422
- function buildAnnotations(theme, connections) {
4488
+ function buildAnnotations(theme, connections, layoutHeight) {
4423
4489
  const annotated = connections.filter((c) => c.annotation);
4424
4490
  if (annotated.length === 0) return "";
4425
4491
  const parts = [];
4426
4492
  const fontFamily = sanitizeForSvg(theme.annotation.fontFamily);
4427
- const noteFontSize = theme.annotation.fontSize + 1;
4493
+ const noteFontSize = theme.annotation.fontSize + 2;
4428
4494
  const markerFontFamily = fontFamily;
4429
4495
  annotated.forEach((conn, i) => {
4430
4496
  const num = i + 1;
@@ -4442,24 +4508,26 @@ function buildAnnotations(theme, connections) {
4442
4508
  mx = (sx + tx) / 2;
4443
4509
  my = (sy + ty) / 2;
4444
4510
  }
4445
- const markerStroke = conn.isFeedback ? theme.cable.colors[conn.signalType].stroke : theme.label.color;
4511
+ const markerStroke = conn.isFeedback ? theme.cable.colors[conn.signalType].stroke : theme.annotation.color;
4446
4512
  parts.push(
4447
4513
  `<circle cx="${mx}" cy="${my}" r="8" fill="${theme.panel.highlight}" stroke="${markerStroke}" stroke-width="0.5" data-annotation-marker="${num}"/>`
4448
4514
  );
4449
4515
  parts.push(
4450
- `<text x="${mx}" y="${my + 3}" text-anchor="middle" font-family="${markerFontFamily}" font-size="9" fill="${theme.label.color}">${num}</text>`
4516
+ `<text x="${mx}" y="${my + 3}" text-anchor="middle" font-family="${markerFontFamily}" font-size="9" fill="${theme.annotation.color}">${num}</text>`
4451
4517
  );
4452
4518
  });
4453
4519
  const panelX = -120;
4454
- let panelY = 20;
4455
- const lineGap = 14;
4520
+ const lineGap = 16;
4521
+ const noteCount = annotated.length;
4522
+ const bottomY = layoutHeight - 10;
4523
+ const firstNoteY = bottomY - (noteCount - 1) * lineGap;
4456
4524
  annotated.forEach((conn, i) => {
4457
4525
  const num = i + 1;
4458
4526
  const noteText = `${num}. ${sanitizeForSvg(conn.annotation)}`;
4527
+ const noteY = firstNoteY + i * lineGap;
4459
4528
  parts.push(
4460
- `<text x="${panelX}" y="${panelY}" font-family="${fontFamily}" font-size="${noteFontSize}" fill="${theme.annotation.color}">${noteText}</text>`
4529
+ `<text x="${panelX}" y="${noteY}" font-family="${fontFamily}" font-size="${noteFontSize}" font-weight="600" fill="${theme.annotation.color}">${noteText}</text>`
4461
4530
  );
4462
- panelY += lineGap;
4463
4531
  });
4464
4532
  return parts.join("");
4465
4533
  }
@@ -4470,16 +4538,18 @@ function buildLegend(theme, layoutResult) {
4470
4538
  const parts = [];
4471
4539
  const fontFamily = sanitizeForSvg(theme.annotation.fontFamily);
4472
4540
  const itemWidth = 70;
4541
+ const totalWidth = used.length * itemWidth;
4542
+ const legendStartX = layoutResult.width - totalWidth;
4473
4543
  const y = layoutResult.height - 20;
4474
- let x = 20;
4475
- for (const sig of used) {
4544
+ for (let i = 0; i < used.length; i++) {
4545
+ const sig = used[i];
4476
4546
  const color = theme.cable.colors[sig].stroke;
4547
+ const x = legendStartX + i * itemWidth;
4477
4548
  let g = `<g transform="translate(${x}, ${y})">`;
4478
4549
  g += `<line x1="0" y1="0" x2="20" y2="0" stroke="${color}" stroke-width="3" stroke-linecap="round"/>`;
4479
4550
  g += `<text x="26" y="3" font-family="${fontFamily}" font-size="9" fill="${theme.annotation.color}">${sig}</text>`;
4480
4551
  g += `</g>`;
4481
4552
  parts.push(g);
4482
- x += itemWidth;
4483
4553
  }
4484
4554
  return parts.join("");
4485
4555
  }
@@ -4508,14 +4578,19 @@ function renderSvg(layoutResult, theme) {
4508
4578
  `<g class="pf-layer-panels" >${buildPanels(theme, idPrefix, layoutResult.blocks)}</g>`,
4509
4579
  `<g class="pf-layer-params">${buildParams(layoutResult.blocks, theme)}</g>`,
4510
4580
  `<g class="pf-layer-jacks">${buildJacks(theme, idPrefix, layoutResult.blocks)}</g>`,
4511
- `<g class="pf-layer-labels">${buildLabels(theme, layoutResult.blocks)}</g>`,
4512
- `<g class="pf-layer-annotations">${buildAnnotations(theme, layoutResult.connections)}</g>`,
4581
+ `<g class="pf-layer-labels">${buildLabels(theme, layoutResult.blocks, layoutResult.connections)}</g>`,
4582
+ `<g class="pf-layer-annotations">${buildAnnotations(theme, layoutResult.connections, height)}</g>`,
4513
4583
  `<g class="pf-layer-legend">${buildLegend(theme, layoutResult)}</g>`
4514
4584
  ].join("");
4515
4585
  const style = `<style>@media print { .pf-panel, .pf-jack { filter: none; } }</style>`;
4516
4586
  const labelPadX = 130;
4517
4587
  const vbWidth = width + labelPadX * 2;
4518
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${-labelPadX} 0 ${vbWidth} ${height}" width="100%" data-pf-min-width="${minWidth + labelPadX * 2}" role="img" aria-labelledby="${idPrefix}-title ${idPrefix}-desc"><title id="${idPrefix}-title">Patch diagram</title><desc id="${idPrefix}-desc">${desc}</desc>` + style + `<defs>${defsParts.join("")}</defs>` + layers + `</svg>`;
4588
+ const topPad = 40;
4589
+ const noteCount = layoutResult.connections.filter((c) => c.annotation).length;
4590
+ const notesHeight = noteCount > 0 ? noteCount * 16 + 10 : 0;
4591
+ const bottomPad = Math.max(40, notesHeight + 10);
4592
+ const vbHeight = height + topPad + bottomPad;
4593
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${-labelPadX} ${-topPad} ${vbWidth} ${vbHeight}" width="100%" data-pf-min-width="${minWidth + labelPadX * 2}" role="img" aria-labelledby="${idPrefix}-title ${idPrefix}-desc"><title id="${idPrefix}-title">Patch diagram</title><desc id="${idPrefix}-desc">${desc}</desc>` + style + `<defs>${defsParts.join("")}</defs>` + layers + `</svg>`;
4519
4594
  return svg;
4520
4595
  }
4521
4596
 
@@ -4578,7 +4653,7 @@ var defaultTheme = {
4578
4653
  annotation: {
4579
4654
  fontFamily: "'SF Mono', 'Fira Code', Consolas, 'Courier New', monospace",
4580
4655
  fontSize: 9,
4581
- color: "#888888",
4656
+ color: "#4a4a4a",
4582
4657
  haloColor: "#f7f5f0"
4583
4658
  },
4584
4659
  grid: {