@ogabrielluiz/patchflow 0.1.3 → 0.1.5

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
@@ -4091,7 +4091,7 @@ function selfLoopArcPath(source, target, blockBottom, arcOffset) {
4091
4091
  var MIN_WIDTH = 140;
4092
4092
  var MIN_HEIGHT = 90;
4093
4093
  function getBlockDimensions(block, portCount) {
4094
- const labelWidth = block.label.length * 8;
4094
+ const labelWidth = block.label.length * 11;
4095
4095
  const subLabelWidth = block.subLabel ? block.subLabel.length * 7 : 0;
4096
4096
  const paramWidths = block.params.map((p) => `${p.key}: ${p.value}`.length * 7);
4097
4097
  const longestParam = paramWidths.length > 0 ? Math.max(...paramWidths) : 0;
@@ -4333,19 +4333,46 @@ 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() {
4347
4366
  return "pf-" + Math.random().toString(16).slice(2, 8);
4348
4367
  }
4368
+ function fitLabel(label, maxWidth, charWidth) {
4369
+ if (maxWidth <= 0 || charWidth <= 0) return label;
4370
+ const maxChars = Math.floor(maxWidth / charWidth);
4371
+ if (maxChars <= 0) return label;
4372
+ if (label.length <= maxChars) return label;
4373
+ if (maxChars === 1) return "\u2026";
4374
+ return label.slice(0, maxChars - 1) + "\u2026";
4375
+ }
4349
4376
  function buildDesc(layoutResult) {
4350
4377
  const blockCount = layoutResult.blocks.length;
4351
4378
  const connCount = layoutResult.connections.length;
@@ -4391,11 +4418,11 @@ function buildPanels(theme, idPrefix, blocks) {
4391
4418
  const parts = [];
4392
4419
  for (const block of blocks) {
4393
4420
  const moduleName = sanitizeForSvg(block.parentModule || block.label);
4394
- const label = sanitizeForSvg(block.label);
4395
4421
  const fontFamily = sanitizeForSvg(theme.label.fontFamily);
4396
4422
  const insetX = block.x + 12;
4397
4423
  const insetY = block.y + 8;
4398
4424
  const insetW = block.width - 24;
4425
+ const label = sanitizeForSvg(fitLabel(block.label, insetW, 11));
4399
4426
  let group = `<g data-module="${moduleName}" filter="url(#${idPrefix}-panel-shadow)">`;
4400
4427
  group += `<rect x="${block.x}" y="${block.y}" width="${block.width}" height="${block.height}" fill="${theme.panel.fill}" stroke="${theme.panel.stroke}" stroke-width="0.75" rx="${theme.panel.cornerRadius}"/>`;
4401
4428
  group += `<line x1="${block.x}" y1="${block.y + 0.5}" x2="${block.x + block.width}" y2="${block.y + 0.5}" stroke="${theme.panel.highlight}" stroke-width="${theme.panel.bevelWidth}"/>`;
@@ -4403,7 +4430,7 @@ function buildPanels(theme, idPrefix, blocks) {
4403
4430
  group += `<rect x="${insetX}" y="${insetY}" width="${insetW}" height="28" fill="${theme.label.plateFill}" stroke="${theme.label.plateStroke}" stroke-width="0.5"/>`;
4404
4431
  group += `<text x="${block.x + block.width / 2}" y="${block.y + 22}" text-anchor="middle" font-family="${fontFamily}" font-size="14" font-weight="700" fill="${theme.label.color}" letter-spacing="3">${label}</text>`;
4405
4432
  if (block.subLabel) {
4406
- const subLabel = sanitizeForSvg(block.subLabel);
4433
+ const subLabel = sanitizeForSvg(fitLabel(block.subLabel, insetW - 8, 7));
4407
4434
  const barX = insetX;
4408
4435
  const barY = insetY + 28;
4409
4436
  const barW = insetW;
@@ -4429,7 +4456,8 @@ function buildParams(blocks, theme) {
4429
4456
  `<rect x="${px}" y="${py}" width="${pw}" height="20" fill="${theme.param.plateFill}" stroke="${theme.param.plateStroke}" stroke-width="0.5"/>`
4430
4457
  );
4431
4458
  const keyNorm = param.key.trim().toLowerCase();
4432
- const text = keyNorm === blockLabelNorm ? sanitizeForSvg(param.value) : `${sanitizeForSvg(param.key)}: ${sanitizeForSvg(param.value)}`;
4459
+ const rawText = keyNorm === blockLabelNorm ? param.value : `${param.key}: ${param.value}`;
4460
+ const text = sanitizeForSvg(fitLabel(rawText, pw - 8, 7));
4433
4461
  parts.push(
4434
4462
  `<text x="${px + pw / 2}" y="${py + 14}" text-anchor="middle" font-family="${monoFont}" font-size="10" fill="${theme.param.textColor}">${text}</text>`
4435
4463
  );
@@ -4580,7 +4608,7 @@ function buildAnnotations(theme, connections, layoutHeight) {
4580
4608
  });
4581
4609
  return parts.join("");
4582
4610
  }
4583
- function buildLegend(theme, layoutResult) {
4611
+ function buildLegend(theme, layoutResult, diagramBottom) {
4584
4612
  const order = ["audio", "cv", "pitch", "gate", "trigger", "clock"];
4585
4613
  const used = order.filter((t) => (layoutResult.signalTypeStats[t] ?? 0) > 0);
4586
4614
  if (used.length === 0) return "";
@@ -4589,7 +4617,7 @@ function buildLegend(theme, layoutResult) {
4589
4617
  const itemWidth = 70;
4590
4618
  const totalWidth = used.length * itemWidth;
4591
4619
  const legendStartX = layoutResult.width - totalWidth;
4592
- const y = layoutResult.height - 20;
4620
+ const y = diagramBottom - 20;
4593
4621
  for (let i = 0; i < used.length; i++) {
4594
4622
  const sig = used[i];
4595
4623
  const color = theme.cable.colors[sig].stroke;
@@ -4621,6 +4649,14 @@ function renderSvg(layoutResult, theme) {
4621
4649
  `<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
4650
  );
4623
4651
  }
4652
+ const labelPadX = 130;
4653
+ const vbWidth = width + labelPadX * 2;
4654
+ const topPad = 40;
4655
+ const noteCount = layoutResult.connections.filter((c) => c.annotation).length;
4656
+ const notesHeight = noteCount > 0 ? noteCount * 16 + 10 : 0;
4657
+ const bottomPad = Math.max(40, notesHeight + 10);
4658
+ const vbHeight = height + topPad + bottomPad;
4659
+ const diagramBottom = height + bottomPad;
4624
4660
  const layers = [
4625
4661
  `<g class="pf-layer-bg">${buildBackground(theme, idPrefix, width, height)}</g>`,
4626
4662
  `<g class="pf-layer-cables">${buildCables(theme, layoutResult.connections)}</g>`,
@@ -4628,17 +4664,10 @@ function renderSvg(layoutResult, theme) {
4628
4664
  `<g class="pf-layer-params">${buildParams(layoutResult.blocks, theme)}</g>`,
4629
4665
  `<g class="pf-layer-jacks">${buildJacks(theme, idPrefix, layoutResult.blocks)}</g>`,
4630
4666
  `<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>`
4667
+ `<g class="pf-layer-annotations">${buildAnnotations(theme, layoutResult.connections, diagramBottom)}</g>`,
4668
+ `<g class="pf-layer-legend">${buildLegend(theme, layoutResult, diagramBottom)}</g>`
4633
4669
  ].join("");
4634
4670
  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
4671
  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
4672
  return svg;
4644
4673
  }