@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.
@@ -39,4 +39,4 @@ export {
39
39
  __toESM,
40
40
  SIGNAL_OPERATORS
41
41
  };
42
- //# sourceMappingURL=chunk-B7DUSPQH.js.map
42
+ //# sourceMappingURL=chunk-4VUBNFI4.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["// ── Signal Types ──\n\nexport type SignalType = 'audio' | 'cv' | 'pitch' | 'gate' | 'trigger' | 'clock';\n\nexport const SIGNAL_OPERATORS: Record<string, SignalType> = {\n '->': 'audio',\n '>>': 'cv',\n 'p>': 'pitch',\n 'g>': 'gate',\n 't>': 'trigger',\n 'c>': 'clock',\n};\n\n// ── Graph Primitives ──\n\nexport interface Port {\n id: string; // normalized key (lowercase, trimmed)\n display: string; // original form for rendering\n direction: 'in' | 'out';\n}\n\nexport interface Param {\n key: string;\n value: string;\n}\n\nexport interface Block {\n id: string;\n label: string;\n subLabel: string | null;\n params: Param[];\n ports: Port[];\n parentModule: string | null; // null for top-level modules, module name for sections\n voice: string | null;\n}\n\nexport interface ConnectionEndpoint {\n blockId: string;\n portId: string; // normalized key\n portDisplay: string; // original form\n}\n\nexport interface Connection {\n id: string;\n source: ConnectionEndpoint;\n target: ConnectionEndpoint;\n signalType: SignalType;\n annotation: string | null;\n graphvizExtras: Record<string, string> | null;\n}\n\n// ── Patch Graph (Parser Output) ──\n\nexport interface PatchGraph {\n declaredBlocks: Block[];\n stubBlocks: Block[];\n connections: Connection[];\n feedbackEdges: Connection[];\n signalTypeStats: Partial<Record<SignalType, number>>;\n voices: string[];\n}\n\n// ── Layout Types ──\n\nexport interface Point {\n x: number;\n y: number;\n}\n\nexport interface LayoutPort extends Port {\n position: Point;\n signalType: SignalType | null;\n}\n\nexport interface LayoutBlock {\n id: string;\n label: string;\n subLabel: string | null;\n params: Param[];\n ports: LayoutPort[];\n parentModule: string | null;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface LayoutConnection {\n id: string;\n source: ConnectionEndpoint;\n target: ConnectionEndpoint;\n signalType: SignalType;\n annotation: string | null;\n path: string; // SVG path d attribute\n isFeedback: boolean;\n sourcePoint: Point;\n targetPoint: Point;\n}\n\nexport interface LayoutResult {\n blocks: LayoutBlock[];\n connections: LayoutConnection[];\n width: number;\n height: number;\n signalTypeStats: Partial<Record<SignalType, number>>;\n}\n\n// ── Theme Types ──\n\nexport interface CableColor {\n stroke: string;\n plugTip: string;\n}\n\nexport interface SocketColors {\n bezel: string;\n bezelStroke: string;\n ring: string;\n hole: string;\n pin: string;\n}\n\nexport interface Theme {\n background: string;\n panel: {\n fill: string;\n stroke: string;\n highlight: string;\n shadow: string;\n cornerRadius: number;\n shadowBlur: number;\n shadowOpacity: number;\n bevelWidth: number;\n };\n label: {\n fontFamily: string;\n color: string;\n subColor: string;\n plateFill: string;\n plateStroke: string;\n };\n param: {\n plateFill: string;\n plateStroke: string;\n textColor: string;\n };\n port: {\n fontFamily: string;\n fontSize: number;\n colors: SocketColors;\n hideSocket: boolean;\n labelColor: string;\n pill: {\n show: boolean;\n fontSize: number;\n textColor: string;\n cornerRadius: number;\n };\n };\n cable: {\n width: number;\n colors: Record<SignalType, CableColor>;\n plugTipRadius: number;\n };\n annotation: {\n fontFamily: string;\n fontSize: number;\n color: string;\n haloColor: string;\n };\n grid: {\n dotColor: string;\n dotRadius: number;\n spacing: number;\n opacity: number;\n } | null;\n}\n\n// ── Options ──\n\nexport interface RenderOptions {\n theme?: DeepPartial<Theme>;\n maxWidth?: number;\n padding?: number;\n legend?: boolean | 'auto';\n}\n\nexport interface LayoutOptions {\n direction?: 'LR' | 'TB';\n nodeSep?: number;\n rankSep?: number;\n feedbackSide?: 'top' | 'bottom';\n}\n\n// ── Parse Result ──\n\nexport type ErrorCode =\n | 'SYNTAX_ERROR'\n | 'UNKNOWN_OPERATOR'\n | 'MISSING_PORT'\n | 'UNCLOSED_PAREN'\n | 'DUPLICATE_MODULE'\n | 'UNKNOWN_MODULE'\n | 'INVALID_PORT';\n\nexport type ErrorSeverity = 'error' | 'warning';\n\nexport interface ParseDiagnostic {\n code: ErrorCode;\n message: string;\n line: number;\n column: number;\n length: number;\n severity: ErrorSeverity;\n}\n\nexport interface ParseResult {\n graph: PatchGraph;\n errors: ParseDiagnostic[];\n warnings: ParseDiagnostic[];\n}\n\n// ── Utility Types ──\n\nexport type DeepPartial<T> = {\n [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAIO,IAAM,mBAA+C;AAAA,EAC1D,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;","names":[]}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["// ── Signal Types ──\n\nexport type SignalType = 'audio' | 'cv' | 'pitch' | 'gate' | 'trigger' | 'clock';\n\nexport const SIGNAL_OPERATORS: Record<string, SignalType> = {\n '->': 'audio',\n '>>': 'cv',\n 'p>': 'pitch',\n 'g>': 'gate',\n 't>': 'trigger',\n 'c>': 'clock',\n};\n\n// ── Graph Primitives ──\n\nexport interface Port {\n id: string; // normalized key (lowercase, trimmed)\n display: string; // original form for rendering\n direction: 'in' | 'out';\n}\n\nexport interface Param {\n key: string;\n value: string;\n}\n\nexport interface Block {\n id: string;\n label: string;\n subLabel: string | null;\n params: Param[];\n ports: Port[];\n parentModule: string | null; // null for top-level modules, module name for sections\n voice: string | null;\n}\n\nexport interface ConnectionEndpoint {\n blockId: string;\n portId: string; // normalized key\n portDisplay: string; // original form\n}\n\nexport interface Connection {\n id: string;\n source: ConnectionEndpoint;\n target: ConnectionEndpoint;\n signalType: SignalType;\n annotation: string | null;\n graphvizExtras: Record<string, string> | null;\n}\n\n// ── Patch Graph (Parser Output) ──\n\nexport interface PatchGraph {\n declaredBlocks: Block[];\n stubBlocks: Block[];\n connections: Connection[];\n feedbackEdges: Connection[];\n signalTypeStats: Partial<Record<SignalType, number>>;\n voices: string[];\n}\n\n// ── Layout Types ──\n\nexport interface Point {\n x: number;\n y: number;\n}\n\nexport interface LayoutPort extends Port {\n position: Point;\n signalType: SignalType | null;\n}\n\nexport interface LayoutBlock {\n id: string;\n label: string;\n subLabel: string | null;\n params: Param[];\n ports: LayoutPort[];\n parentModule: string | null;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface LayoutConnection {\n id: string;\n source: ConnectionEndpoint;\n target: ConnectionEndpoint;\n signalType: SignalType;\n annotation: string | null;\n path: string; // SVG path d attribute\n isFeedback: boolean;\n sourcePoint: Point;\n targetPoint: Point;\n}\n\nexport interface LayoutResult {\n blocks: LayoutBlock[];\n connections: LayoutConnection[];\n width: number;\n height: number;\n signalTypeStats: Partial<Record<SignalType, number>>;\n}\n\n// ── Theme Types ──\n\nexport interface CableColor {\n stroke: string;\n plugTip: string;\n}\n\nexport interface SocketColors {\n bezel: string;\n bezelStroke: string;\n ring: string;\n hole: string;\n pin: string;\n}\n\nexport interface Theme {\n background: string;\n panel: {\n fill: string;\n stroke: string;\n highlight: string;\n shadow: string;\n cornerRadius: number;\n shadowBlur: number;\n shadowOpacity: number;\n bevelWidth: number;\n };\n label: {\n fontFamily: string;\n color: string;\n subColor: string;\n plateFill: string;\n plateStroke: string;\n };\n param: {\n plateFill: string;\n plateStroke: string;\n textColor: string;\n };\n port: {\n fontFamily: string;\n fontSize: number;\n colors: SocketColors;\n hideSocket: boolean;\n labelColor: string;\n pill: {\n show: boolean;\n fontSize: number;\n textColor: string;\n cornerRadius: number;\n };\n };\n cable: {\n width: number;\n colors: Record<SignalType, CableColor>;\n plugTipRadius: number;\n };\n annotation: {\n fontFamily: string;\n fontSize: number;\n color: string;\n haloColor: string;\n };\n grid: {\n dotColor: string;\n dotRadius: number;\n spacing: number;\n opacity: number;\n } | null;\n}\n\n// ── Options ──\n\nexport interface RenderOptions {\n theme?: DeepPartial<Theme>;\n maxWidth?: number;\n padding?: number;\n legend?: boolean | 'auto';\n}\n\nexport interface LayoutOptions {\n direction?: 'LR' | 'TB';\n nodeSep?: number;\n rankSep?: number;\n feedbackSide?: 'top' | 'bottom';\n}\n\n// ── Parse Result ──\n\nexport type ErrorCode =\n | 'SYNTAX_ERROR'\n | 'UNKNOWN_OPERATOR'\n | 'MISSING_PORT'\n | 'UNCLOSED_PAREN'\n | 'DUPLICATE_MODULE'\n | 'UNKNOWN_MODULE'\n | 'INVALID_PORT'\n | 'AMBIGUOUS_PORT_DIRECTION';\n\nexport type ErrorSeverity = 'error' | 'warning';\n\nexport interface ParseDiagnostic {\n code: ErrorCode;\n message: string;\n line: number;\n column: number;\n length: number;\n severity: ErrorSeverity;\n}\n\nexport interface ParseResult {\n graph: PatchGraph;\n errors: ParseDiagnostic[];\n warnings: ParseDiagnostic[];\n}\n\n// ── Utility Types ──\n\nexport type DeepPartial<T> = {\n [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAIO,IAAM,mBAA+C;AAAA,EAC1D,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;","names":[]}
package/dist/index.cjs CHANGED
@@ -3591,7 +3591,8 @@ var errorMessages = {
3591
3591
  duplicateModule: (name) => `Module "${name}" declared more than once.`,
3592
3592
  didYouMean: (got, suggestion) => `Module "${got}" is not declared. Did you mean "${suggestion}"?`,
3593
3593
  syntaxError: (detail) => `Syntax error: ${detail}`,
3594
- emptyDiagram: () => `Diagram is empty \u2014 no connections found.`
3594
+ emptyDiagram: () => `Diagram is empty \u2014 no connections found.`,
3595
+ ambiguousPortDirection: (port, module2) => `Port "${port}" on module "${module2}" is used as both input and output. Use distinct names like "In ${port}"/"Out ${port}" to disambiguate.`
3595
3596
  };
3596
3597
  function editDistance(a, b) {
3597
3598
  const la = a.length;
@@ -3901,6 +3902,39 @@ function parse(input) {
3901
3902
  }
3902
3903
  }
3903
3904
  }
3905
+ const portUsageMap = /* @__PURE__ */ new Map();
3906
+ const allConns = [...forward, ...feedback];
3907
+ for (const conn of allConns) {
3908
+ const srcBlock = conn.source.blockId;
3909
+ if (!portUsageMap.has(srcBlock)) portUsageMap.set(srcBlock, /* @__PURE__ */ new Map());
3910
+ const srcPorts = portUsageMap.get(srcBlock);
3911
+ if (!srcPorts.has(conn.source.portId)) srcPorts.set(conn.source.portId, { asSource: [], asTarget: [] });
3912
+ srcPorts.get(conn.source.portId).asSource.push(0);
3913
+ const tgtBlock = conn.target.blockId;
3914
+ if (!portUsageMap.has(tgtBlock)) portUsageMap.set(tgtBlock, /* @__PURE__ */ new Map());
3915
+ const tgtPorts = portUsageMap.get(tgtBlock);
3916
+ if (!tgtPorts.has(conn.target.portId)) tgtPorts.set(conn.target.portId, { asSource: [], asTarget: [] });
3917
+ tgtPorts.get(conn.target.portId).asTarget.push(0);
3918
+ }
3919
+ for (const [blockId, portMap] of portUsageMap) {
3920
+ for (const [portId, usage] of portMap) {
3921
+ if (usage.asSource.length > 0 && usage.asTarget.length > 0) {
3922
+ const block = allBlocks.get(blockId);
3923
+ const blockLabel = block ? block.label : blockId;
3924
+ const srcConn = allConns.find((c) => c.source.blockId === blockId && c.source.portId === portId);
3925
+ const tgtConn = allConns.find((c) => c.target.blockId === blockId && c.target.portId === portId);
3926
+ const portDisplay = srcConn ? srcConn.source.portDisplay : tgtConn ? tgtConn.target.portDisplay : portId;
3927
+ warnings.push({
3928
+ code: "AMBIGUOUS_PORT_DIRECTION",
3929
+ message: errorMessages.ambiguousPortDirection(portDisplay, blockLabel),
3930
+ line: 0,
3931
+ column: 1,
3932
+ length: 1,
3933
+ severity: "warning"
3934
+ });
3935
+ }
3936
+ }
3937
+ }
3904
3938
  const graph = {
3905
3939
  declaredBlocks: declaredBlocksList,
3906
3940
  stubBlocks,
@@ -4117,23 +4151,33 @@ function collectPorts(blocks, connections) {
4117
4151
  }
4118
4152
  return result;
4119
4153
  }
4154
+ function startYForFace(blockY, blockHeight, portCount) {
4155
+ const topMargin = 40;
4156
+ const bottomMargin = 30;
4157
+ const topY = blockY + topMargin;
4158
+ const bottomY = blockY + blockHeight - bottomMargin;
4159
+ const availableHeight = bottomY - topY;
4160
+ const clusterHeight = Math.max(0, (portCount - 1) * 24);
4161
+ return topY + (availableHeight - clusterHeight) / 2;
4162
+ }
4120
4163
  function placePorts(block, ports) {
4121
4164
  const inPorts = ports.filter((p) => p.direction === "in");
4122
4165
  const outPorts = ports.filter((p) => p.direction === "out");
4123
4166
  const layoutPorts = [];
4124
- const startY = block.y + 40 + (block.subLabel ? 18 : 0);
4125
4167
  const spacing = 24;
4168
+ const inStartY = startYForFace(block.y, block.height, inPorts.length);
4126
4169
  inPorts.forEach((p, i) => {
4127
4170
  layoutPorts.push({
4128
4171
  ...p,
4129
- position: { x: block.x, y: startY + i * spacing },
4172
+ position: { x: block.x, y: inStartY + i * spacing },
4130
4173
  signalType: null
4131
4174
  });
4132
4175
  });
4176
+ const outStartY = startYForFace(block.y, block.height, outPorts.length);
4133
4177
  outPorts.forEach((p, i) => {
4134
4178
  layoutPorts.push({
4135
4179
  ...p,
4136
- position: { x: block.x + block.width, y: startY + i * spacing },
4180
+ position: { x: block.x + block.width, y: outStartY + i * spacing },
4137
4181
  signalType: null
4138
4182
  });
4139
4183
  });
@@ -4176,7 +4220,7 @@ function findPortPosition(block, portId, direction) {
4176
4220
  }
4177
4221
  function layout(graph, options = {}) {
4178
4222
  const direction = options.direction ?? "LR";
4179
- const rankSep = options.rankSep ?? 200;
4223
+ const rankSep = options.rankSep ?? 120;
4180
4224
  const nodeSep = options.nodeSep ?? 40;
4181
4225
  const allBlocks = [...graph.declaredBlocks, ...graph.stubBlocks];
4182
4226
  const allConnections = [...graph.connections, ...graph.feedbackEdges];
@@ -4421,7 +4465,28 @@ var SIGNAL_PILL_LABEL = {
4421
4465
  trigger: "trig",
4422
4466
  clock: "clk"
4423
4467
  };
4424
- function buildLabels(theme, blocks) {
4468
+ var UPWARD_CABLE_THRESHOLD = 20;
4469
+ function computeLabelBelowMap(connections) {
4470
+ const map = /* @__PURE__ */ new Map();
4471
+ for (const conn of connections) {
4472
+ const srcKey = `${conn.source.blockId}:${conn.source.portId}:out`;
4473
+ const srcDy = conn.targetPoint.y - conn.sourcePoint.y;
4474
+ if (srcDy < -UPWARD_CABLE_THRESHOLD) {
4475
+ map.set(srcKey, true);
4476
+ } else if (!map.has(srcKey)) {
4477
+ map.set(srcKey, false);
4478
+ }
4479
+ const tgtKey = `${conn.target.blockId}:${conn.target.portId}:in`;
4480
+ const tgtDy = conn.sourcePoint.y - conn.targetPoint.y;
4481
+ if (conn.isFeedback || tgtDy > UPWARD_CABLE_THRESHOLD) {
4482
+ map.set(tgtKey, true);
4483
+ } else if (!map.has(tgtKey)) {
4484
+ map.set(tgtKey, false);
4485
+ }
4486
+ }
4487
+ return map;
4488
+ }
4489
+ function buildLabels(theme, blocks, connections) {
4425
4490
  const parts = [];
4426
4491
  const fontFamily = sanitizeForSvg(theme.port.fontFamily);
4427
4492
  const pillShow = theme.port.pill.show;
@@ -4430,50 +4495,51 @@ function buildLabels(theme, blocks) {
4430
4495
  const pillRadius = theme.port.pill.cornerRadius;
4431
4496
  const pillPadX = 3;
4432
4497
  const pillHeight = 11;
4433
- const pillGap = 6;
4434
4498
  const charWidth = 6.5;
4499
+ const pillOffsetAbove = 20;
4500
+ const nameOffsetAbove = 32;
4501
+ const pillOffsetBelow = 20;
4502
+ const nameOffsetBelow = 32;
4503
+ const belowMap = computeLabelBelowMap(connections);
4504
+ const labelOffsetX = 6;
4435
4505
  for (const block of blocks) {
4436
4506
  for (const port of block.ports) {
4437
4507
  const { x, y } = port.position;
4438
- const isOut = port.direction === "out";
4439
- const labelX = isOut ? x + 14 : x - 14;
4440
- const labelY = y + 3;
4441
- const anchor = isOut ? "start" : "end";
4508
+ const key = `${block.id}:${port.id}:${port.direction}`;
4509
+ const below = belowMap.get(key) === true;
4442
4510
  const display = sanitizeForSvg(port.display);
4443
- const labelWidth = port.display.length * charWidth;
4511
+ const isOutput = port.direction === "out";
4512
+ const textY = below ? y + nameOffsetBelow : y - nameOffsetAbove;
4513
+ const textX = isOutput ? x + labelOffsetX : x - labelOffsetX;
4514
+ const textAnchor = isOutput ? "start" : "end";
4444
4515
  parts.push(
4445
- `<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>`
4516
+ `<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>`
4446
4517
  );
4447
4518
  if (pillShow && port.signalType) {
4448
4519
  const pillText = SIGNAL_PILL_LABEL[port.signalType];
4449
4520
  const pillWidth = pillText.length * charWidth + pillPadX * 2;
4450
4521
  const pillColor = theme.cable.colors[port.signalType].stroke;
4451
- let pillX;
4452
- if (isOut) {
4453
- pillX = labelX + labelWidth + pillGap;
4454
- } else {
4455
- pillX = labelX - labelWidth - pillGap - pillWidth;
4456
- }
4457
- const pillY = y - pillHeight / 2;
4458
- const textX = pillX + pillWidth / 2;
4459
- const textY = pillY + pillHeight / 2 + pillFontSize / 2 - 1;
4522
+ const pillCenterY = below ? y + pillOffsetBelow : y - pillOffsetAbove;
4523
+ const pillX = isOutput ? x + labelOffsetX : x - labelOffsetX - pillWidth;
4524
+ const pillY = pillCenterY - pillHeight / 2;
4525
+ const pillTextX = pillX + pillWidth / 2;
4460
4526
  parts.push(
4461
4527
  `<rect class="pf-port-pill" x="${pillX}" y="${pillY}" width="${pillWidth}" height="${pillHeight}" rx="${pillRadius}" fill="${pillColor}" data-signal="${port.signalType}"/>`
4462
4528
  );
4463
4529
  parts.push(
4464
- `<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>`
4530
+ `<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>`
4465
4531
  );
4466
4532
  }
4467
4533
  }
4468
4534
  }
4469
4535
  return parts.join("");
4470
4536
  }
4471
- function buildAnnotations(theme, connections) {
4537
+ function buildAnnotations(theme, connections, layoutHeight) {
4472
4538
  const annotated = connections.filter((c) => c.annotation);
4473
4539
  if (annotated.length === 0) return "";
4474
4540
  const parts = [];
4475
4541
  const fontFamily = sanitizeForSvg(theme.annotation.fontFamily);
4476
- const noteFontSize = theme.annotation.fontSize + 1;
4542
+ const noteFontSize = theme.annotation.fontSize + 2;
4477
4543
  const markerFontFamily = fontFamily;
4478
4544
  annotated.forEach((conn, i) => {
4479
4545
  const num = i + 1;
@@ -4491,24 +4557,26 @@ function buildAnnotations(theme, connections) {
4491
4557
  mx = (sx + tx) / 2;
4492
4558
  my = (sy + ty) / 2;
4493
4559
  }
4494
- const markerStroke = conn.isFeedback ? theme.cable.colors[conn.signalType].stroke : theme.label.color;
4560
+ const markerStroke = conn.isFeedback ? theme.cable.colors[conn.signalType].stroke : theme.annotation.color;
4495
4561
  parts.push(
4496
4562
  `<circle cx="${mx}" cy="${my}" r="8" fill="${theme.panel.highlight}" stroke="${markerStroke}" stroke-width="0.5" data-annotation-marker="${num}"/>`
4497
4563
  );
4498
4564
  parts.push(
4499
- `<text x="${mx}" y="${my + 3}" text-anchor="middle" font-family="${markerFontFamily}" font-size="9" fill="${theme.label.color}">${num}</text>`
4565
+ `<text x="${mx}" y="${my + 3}" text-anchor="middle" font-family="${markerFontFamily}" font-size="9" fill="${theme.annotation.color}">${num}</text>`
4500
4566
  );
4501
4567
  });
4502
4568
  const panelX = -120;
4503
- let panelY = 20;
4504
- const lineGap = 14;
4569
+ const lineGap = 16;
4570
+ const noteCount = annotated.length;
4571
+ const bottomY = layoutHeight - 10;
4572
+ const firstNoteY = bottomY - (noteCount - 1) * lineGap;
4505
4573
  annotated.forEach((conn, i) => {
4506
4574
  const num = i + 1;
4507
4575
  const noteText = `${num}. ${sanitizeForSvg(conn.annotation)}`;
4576
+ const noteY = firstNoteY + i * lineGap;
4508
4577
  parts.push(
4509
- `<text x="${panelX}" y="${panelY}" font-family="${fontFamily}" font-size="${noteFontSize}" fill="${theme.annotation.color}">${noteText}</text>`
4578
+ `<text x="${panelX}" y="${noteY}" font-family="${fontFamily}" font-size="${noteFontSize}" font-weight="600" fill="${theme.annotation.color}">${noteText}</text>`
4510
4579
  );
4511
- panelY += lineGap;
4512
4580
  });
4513
4581
  return parts.join("");
4514
4582
  }
@@ -4519,16 +4587,18 @@ function buildLegend(theme, layoutResult) {
4519
4587
  const parts = [];
4520
4588
  const fontFamily = sanitizeForSvg(theme.annotation.fontFamily);
4521
4589
  const itemWidth = 70;
4590
+ const totalWidth = used.length * itemWidth;
4591
+ const legendStartX = layoutResult.width - totalWidth;
4522
4592
  const y = layoutResult.height - 20;
4523
- let x = 20;
4524
- for (const sig of used) {
4593
+ for (let i = 0; i < used.length; i++) {
4594
+ const sig = used[i];
4525
4595
  const color = theme.cable.colors[sig].stroke;
4596
+ const x = legendStartX + i * itemWidth;
4526
4597
  let g = `<g transform="translate(${x}, ${y})">`;
4527
4598
  g += `<line x1="0" y1="0" x2="20" y2="0" stroke="${color}" stroke-width="3" stroke-linecap="round"/>`;
4528
4599
  g += `<text x="26" y="3" font-family="${fontFamily}" font-size="9" fill="${theme.annotation.color}">${sig}</text>`;
4529
4600
  g += `</g>`;
4530
4601
  parts.push(g);
4531
- x += itemWidth;
4532
4602
  }
4533
4603
  return parts.join("");
4534
4604
  }
@@ -4557,14 +4627,19 @@ function renderSvg(layoutResult, theme) {
4557
4627
  `<g class="pf-layer-panels" >${buildPanels(theme, idPrefix, layoutResult.blocks)}</g>`,
4558
4628
  `<g class="pf-layer-params">${buildParams(layoutResult.blocks, theme)}</g>`,
4559
4629
  `<g class="pf-layer-jacks">${buildJacks(theme, idPrefix, layoutResult.blocks)}</g>`,
4560
- `<g class="pf-layer-labels">${buildLabels(theme, layoutResult.blocks)}</g>`,
4561
- `<g class="pf-layer-annotations">${buildAnnotations(theme, layoutResult.connections)}</g>`,
4630
+ `<g class="pf-layer-labels">${buildLabels(theme, layoutResult.blocks, layoutResult.connections)}</g>`,
4631
+ `<g class="pf-layer-annotations">${buildAnnotations(theme, layoutResult.connections, height)}</g>`,
4562
4632
  `<g class="pf-layer-legend">${buildLegend(theme, layoutResult)}</g>`
4563
4633
  ].join("");
4564
4634
  const style = `<style>@media print { .pf-panel, .pf-jack { filter: none; } }</style>`;
4565
4635
  const labelPadX = 130;
4566
4636
  const vbWidth = width + labelPadX * 2;
4567
- 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>`;
4637
+ const topPad = 40;
4638
+ const noteCount = layoutResult.connections.filter((c) => c.annotation).length;
4639
+ const notesHeight = noteCount > 0 ? noteCount * 16 + 10 : 0;
4640
+ const bottomPad = Math.max(40, notesHeight + 10);
4641
+ const vbHeight = height + topPad + bottomPad;
4642
+ 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>`;
4568
4643
  return svg;
4569
4644
  }
4570
4645
 
@@ -4627,7 +4702,7 @@ var defaultTheme = {
4627
4702
  annotation: {
4628
4703
  fontFamily: "'SF Mono', 'Fira Code', Consolas, 'Courier New', monospace",
4629
4704
  fontSize: 9,
4630
- color: "#888888",
4705
+ color: "#4a4a4a",
4631
4706
  haloColor: "#f7f5f0"
4632
4707
  },
4633
4708
  grid: {