Run ImpedenceFinder Z0 on routed signal nets during project analysis.
Pipeline samples PCB traces when stackup εr/h is present; power/ground are skipped. Named nets can be re-analyzed from the Impedance tab. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -347,7 +347,9 @@ export default function ProjectDetailPage({
|
||||
/>
|
||||
)}
|
||||
|
||||
{tab === "impedance" && <ImpedancePanel />}
|
||||
{tab === "impedance" && (
|
||||
<ImpedancePanel projectId={id} hasPcb={Boolean(project.hasPcb)} />
|
||||
)}
|
||||
|
||||
{tab === "logs" && (
|
||||
<ApiLogsSection logs={logs} />
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { computeImpedance } from "@/lib/api";
|
||||
import {
|
||||
analyzeImpedanceNets,
|
||||
computeImpedance,
|
||||
fetchImpedanceNets,
|
||||
} from "@/lib/api";
|
||||
import type {
|
||||
ImpedanceKind,
|
||||
ImpedanceNetsReport,
|
||||
ImpedanceStackupResult,
|
||||
ImpedanceTraceResult,
|
||||
} from "@/lib/types";
|
||||
@@ -21,7 +26,13 @@ function fmt(n: number | null | undefined, digits = 2): string {
|
||||
return n.toFixed(digits);
|
||||
}
|
||||
|
||||
export function ImpedancePanel() {
|
||||
export function ImpedancePanel({
|
||||
projectId,
|
||||
hasPcb,
|
||||
}: {
|
||||
projectId: string;
|
||||
hasPcb: boolean;
|
||||
}) {
|
||||
const [kind, setKind] = useState<ImpedanceKind>("microstrip");
|
||||
const [h, setH] = useState("0.20");
|
||||
const [er, setEr] = useState("4.5");
|
||||
@@ -33,6 +44,22 @@ export function ImpedancePanel() {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [trace, setTrace] = useState<ImpedanceTraceResult | null>(null);
|
||||
const [stackup, setStackup] = useState<ImpedanceStackupResult | null>(null);
|
||||
const [boardNets, setBoardNets] = useState<ImpedanceNetsReport | null>(null);
|
||||
const [extraNets, setExtraNets] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchImpedanceNets(projectId)
|
||||
.then((r) => {
|
||||
if (!cancelled) setBoardNets(r);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setBoardNets(null);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [projectId]);
|
||||
|
||||
async function runTrace() {
|
||||
setBusy(true);
|
||||
@@ -90,6 +117,23 @@ export function ImpedancePanel() {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function runSpecifiedNets() {
|
||||
const names = extraNets
|
||||
.split(/[\s,]+/)
|
||||
.map((n) => n.trim())
|
||||
.filter(Boolean);
|
||||
if (names.length === 0) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
setBoardNets(await analyzeImpedanceNets(projectId, names));
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Net analysis failed");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const needsGap = kind === "cpw" || kind === "diff";
|
||||
|
||||
return (
|
||||
@@ -100,9 +144,9 @@ export function ImpedancePanel() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
IPC-2141 / Hammerstad–Jensen from ImpedenceFinder (same as KiCad
|
||||
pcb_calculator). Advice only — this tab does not sample the PCB
|
||||
and does not emit findings. CPWG is not implemented.
|
||||
ImpedenceFinder (Hammerstad–Jensen). The calculator is advice only.
|
||||
With a `.kicad_pcb` and stackup, a pipeline run samples routed signal
|
||||
nets. CPWG is not implemented upstream.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<label className="space-y-1">
|
||||
@@ -175,6 +219,72 @@ export function ImpedancePanel() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">PCB net Z0 (ImpedenceFinder)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{!hasPcb && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Upload a `.kicad_pcb` and run analysis. Power/ground nets are
|
||||
skipped; signal traces with stackup εr/h are sampled.
|
||||
</p>
|
||||
)}
|
||||
{hasPcb && boardNets?.skipped && (
|
||||
<p className="text-sm text-muted-foreground">{boardNets.skipped}</p>
|
||||
)}
|
||||
{boardNets && boardNets.nets.length > 0 && (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-muted-foreground">
|
||||
<th className="py-1">Net</th>
|
||||
<th>Z0 avg Ω</th>
|
||||
<th>min–max</th>
|
||||
<th>mm</th>
|
||||
<th>topology</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{boardNets.nets.map((row) => (
|
||||
<tr key={row.net_name} className="border-t border-border">
|
||||
<td className="py-1">{row.net_name}</td>
|
||||
<td className="tabular-nums">
|
||||
{row.error ?? fmt(row.z0_avg_ohms)}
|
||||
</td>
|
||||
<td className="tabular-nums">
|
||||
{row.z0_min_ohms != null
|
||||
? `${fmt(row.z0_min_ohms)}–${fmt(row.z0_max_ohms)}`
|
||||
: "—"}
|
||||
</td>
|
||||
<td className="tabular-nums">{fmt(row.length_mm, 2)}</td>
|
||||
<td>{(row.topologies || []).join(", ") || "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{hasPcb && (
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<label className="min-w-40 flex-1 space-y-1">
|
||||
<Label>Extra nets</Label>
|
||||
<Input
|
||||
value={extraNets}
|
||||
onChange={(e) => setExtraNets(e.target.value)}
|
||||
placeholder="/USB.D+ /USB.D-"
|
||||
/>
|
||||
</label>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={runSpecifiedNets}
|
||||
disabled={busy}
|
||||
>
|
||||
Analyze named nets
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{stackup && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
DesignGraph,
|
||||
DeratingRow,
|
||||
ImpedanceKind,
|
||||
ImpedanceNetsReport,
|
||||
ImpedanceStackupResult,
|
||||
ImpedanceTraceResult,
|
||||
EdifSubDesign,
|
||||
@@ -609,6 +610,43 @@ export async function computeImpedance(body: {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchImpedanceNets(
|
||||
projectId: string,
|
||||
): Promise<ImpedanceNetsReport> {
|
||||
const res = await authFetch(
|
||||
`${BASE}/api/projects/${projectId}/impedance/nets`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
const detail = await res.json().catch(() => ({ detail: res.statusText }));
|
||||
throw new Error(
|
||||
typeof detail.detail === "string" ? detail.detail : "Impedance nets failed",
|
||||
);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function analyzeImpedanceNets(
|
||||
projectId: string,
|
||||
nets: string[],
|
||||
pitch_mm?: number,
|
||||
): Promise<ImpedanceNetsReport> {
|
||||
const res = await authFetch(
|
||||
`${BASE}/api/projects/${projectId}/impedance/nets`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ nets, pitch_mm }),
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
const detail = await res.json().catch(() => ({ detail: res.statusText }));
|
||||
throw new Error(
|
||||
typeof detail.detail === "string" ? detail.detail : "Impedance nets failed",
|
||||
);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchReport(
|
||||
projectId: string,
|
||||
): Promise<ValidationReport> {
|
||||
|
||||
@@ -364,6 +364,27 @@ export interface ImpedanceStackupResult {
|
||||
kicad_dru: string;
|
||||
}
|
||||
|
||||
export interface ImpedanceNetRow {
|
||||
net_name: string;
|
||||
length_mm?: number;
|
||||
branch_count?: number;
|
||||
is_differential?: boolean;
|
||||
partner_net_name?: string | null;
|
||||
topologies?: string[];
|
||||
z0_min_ohms?: number | null;
|
||||
z0_max_ohms?: number | null;
|
||||
z0_avg_ohms?: number | null;
|
||||
flags?: string[];
|
||||
sample_count?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ImpedanceNetsReport {
|
||||
pitch_mm: number;
|
||||
nets: ImpedanceNetRow[];
|
||||
skipped: string | null;
|
||||
}
|
||||
|
||||
export interface NetlistPreviewDesignator {
|
||||
ref: string;
|
||||
pins: { number: string; net_name: string }[];
|
||||
|
||||
Reference in New Issue
Block a user