@ogabrielluiz/patchflow 0.1.0 → 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/{chunk-B7DUSPQH.js → chunk-4VUBNFI4.js} +1 -1
- package/dist/{chunk-B7DUSPQH.js.map → chunk-4VUBNFI4.js.map} +1 -1
- package/dist/index.cjs +136 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +137 -55
- package/dist/index.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/package.json +11 -3
|
@@ -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:
|
|
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];
|
|
@@ -4421,7 +4465,28 @@ var SIGNAL_PILL_LABEL = {
|
|
|
4421
4465
|
trigger: "trig",
|
|
4422
4466
|
clock: "clk"
|
|
4423
4467
|
};
|
|
4424
|
-
|
|
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,79 +4495,89 @@ 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
|
|
4439
|
-
const
|
|
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
|
|
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="${
|
|
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
|
-
|
|
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;
|
|
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="${
|
|
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) {
|
|
4538
|
+
const annotated = connections.filter((c) => c.annotation);
|
|
4539
|
+
if (annotated.length === 0) return "";
|
|
4472
4540
|
const parts = [];
|
|
4473
4541
|
const fontFamily = sanitizeForSvg(theme.annotation.fontFamily);
|
|
4474
|
-
const
|
|
4475
|
-
const
|
|
4476
|
-
|
|
4477
|
-
|
|
4542
|
+
const noteFontSize = theme.annotation.fontSize + 2;
|
|
4543
|
+
const markerFontFamily = fontFamily;
|
|
4544
|
+
annotated.forEach((conn, i) => {
|
|
4545
|
+
const num = i + 1;
|
|
4478
4546
|
const sx = conn.sourcePoint.x;
|
|
4479
4547
|
const sy = conn.sourcePoint.y;
|
|
4480
4548
|
const tx = conn.targetPoint.x;
|
|
4481
4549
|
const ty = conn.targetPoint.y;
|
|
4482
|
-
let
|
|
4483
|
-
let
|
|
4550
|
+
let mx;
|
|
4551
|
+
let my;
|
|
4484
4552
|
if (conn.isFeedback) {
|
|
4485
|
-
|
|
4486
|
-
|
|
4553
|
+
mx = (sx + tx) / 2;
|
|
4554
|
+
const match = conn.path.match(/L\s+\S+\s+\S+\s+L\s+\S+\s+(\S+)/);
|
|
4555
|
+
my = match ? parseFloat(match[1]) : Math.max(sy, ty) + 30;
|
|
4487
4556
|
} else {
|
|
4488
|
-
|
|
4489
|
-
|
|
4557
|
+
mx = (sx + tx) / 2;
|
|
4558
|
+
my = (sy + ty) / 2;
|
|
4490
4559
|
}
|
|
4491
|
-
const
|
|
4492
|
-
let raw = conn.annotation;
|
|
4493
|
-
if (!conn.isFeedback) {
|
|
4494
|
-
const available = Math.abs(tx - sx);
|
|
4495
|
-
const prefixWidth = prefix.length * charWidth;
|
|
4496
|
-
const maxChars = Math.max(0, Math.floor((available - prefixWidth) / charWidth));
|
|
4497
|
-
if (maxChars > 0 && raw.length > maxChars) {
|
|
4498
|
-
raw = maxChars > 1 ? raw.slice(0, maxChars - 1) + "\u2026" : raw.slice(0, maxChars);
|
|
4499
|
-
}
|
|
4500
|
-
}
|
|
4501
|
-
const text = prefix + sanitizeForSvg(raw);
|
|
4560
|
+
const markerStroke = conn.isFeedback ? theme.cable.colors[conn.signalType].stroke : theme.annotation.color;
|
|
4502
4561
|
parts.push(
|
|
4503
|
-
`<
|
|
4562
|
+
`<circle cx="${mx}" cy="${my}" r="8" fill="${theme.panel.highlight}" stroke="${markerStroke}" stroke-width="0.5" data-annotation-marker="${num}"/>`
|
|
4504
4563
|
);
|
|
4505
|
-
|
|
4564
|
+
parts.push(
|
|
4565
|
+
`<text x="${mx}" y="${my + 3}" text-anchor="middle" font-family="${markerFontFamily}" font-size="9" fill="${theme.annotation.color}">${num}</text>`
|
|
4566
|
+
);
|
|
4567
|
+
});
|
|
4568
|
+
const panelX = -120;
|
|
4569
|
+
const lineGap = 16;
|
|
4570
|
+
const noteCount = annotated.length;
|
|
4571
|
+
const bottomY = layoutHeight - 10;
|
|
4572
|
+
const firstNoteY = bottomY - (noteCount - 1) * lineGap;
|
|
4573
|
+
annotated.forEach((conn, i) => {
|
|
4574
|
+
const num = i + 1;
|
|
4575
|
+
const noteText = `${num}. ${sanitizeForSvg(conn.annotation)}`;
|
|
4576
|
+
const noteY = firstNoteY + i * lineGap;
|
|
4577
|
+
parts.push(
|
|
4578
|
+
`<text x="${panelX}" y="${noteY}" font-family="${fontFamily}" font-size="${noteFontSize}" font-weight="600" fill="${theme.annotation.color}">${noteText}</text>`
|
|
4579
|
+
);
|
|
4580
|
+
});
|
|
4506
4581
|
return parts.join("");
|
|
4507
4582
|
}
|
|
4508
4583
|
function buildLegend(theme, layoutResult) {
|
|
@@ -4512,16 +4587,18 @@ function buildLegend(theme, layoutResult) {
|
|
|
4512
4587
|
const parts = [];
|
|
4513
4588
|
const fontFamily = sanitizeForSvg(theme.annotation.fontFamily);
|
|
4514
4589
|
const itemWidth = 70;
|
|
4590
|
+
const totalWidth = used.length * itemWidth;
|
|
4591
|
+
const legendStartX = layoutResult.width - totalWidth;
|
|
4515
4592
|
const y = layoutResult.height - 20;
|
|
4516
|
-
let
|
|
4517
|
-
|
|
4593
|
+
for (let i = 0; i < used.length; i++) {
|
|
4594
|
+
const sig = used[i];
|
|
4518
4595
|
const color = theme.cable.colors[sig].stroke;
|
|
4596
|
+
const x = legendStartX + i * itemWidth;
|
|
4519
4597
|
let g = `<g transform="translate(${x}, ${y})">`;
|
|
4520
4598
|
g += `<line x1="0" y1="0" x2="20" y2="0" stroke="${color}" stroke-width="3" stroke-linecap="round"/>`;
|
|
4521
4599
|
g += `<text x="26" y="3" font-family="${fontFamily}" font-size="9" fill="${theme.annotation.color}">${sig}</text>`;
|
|
4522
4600
|
g += `</g>`;
|
|
4523
4601
|
parts.push(g);
|
|
4524
|
-
x += itemWidth;
|
|
4525
4602
|
}
|
|
4526
4603
|
return parts.join("");
|
|
4527
4604
|
}
|
|
@@ -4550,14 +4627,19 @@ function renderSvg(layoutResult, theme) {
|
|
|
4550
4627
|
`<g class="pf-layer-panels" >${buildPanels(theme, idPrefix, layoutResult.blocks)}</g>`,
|
|
4551
4628
|
`<g class="pf-layer-params">${buildParams(layoutResult.blocks, theme)}</g>`,
|
|
4552
4629
|
`<g class="pf-layer-jacks">${buildJacks(theme, idPrefix, layoutResult.blocks)}</g>`,
|
|
4553
|
-
`<g class="pf-layer-labels">${buildLabels(theme, layoutResult.blocks)}</g>`,
|
|
4554
|
-
`<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>`,
|
|
4555
4632
|
`<g class="pf-layer-legend">${buildLegend(theme, layoutResult)}</g>`
|
|
4556
4633
|
].join("");
|
|
4557
4634
|
const style = `<style>@media print { .pf-panel, .pf-jack { filter: none; } }</style>`;
|
|
4558
4635
|
const labelPadX = 130;
|
|
4559
4636
|
const vbWidth = width + labelPadX * 2;
|
|
4560
|
-
const
|
|
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>`;
|
|
4561
4643
|
return svg;
|
|
4562
4644
|
}
|
|
4563
4645
|
|
|
@@ -4620,7 +4702,7 @@ var defaultTheme = {
|
|
|
4620
4702
|
annotation: {
|
|
4621
4703
|
fontFamily: "'SF Mono', 'Fira Code', Consolas, 'Courier New', monospace",
|
|
4622
4704
|
fontSize: 9,
|
|
4623
|
-
color: "#
|
|
4705
|
+
color: "#4a4a4a",
|
|
4624
4706
|
haloColor: "#f7f5f0"
|
|
4625
4707
|
},
|
|
4626
4708
|
grid: {
|