@ogabrielluiz/patchflow 0.1.1 → 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.
- package/dist/{chunk-B7DUSPQH.js → chunk-YONEHQMI.js} +1 -1
- package/dist/chunk-YONEHQMI.js.map +1 -0
- package/dist/index.cjs +139 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +140 -45
- package/dist/index.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +7 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-B7DUSPQH.js.map +0 -1
|
@@ -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
|
@@ -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:
|
|
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:
|
|
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 ??
|
|
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];
|
|
@@ -4289,14 +4333,33 @@ function layout(graph, options = {}) {
|
|
|
4289
4333
|
const feedbackSpace = hasFeedback ? diagramBottom - maxY + feedbackArcOffset + 30 + 16 : 0;
|
|
4290
4334
|
const width = maxX + margin;
|
|
4291
4335
|
const height = maxY + margin + feedbackSpace;
|
|
4336
|
+
const warnings = checkHeightInvariant({
|
|
4337
|
+
blocks: layoutBlocks,
|
|
4338
|
+
height,
|
|
4339
|
+
hasFeedback,
|
|
4340
|
+
feedbackBottom: diagramBottom + feedbackArcOffset
|
|
4341
|
+
});
|
|
4292
4342
|
return {
|
|
4293
4343
|
blocks: layoutBlocks,
|
|
4294
4344
|
connections: layoutConnections,
|
|
4295
4345
|
width,
|
|
4296
4346
|
height,
|
|
4297
|
-
signalTypeStats: graph.signalTypeStats
|
|
4347
|
+
signalTypeStats: graph.signalTypeStats,
|
|
4348
|
+
warnings
|
|
4298
4349
|
};
|
|
4299
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
|
+
}
|
|
4300
4363
|
|
|
4301
4364
|
// src/renderer.ts
|
|
4302
4365
|
function genId() {
|
|
@@ -4421,7 +4484,28 @@ var SIGNAL_PILL_LABEL = {
|
|
|
4421
4484
|
trigger: "trig",
|
|
4422
4485
|
clock: "clk"
|
|
4423
4486
|
};
|
|
4424
|
-
|
|
4487
|
+
var UPWARD_CABLE_THRESHOLD = 20;
|
|
4488
|
+
function computeLabelBelowMap(connections) {
|
|
4489
|
+
const map = /* @__PURE__ */ new Map();
|
|
4490
|
+
for (const conn of connections) {
|
|
4491
|
+
const srcKey = `${conn.source.blockId}:${conn.source.portId}:out`;
|
|
4492
|
+
const srcDy = conn.targetPoint.y - conn.sourcePoint.y;
|
|
4493
|
+
if (srcDy < -UPWARD_CABLE_THRESHOLD) {
|
|
4494
|
+
map.set(srcKey, true);
|
|
4495
|
+
} else if (!map.has(srcKey)) {
|
|
4496
|
+
map.set(srcKey, false);
|
|
4497
|
+
}
|
|
4498
|
+
const tgtKey = `${conn.target.blockId}:${conn.target.portId}:in`;
|
|
4499
|
+
const tgtDy = conn.sourcePoint.y - conn.targetPoint.y;
|
|
4500
|
+
if (conn.isFeedback || tgtDy > UPWARD_CABLE_THRESHOLD) {
|
|
4501
|
+
map.set(tgtKey, true);
|
|
4502
|
+
} else if (!map.has(tgtKey)) {
|
|
4503
|
+
map.set(tgtKey, false);
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
return map;
|
|
4507
|
+
}
|
|
4508
|
+
function buildLabels(theme, blocks, connections) {
|
|
4425
4509
|
const parts = [];
|
|
4426
4510
|
const fontFamily = sanitizeForSvg(theme.port.fontFamily);
|
|
4427
4511
|
const pillShow = theme.port.pill.show;
|
|
@@ -4430,50 +4514,51 @@ function buildLabels(theme, blocks) {
|
|
|
4430
4514
|
const pillRadius = theme.port.pill.cornerRadius;
|
|
4431
4515
|
const pillPadX = 3;
|
|
4432
4516
|
const pillHeight = 11;
|
|
4433
|
-
const pillGap = 6;
|
|
4434
4517
|
const charWidth = 6.5;
|
|
4518
|
+
const pillOffsetAbove = 20;
|
|
4519
|
+
const nameOffsetAbove = 32;
|
|
4520
|
+
const pillOffsetBelow = 20;
|
|
4521
|
+
const nameOffsetBelow = 32;
|
|
4522
|
+
const belowMap = computeLabelBelowMap(connections);
|
|
4523
|
+
const labelOffsetX = 6;
|
|
4435
4524
|
for (const block of blocks) {
|
|
4436
4525
|
for (const port of block.ports) {
|
|
4437
4526
|
const { x, y } = port.position;
|
|
4438
|
-
const
|
|
4439
|
-
const
|
|
4440
|
-
const labelY = y + 3;
|
|
4441
|
-
const anchor = isOut ? "start" : "end";
|
|
4527
|
+
const key = `${block.id}:${port.id}:${port.direction}`;
|
|
4528
|
+
const below = belowMap.get(key) === true;
|
|
4442
4529
|
const display = sanitizeForSvg(port.display);
|
|
4443
|
-
const
|
|
4530
|
+
const isOutput = port.direction === "out";
|
|
4531
|
+
const textY = below ? y + nameOffsetBelow : y - nameOffsetAbove;
|
|
4532
|
+
const textX = isOutput ? x + labelOffsetX : x - labelOffsetX;
|
|
4533
|
+
const textAnchor = isOutput ? "start" : "end";
|
|
4444
4534
|
parts.push(
|
|
4445
|
-
`<text x="${
|
|
4535
|
+
`<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
4536
|
);
|
|
4447
4537
|
if (pillShow && port.signalType) {
|
|
4448
4538
|
const pillText = SIGNAL_PILL_LABEL[port.signalType];
|
|
4449
4539
|
const pillWidth = pillText.length * charWidth + pillPadX * 2;
|
|
4450
4540
|
const pillColor = theme.cable.colors[port.signalType].stroke;
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
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;
|
|
4541
|
+
const pillCenterY = below ? y + pillOffsetBelow : y - pillOffsetAbove;
|
|
4542
|
+
const pillX = isOutput ? x + labelOffsetX : x - labelOffsetX - pillWidth;
|
|
4543
|
+
const pillY = pillCenterY - pillHeight / 2;
|
|
4544
|
+
const pillTextX = pillX + pillWidth / 2;
|
|
4460
4545
|
parts.push(
|
|
4461
4546
|
`<rect class="pf-port-pill" x="${pillX}" y="${pillY}" width="${pillWidth}" height="${pillHeight}" rx="${pillRadius}" fill="${pillColor}" data-signal="${port.signalType}"/>`
|
|
4462
4547
|
);
|
|
4463
4548
|
parts.push(
|
|
4464
|
-
`<text class="pf-port-pill-text" x="${
|
|
4549
|
+
`<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
4550
|
);
|
|
4466
4551
|
}
|
|
4467
4552
|
}
|
|
4468
4553
|
}
|
|
4469
4554
|
return parts.join("");
|
|
4470
4555
|
}
|
|
4471
|
-
function buildAnnotations(theme, connections) {
|
|
4556
|
+
function buildAnnotations(theme, connections, layoutHeight) {
|
|
4472
4557
|
const annotated = connections.filter((c) => c.annotation);
|
|
4473
4558
|
if (annotated.length === 0) return "";
|
|
4474
4559
|
const parts = [];
|
|
4475
4560
|
const fontFamily = sanitizeForSvg(theme.annotation.fontFamily);
|
|
4476
|
-
const noteFontSize = theme.annotation.fontSize +
|
|
4561
|
+
const noteFontSize = theme.annotation.fontSize + 2;
|
|
4477
4562
|
const markerFontFamily = fontFamily;
|
|
4478
4563
|
annotated.forEach((conn, i) => {
|
|
4479
4564
|
const num = i + 1;
|
|
@@ -4491,44 +4576,48 @@ function buildAnnotations(theme, connections) {
|
|
|
4491
4576
|
mx = (sx + tx) / 2;
|
|
4492
4577
|
my = (sy + ty) / 2;
|
|
4493
4578
|
}
|
|
4494
|
-
const markerStroke = conn.isFeedback ? theme.cable.colors[conn.signalType].stroke : theme.
|
|
4579
|
+
const markerStroke = conn.isFeedback ? theme.cable.colors[conn.signalType].stroke : theme.annotation.color;
|
|
4495
4580
|
parts.push(
|
|
4496
4581
|
`<circle cx="${mx}" cy="${my}" r="8" fill="${theme.panel.highlight}" stroke="${markerStroke}" stroke-width="0.5" data-annotation-marker="${num}"/>`
|
|
4497
4582
|
);
|
|
4498
4583
|
parts.push(
|
|
4499
|
-
`<text x="${mx}" y="${my + 3}" text-anchor="middle" font-family="${markerFontFamily}" font-size="9" fill="${theme.
|
|
4584
|
+
`<text x="${mx}" y="${my + 3}" text-anchor="middle" font-family="${markerFontFamily}" font-size="9" fill="${theme.annotation.color}">${num}</text>`
|
|
4500
4585
|
);
|
|
4501
4586
|
});
|
|
4502
4587
|
const panelX = -120;
|
|
4503
|
-
|
|
4504
|
-
const
|
|
4588
|
+
const lineGap = 16;
|
|
4589
|
+
const noteCount = annotated.length;
|
|
4590
|
+
const bottomY = layoutHeight - 10;
|
|
4591
|
+
const firstNoteY = bottomY - (noteCount - 1) * lineGap;
|
|
4505
4592
|
annotated.forEach((conn, i) => {
|
|
4506
4593
|
const num = i + 1;
|
|
4507
4594
|
const noteText = `${num}. ${sanitizeForSvg(conn.annotation)}`;
|
|
4595
|
+
const noteY = firstNoteY + i * lineGap;
|
|
4508
4596
|
parts.push(
|
|
4509
|
-
`<text x="${panelX}" y="${
|
|
4597
|
+
`<text x="${panelX}" y="${noteY}" font-family="${fontFamily}" font-size="${noteFontSize}" font-weight="600" fill="${theme.annotation.color}">${noteText}</text>`
|
|
4510
4598
|
);
|
|
4511
|
-
panelY += lineGap;
|
|
4512
4599
|
});
|
|
4513
4600
|
return parts.join("");
|
|
4514
4601
|
}
|
|
4515
|
-
function buildLegend(theme, layoutResult) {
|
|
4602
|
+
function buildLegend(theme, layoutResult, diagramBottom) {
|
|
4516
4603
|
const order = ["audio", "cv", "pitch", "gate", "trigger", "clock"];
|
|
4517
4604
|
const used = order.filter((t) => (layoutResult.signalTypeStats[t] ?? 0) > 0);
|
|
4518
4605
|
if (used.length === 0) return "";
|
|
4519
4606
|
const parts = [];
|
|
4520
4607
|
const fontFamily = sanitizeForSvg(theme.annotation.fontFamily);
|
|
4521
4608
|
const itemWidth = 70;
|
|
4522
|
-
const
|
|
4523
|
-
|
|
4524
|
-
|
|
4609
|
+
const totalWidth = used.length * itemWidth;
|
|
4610
|
+
const legendStartX = layoutResult.width - totalWidth;
|
|
4611
|
+
const y = diagramBottom - 20;
|
|
4612
|
+
for (let i = 0; i < used.length; i++) {
|
|
4613
|
+
const sig = used[i];
|
|
4525
4614
|
const color = theme.cable.colors[sig].stroke;
|
|
4615
|
+
const x = legendStartX + i * itemWidth;
|
|
4526
4616
|
let g = `<g transform="translate(${x}, ${y})">`;
|
|
4527
4617
|
g += `<line x1="0" y1="0" x2="20" y2="0" stroke="${color}" stroke-width="3" stroke-linecap="round"/>`;
|
|
4528
4618
|
g += `<text x="26" y="3" font-family="${fontFamily}" font-size="9" fill="${theme.annotation.color}">${sig}</text>`;
|
|
4529
4619
|
g += `</g>`;
|
|
4530
4620
|
parts.push(g);
|
|
4531
|
-
x += itemWidth;
|
|
4532
4621
|
}
|
|
4533
4622
|
return parts.join("");
|
|
4534
4623
|
}
|
|
@@ -4551,20 +4640,26 @@ function renderSvg(layoutResult, theme) {
|
|
|
4551
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>`
|
|
4552
4641
|
);
|
|
4553
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;
|
|
4554
4651
|
const layers = [
|
|
4555
4652
|
`<g class="pf-layer-bg">${buildBackground(theme, idPrefix, width, height)}</g>`,
|
|
4556
4653
|
`<g class="pf-layer-cables">${buildCables(theme, layoutResult.connections)}</g>`,
|
|
4557
4654
|
`<g class="pf-layer-panels" >${buildPanels(theme, idPrefix, layoutResult.blocks)}</g>`,
|
|
4558
4655
|
`<g class="pf-layer-params">${buildParams(layoutResult.blocks, theme)}</g>`,
|
|
4559
4656
|
`<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>`,
|
|
4562
|
-
`<g class="pf-layer-legend">${buildLegend(theme, layoutResult)}</g>`
|
|
4657
|
+
`<g class="pf-layer-labels">${buildLabels(theme, layoutResult.blocks, layoutResult.connections)}</g>`,
|
|
4658
|
+
`<g class="pf-layer-annotations">${buildAnnotations(theme, layoutResult.connections, diagramBottom)}</g>`,
|
|
4659
|
+
`<g class="pf-layer-legend">${buildLegend(theme, layoutResult, diagramBottom)}</g>`
|
|
4563
4660
|
].join("");
|
|
4564
4661
|
const style = `<style>@media print { .pf-panel, .pf-jack { filter: none; } }</style>`;
|
|
4565
|
-
const labelPadX =
|
|
4566
|
-
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>`;
|
|
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>`;
|
|
4568
4663
|
return svg;
|
|
4569
4664
|
}
|
|
4570
4665
|
|
|
@@ -4627,7 +4722,7 @@ var defaultTheme = {
|
|
|
4627
4722
|
annotation: {
|
|
4628
4723
|
fontFamily: "'SF Mono', 'Fira Code', Consolas, 'Courier New', monospace",
|
|
4629
4724
|
fontSize: 9,
|
|
4630
|
-
color: "#
|
|
4725
|
+
color: "#4a4a4a",
|
|
4631
4726
|
haloColor: "#f7f5f0"
|
|
4632
4727
|
},
|
|
4633
4728
|
grid: {
|