@ogabrielluiz/patchflow 0.1.3 → 0.1.4

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-4VUBNFI4.js.map
42
+ //# sourceMappingURL=chunk-YONEHQMI.js.map
@@ -0,0 +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 * Non-fatal diagnostics produced during layout. Currently used to surface\n * internal consistency issues (e.g. computed height doesn't cover the\n * actual block content) so future regressions are visible.\n */\n warnings?: string[];\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
@@ -4333,14 +4333,33 @@ function layout(graph, options = {}) {
4333
4333
  const feedbackSpace = hasFeedback ? diagramBottom - maxY + feedbackArcOffset + 30 + 16 : 0;
4334
4334
  const width = maxX + margin;
4335
4335
  const height = maxY + margin + feedbackSpace;
4336
+ const warnings = checkHeightInvariant({
4337
+ blocks: layoutBlocks,
4338
+ height,
4339
+ hasFeedback,
4340
+ feedbackBottom: diagramBottom + feedbackArcOffset
4341
+ });
4336
4342
  return {
4337
4343
  blocks: layoutBlocks,
4338
4344
  connections: layoutConnections,
4339
4345
  width,
4340
4346
  height,
4341
- signalTypeStats: graph.signalTypeStats
4347
+ signalTypeStats: graph.signalTypeStats,
4348
+ warnings
4342
4349
  };
4343
4350
  }
4351
+ function checkHeightInvariant(args) {
4352
+ const { blocks, height, hasFeedback, feedbackBottom } = args;
4353
+ const warnings = [];
4354
+ const blockBottomMax = blocks.length > 0 ? Math.max(...blocks.map((b) => b.y + b.height)) : 0;
4355
+ const contentBottom = hasFeedback ? Math.max(blockBottomMax, feedbackBottom) : blockBottomMax;
4356
+ if (height < contentBottom) {
4357
+ warnings.push(
4358
+ `layout: computed height (${height.toFixed(1)}) is below content bottom (${contentBottom.toFixed(1)}); legend/notes may overlap block content`
4359
+ );
4360
+ }
4361
+ return warnings;
4362
+ }
4344
4363
 
4345
4364
  // src/renderer.ts
4346
4365
  function genId() {
@@ -4580,7 +4599,7 @@ function buildAnnotations(theme, connections, layoutHeight) {
4580
4599
  });
4581
4600
  return parts.join("");
4582
4601
  }
4583
- function buildLegend(theme, layoutResult) {
4602
+ function buildLegend(theme, layoutResult, diagramBottom) {
4584
4603
  const order = ["audio", "cv", "pitch", "gate", "trigger", "clock"];
4585
4604
  const used = order.filter((t) => (layoutResult.signalTypeStats[t] ?? 0) > 0);
4586
4605
  if (used.length === 0) return "";
@@ -4589,7 +4608,7 @@ function buildLegend(theme, layoutResult) {
4589
4608
  const itemWidth = 70;
4590
4609
  const totalWidth = used.length * itemWidth;
4591
4610
  const legendStartX = layoutResult.width - totalWidth;
4592
- const y = layoutResult.height - 20;
4611
+ const y = diagramBottom - 20;
4593
4612
  for (let i = 0; i < used.length; i++) {
4594
4613
  const sig = used[i];
4595
4614
  const color = theme.cable.colors[sig].stroke;
@@ -4621,6 +4640,14 @@ function renderSvg(layoutResult, theme) {
4621
4640
  `<pattern id="${idPrefix}-dots" width="${spacing}" height="${spacing}" patternUnits="userSpaceOnUse"><circle cx="${spacing / 2}" cy="${spacing / 2}" r="${theme.grid.dotRadius}" fill="${theme.grid.dotColor}" opacity="${theme.grid.opacity}"/></pattern>`
4622
4641
  );
4623
4642
  }
4643
+ const labelPadX = 130;
4644
+ const vbWidth = width + labelPadX * 2;
4645
+ const topPad = 40;
4646
+ const noteCount = layoutResult.connections.filter((c) => c.annotation).length;
4647
+ const notesHeight = noteCount > 0 ? noteCount * 16 + 10 : 0;
4648
+ const bottomPad = Math.max(40, notesHeight + 10);
4649
+ const vbHeight = height + topPad + bottomPad;
4650
+ const diagramBottom = height + bottomPad;
4624
4651
  const layers = [
4625
4652
  `<g class="pf-layer-bg">${buildBackground(theme, idPrefix, width, height)}</g>`,
4626
4653
  `<g class="pf-layer-cables">${buildCables(theme, layoutResult.connections)}</g>`,
@@ -4628,17 +4655,10 @@ function renderSvg(layoutResult, theme) {
4628
4655
  `<g class="pf-layer-params">${buildParams(layoutResult.blocks, theme)}</g>`,
4629
4656
  `<g class="pf-layer-jacks">${buildJacks(theme, idPrefix, layoutResult.blocks)}</g>`,
4630
4657
  `<g class="pf-layer-labels">${buildLabels(theme, layoutResult.blocks, layoutResult.connections)}</g>`,
4631
- `<g class="pf-layer-annotations">${buildAnnotations(theme, layoutResult.connections, height)}</g>`,
4632
- `<g class="pf-layer-legend">${buildLegend(theme, layoutResult)}</g>`
4658
+ `<g class="pf-layer-annotations">${buildAnnotations(theme, layoutResult.connections, diagramBottom)}</g>`,
4659
+ `<g class="pf-layer-legend">${buildLegend(theme, layoutResult, diagramBottom)}</g>`
4633
4660
  ].join("");
4634
4661
  const style = `<style>@media print { .pf-panel, .pf-jack { filter: none; } }</style>`;
4635
- const labelPadX = 130;
4636
- const vbWidth = width + labelPadX * 2;
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
4662
  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>`;
4643
4663
  return svg;
4644
4664
  }