Add Domains and Power rails views for functional groups.
Sidebar tabs show routing-first topology from placement_plan/functional_groups, with IC groups and satellite roles highlighted per domain and per supply rail. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -43,6 +43,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useOptionalUser } from "@/hooks/use-optional-auth";
|
||||
import { ImpedancePanel } from "@/components/project/impedance-panel";
|
||||
import { TopologyPanel } from "@/components/project/topology-panel";
|
||||
import { PcbUploadButton } from "@/components/project/pcb-upload";
|
||||
import { PdfViewerSheet } from "@/components/pdf/pdf-viewer-sheet";
|
||||
|
||||
@@ -401,6 +402,14 @@ export default function ProjectDetailPage({
|
||||
/>
|
||||
)}
|
||||
|
||||
{tab === "domains" && (
|
||||
<TopologyPanel projectId={id} mode="domains" />
|
||||
)}
|
||||
|
||||
{tab === "rails" && (
|
||||
<TopologyPanel projectId={id} mode="rails" />
|
||||
)}
|
||||
|
||||
{tab === "derating" && (
|
||||
<DeratingTable
|
||||
rows={deratingRows}
|
||||
|
||||
@@ -11,17 +11,18 @@ import {
|
||||
cancelPlacementPipeline,
|
||||
fetchPlacementPlan,
|
||||
fetchProject,
|
||||
startPlacementPipeline,
|
||||
} from "@/lib/api";
|
||||
import type { PlacementPlan } from "@/lib/types";
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle2,
|
||||
Loader2,
|
||||
OctagonX,
|
||||
Ban,
|
||||
LayoutGrid,
|
||||
} from "lucide-react";
|
||||
|
||||
type Plan = Awaited<ReturnType<typeof fetchPlacementPlan>>;
|
||||
|
||||
export default function PlacementPage({
|
||||
params,
|
||||
}: {
|
||||
@@ -31,13 +32,17 @@ export default function PlacementPage({
|
||||
const router = useRouter();
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const [placementStatus, setPlacementStatus] = useState<string>("draft");
|
||||
const [plan, setPlan] = useState<Plan | null>(null);
|
||||
const [plan, setPlan] = useState<PlacementPlan | null>(null);
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
const [starting, setStarting] = useState(false);
|
||||
const [statusLoaded, setStatusLoaded] = useState(false);
|
||||
|
||||
const active =
|
||||
placementStatus === "queued" || placementStatus === "running";
|
||||
const alreadyDone = placementStatus === "complete";
|
||||
const { steps, done, cancelled, error, summary, started } = usePlacementProgress(
|
||||
id,
|
||||
!alreadyDone && placementStatus !== "draft",
|
||||
statusLoaded && active,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -45,16 +50,24 @@ export default function PlacementPage({
|
||||
.then((p) => {
|
||||
setProjectName(p.name);
|
||||
setPlacementStatus(p.placementStatus ?? "draft");
|
||||
setStatusLoaded(true);
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => setStatusLoaded(true));
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!alreadyDone && !done) return;
|
||||
if (done) setPlacementStatus("complete");
|
||||
}, [done]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!statusLoaded) return;
|
||||
if (!alreadyDone && !done && placementStatus !== "draft" && placementStatus !== "error") {
|
||||
return;
|
||||
}
|
||||
fetchPlacementPlan(id)
|
||||
.then(setPlan)
|
||||
.catch(() => setPlan(null));
|
||||
}, [id, alreadyDone, done]);
|
||||
}, [id, alreadyDone, done, placementStatus, statusLoaded]);
|
||||
|
||||
const handleCancel = async () => {
|
||||
setCancelling(true);
|
||||
@@ -67,9 +80,21 @@ export default function PlacementPage({
|
||||
}
|
||||
};
|
||||
|
||||
const handleStart = async () => {
|
||||
setStarting(true);
|
||||
try {
|
||||
await startPlacementPipeline(id);
|
||||
setPlacementStatus("queued");
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
setStarting(false);
|
||||
alert(e instanceof Error ? e.message : "Failed to start placement");
|
||||
}
|
||||
};
|
||||
|
||||
const finished = alreadyDone || done;
|
||||
const isRunning = !finished && !cancelled && !error;
|
||||
const isQueued = isRunning && !started && !alreadyDone;
|
||||
const isRunning = statusLoaded && active && !done && !cancelled && !error;
|
||||
const isQueued = isRunning && !started;
|
||||
|
||||
return (
|
||||
<div className="flex-1 p-6 max-w-3xl mx-auto w-full space-y-6">
|
||||
@@ -81,14 +106,28 @@ export default function PlacementPage({
|
||||
Routing-first topology (no millimetres)
|
||||
</p>
|
||||
</div>
|
||||
<Link href={`/project/${id}`}>
|
||||
<Button size="sm" variant="outline">
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
Project
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href={`/project/${id}?tab=domains`}>
|
||||
<Button size="sm" variant="ghost">
|
||||
Domains
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/project/${id}`}>
|
||||
<Button size="sm" variant="outline">
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
Project
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!statusLoaded && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Checking placement status…
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isQueued && (
|
||||
<div className="flex items-center gap-3 p-4 rounded-lg border border-blue-500/30 bg-blue-500/5">
|
||||
<Loader2 className="h-5 w-5 text-blue-600 animate-spin" />
|
||||
@@ -133,25 +172,38 @@ export default function PlacementPage({
|
||||
size="sm"
|
||||
className="mt-3"
|
||||
variant="outline"
|
||||
onClick={() => router.push(`/project/${id}`)}
|
||||
disabled={starting}
|
||||
onClick={handleStart}
|
||||
>
|
||||
Back to project
|
||||
<LayoutGrid className="h-4 w-4 mr-1" />
|
||||
{starting ? "Starting…" : "Retry placement"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{finished && !error && !cancelled && (
|
||||
{statusLoaded && !active && !error && !cancelled && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 text-sm text-emerald-600 dark:text-emerald-400">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
Placement plan ready
|
||||
{summary && (
|
||||
{finished || plan ? (
|
||||
<div className="flex items-center gap-2 text-sm text-emerald-600 dark:text-emerald-400">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
Placement plan ready
|
||||
<span className="text-muted-foreground">
|
||||
· {summary.domains ?? plan?.domains.length ?? "?"} domains,{" "}
|
||||
{summary.groups ?? plan?.groups.length ?? "?"} IC groups
|
||||
· {summary?.domains ?? plan?.domains.length ?? "?"} domains,{" "}
|
||||
{summary?.groups ?? plan?.groups.length ?? "?"} IC groups
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border p-4 space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No placement plan yet. Build one (free, no LLM) or open Domains
|
||||
if analysis already wrote functional groups.
|
||||
</p>
|
||||
<Button size="sm" disabled={starting} onClick={handleStart}>
|
||||
<LayoutGrid className="h-4 w-4 mr-1" />
|
||||
{starting ? "Starting…" : "Build placement plan"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{plan && (
|
||||
<Card>
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
ScrollText,
|
||||
MessageSquareWarning,
|
||||
Library,
|
||||
Boxes,
|
||||
CircuitBoard,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAuthApi } from "@/hooks/use-auth-api";
|
||||
@@ -186,6 +188,8 @@ type NavItem =
|
||||
const PROJECT_NAV_ITEMS: NavItem[] = [
|
||||
{ type: "route", path: "/report", label: "Report", icon: ClipboardList },
|
||||
{ type: "tab", tab: "bom", label: "BOM", icon: TableProperties },
|
||||
{ type: "tab", tab: "domains", label: "Domains", icon: Boxes },
|
||||
{ type: "tab", tab: "rails", label: "Power rails", icon: CircuitBoard },
|
||||
{ type: "tab", tab: "derating", label: "Derating", icon: Zap },
|
||||
{ type: "tab", tab: "impedance", label: "Impedance", icon: Ruler },
|
||||
{ type: "tab", tab: "logs", label: "Logs", icon: ScrollText, adminOnly: true },
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { fetchPlacementPlan } from "@/lib/api";
|
||||
import type { PlacementPlan, PlacementIcGroup, RoleHint } from "@/lib/types";
|
||||
import { LayoutGrid, Zap, Loader2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ROLE_STYLES: Record<RoleHint | "ic", string> = {
|
||||
ic: "bg-sky-500/15 text-sky-700 dark:text-sky-300 border-sky-500/30",
|
||||
decoupling: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-500/30",
|
||||
bulk: "bg-teal-500/15 text-teal-700 dark:text-teal-300 border-teal-500/30",
|
||||
load_cap: "bg-violet-500/15 text-violet-700 dark:text-violet-300 border-violet-500/30",
|
||||
crystal: "bg-fuchsia-500/15 text-fuchsia-700 dark:text-fuchsia-300 border-fuchsia-500/30",
|
||||
filter: "bg-amber-500/15 text-amber-700 dark:text-amber-300 border-amber-500/30",
|
||||
pullup: "bg-orange-500/15 text-orange-700 dark:text-orange-300 border-orange-500/30",
|
||||
series: "bg-rose-500/15 text-rose-700 dark:text-rose-300 border-rose-500/30",
|
||||
divider: "bg-pink-500/15 text-pink-700 dark:text-pink-300 border-pink-500/30",
|
||||
bridge: "bg-indigo-500/15 text-indigo-700 dark:text-indigo-300 border-indigo-500/30",
|
||||
other: "bg-muted text-muted-foreground border-border",
|
||||
};
|
||||
|
||||
function RoleBadge({ role, label }: { role: RoleHint | "ic"; label: string }) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-md border px-1.5 py-0.5 text-[11px] font-mono tabular-nums",
|
||||
ROLE_STYLES[role] ?? ROLE_STYLES.other,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{role !== "ic" && (
|
||||
<span className="ml-1 opacity-70 font-sans">{role}</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupBlock({ group, emphasize }: { group: PlacementIcGroup; emphasize?: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border p-3 space-y-2",
|
||||
emphasize
|
||||
? "border-sky-500/40 bg-sky-500/[0.06]"
|
||||
: "border-border bg-muted/20",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<RoleBadge role="ic" label={group.ref} />
|
||||
{group.mpn && (
|
||||
<span className="text-xs text-muted-foreground truncate">{group.mpn}</span>
|
||||
)}
|
||||
{group.component_subtype && (
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{group.component_subtype}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{group.satellites.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{group.satellites.map((s) => (
|
||||
<RoleBadge
|
||||
key={s.ref}
|
||||
role={(s.role_hint as RoleHint) || "other"}
|
||||
label={s.ref}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">No satellite parts classified</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyPlan({ projectId }: { projectId: string }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="py-10 text-center space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No topology plan yet. Run analysis (graph build writes{" "}
|
||||
<code className="text-xs">functional_groups.json</code>) or build a
|
||||
placement plan.
|
||||
</p>
|
||||
<Link href={`/project/${projectId}/placement`}>
|
||||
<Button size="sm" variant="outline">
|
||||
<LayoutGrid className="h-4 w-4 mr-1" />
|
||||
Build placement plan
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function DomainsView({ plan }: { plan: PlacementPlan }) {
|
||||
const byRef = useMemo(
|
||||
() => Object.fromEntries(plan.groups.map((g) => [g.ref, g])),
|
||||
[plan.groups],
|
||||
);
|
||||
|
||||
if (plan.domains.length === 0) {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">No power domains detected.</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{plan.domains.map((dom) => {
|
||||
const groups = dom.ic_refs
|
||||
.map((r) => byRef[r])
|
||||
.filter(Boolean) as PlacementIcGroup[];
|
||||
return (
|
||||
<Card key={dom.domain_id}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<CardTitle className="text-sm font-semibold">{dom.domain_id}</CardTitle>
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
{groups.length} IC group{groups.length === 1 ? "" : "s"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Power nets:{" "}
|
||||
{dom.power_nets.length
|
||||
? dom.power_nets.map((n) => (
|
||||
<code key={n} className="mr-1.5 font-mono text-[11px]">
|
||||
{n}
|
||||
</code>
|
||||
))
|
||||
: "—"}
|
||||
</p>
|
||||
{dom.assemble_order.length > 0 && (
|
||||
<p className="text-[11px] text-muted-foreground mt-1 font-mono">
|
||||
Assemble: {dom.assemble_order.join(" → ")}
|
||||
</p>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<p className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide">
|
||||
Functional groups
|
||||
</p>
|
||||
{groups.map((g) => (
|
||||
<GroupBlock key={g.ref} group={g} emphasize />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type RailRow = {
|
||||
net: string;
|
||||
domainIds: string[];
|
||||
groups: PlacementIcGroup[];
|
||||
};
|
||||
|
||||
function RailsView({ plan }: { plan: PlacementPlan }) {
|
||||
const rails = useMemo(() => {
|
||||
const byRef = Object.fromEntries(plan.groups.map((g) => [g.ref, g]));
|
||||
const map = new Map<string, { domainIds: Set<string>; groupRefs: Set<string> }>();
|
||||
|
||||
for (const dom of plan.domains) {
|
||||
for (const net of dom.power_nets) {
|
||||
let entry = map.get(net);
|
||||
if (!entry) {
|
||||
entry = { domainIds: new Set(), groupRefs: new Set() };
|
||||
map.set(net, entry);
|
||||
}
|
||||
entry.domainIds.add(dom.domain_id);
|
||||
for (const iref of dom.ic_refs) entry.groupRefs.add(iref);
|
||||
}
|
||||
}
|
||||
|
||||
// Also attach groups that list the rail on their nets / satellites
|
||||
for (const g of plan.groups) {
|
||||
for (const net of g.nets) {
|
||||
const entry = map.get(net);
|
||||
if (entry) entry.groupRefs.add(g.ref);
|
||||
}
|
||||
for (const s of g.satellites) {
|
||||
for (const net of s.nets || []) {
|
||||
const entry = map.get(net);
|
||||
if (entry) entry.groupRefs.add(g.ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows: RailRow[] = [...map.entries()]
|
||||
.map(([net, v]) => ({
|
||||
net,
|
||||
domainIds: [...v.domainIds].sort(),
|
||||
groups: [...v.groupRefs]
|
||||
.map((r) => byRef[r])
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => (a.rank ?? 99) - (b.rank ?? 99) || a.ref.localeCompare(b.ref)),
|
||||
}))
|
||||
.sort((a, b) => a.net.localeCompare(b.net));
|
||||
|
||||
return rows;
|
||||
}, [plan]);
|
||||
|
||||
if (rails.length === 0) {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">No power rails in the topology plan.</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{rails.map((rail) => (
|
||||
<Card key={rail.net}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
<CardTitle className="text-sm font-mono">{rail.net}</CardTitle>
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
{rail.groups.length} group{rail.groups.length === 1 ? "" : "s"}
|
||||
</Badge>
|
||||
</div>
|
||||
{rail.domainIds.length > 0 && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Domain{rail.domainIds.length === 1 ? "" : "s"}:{" "}
|
||||
{rail.domainIds.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<p className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide">
|
||||
Functional groups on this rail
|
||||
</p>
|
||||
{rail.groups.map((g) => {
|
||||
const onRailSats = g.satellites.filter(
|
||||
(s) => (s.nets || []).includes(rail.net),
|
||||
);
|
||||
const highlight: PlacementIcGroup = {
|
||||
...g,
|
||||
satellites:
|
||||
onRailSats.length > 0
|
||||
? onRailSats
|
||||
: g.satellites.filter((s) =>
|
||||
["decoupling", "bulk"].includes(s.role_hint || ""),
|
||||
),
|
||||
};
|
||||
return <GroupBlock key={g.ref} group={highlight} emphasize />;
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TopologyPanel({
|
||||
projectId,
|
||||
mode,
|
||||
}: {
|
||||
projectId: string;
|
||||
mode: "domains" | "rails";
|
||||
}) {
|
||||
const [plan, setPlan] = useState<PlacementPlan | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [missing, setMissing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setMissing(false);
|
||||
fetchPlacementPlan(projectId)
|
||||
.then((p) => {
|
||||
if (!cancelled) {
|
||||
setPlan(p);
|
||||
setMissing(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setPlan(null);
|
||||
setMissing(true);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [projectId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground py-8">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Loading topology…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (missing || !plan) {
|
||||
return <EmptyPlan projectId={projectId} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">
|
||||
{mode === "domains" ? "Functional domains" : "Power rails"}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{mode === "domains"
|
||||
? "Power-net islands with IC functional groups and satellite roles (routing-first, no mm)."
|
||||
: "Each supply rail with the functional groups that hang off it."}
|
||||
</p>
|
||||
</div>
|
||||
{mode === "domains" ? <DomainsView plan={plan} /> : <RailsView plan={plan} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+2
-18
@@ -21,6 +21,7 @@ import type {
|
||||
LcscPayload,
|
||||
NetlistPreviewDesignator,
|
||||
PauseCheckpoint,
|
||||
PlacementPlan,
|
||||
Project,
|
||||
SkippedComponent,
|
||||
ValidationReport,
|
||||
@@ -608,24 +609,7 @@ export function placementEventsUrl(projectId: string): string {
|
||||
return `${BASE}/api/pipeline/${projectId}/placement/events`;
|
||||
}
|
||||
|
||||
export async function fetchPlacementPlan(projectId: string): Promise<{
|
||||
objective?: string;
|
||||
domains: Array<{
|
||||
domain_id: string;
|
||||
power_nets: string[];
|
||||
ic_refs: string[];
|
||||
assemble_order: string[];
|
||||
}>;
|
||||
groups: Array<{
|
||||
ref: string;
|
||||
mpn?: string | null;
|
||||
component_subtype?: string | null;
|
||||
rank?: number;
|
||||
satellites: Array<{ ref: string; role_hint?: string; hop?: number }>;
|
||||
layout_rules?: unknown[];
|
||||
assemble_order?: string[];
|
||||
}>;
|
||||
}> {
|
||||
export async function fetchPlacementPlan(projectId: string): Promise<PlacementPlan> {
|
||||
const res = await authFetch(`${BASE}/api/pipeline/${projectId}/placement/plan`);
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ detail: "Placement plan not found" }));
|
||||
|
||||
@@ -285,6 +285,51 @@ export interface Project {
|
||||
placementState?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export type RoleHint =
|
||||
| "decoupling"
|
||||
| "bulk"
|
||||
| "load_cap"
|
||||
| "filter"
|
||||
| "pullup"
|
||||
| "series"
|
||||
| "divider"
|
||||
| "bridge"
|
||||
| "crystal"
|
||||
| "other";
|
||||
|
||||
export interface PlacementSatellite {
|
||||
ref: string;
|
||||
component_type?: string;
|
||||
component_subtype?: string | null;
|
||||
nets?: string[];
|
||||
hop?: number;
|
||||
role_hint?: RoleHint;
|
||||
}
|
||||
|
||||
export interface PlacementIcGroup {
|
||||
ref: string;
|
||||
mpn?: string | null;
|
||||
component_subtype?: string | null;
|
||||
rank?: number;
|
||||
nets?: string[];
|
||||
satellites: PlacementSatellite[];
|
||||
layout_rules?: unknown[];
|
||||
assemble_order?: string[];
|
||||
}
|
||||
|
||||
export interface PlacementDomain {
|
||||
domain_id: string;
|
||||
power_nets: string[];
|
||||
ic_refs: string[];
|
||||
assemble_order: string[];
|
||||
}
|
||||
|
||||
export interface PlacementPlan {
|
||||
objective?: string;
|
||||
domains: PlacementDomain[];
|
||||
groups: PlacementIcGroup[];
|
||||
}
|
||||
|
||||
// One entry per EDIF sub-design (`&NNNN` ID prefix). Returned by the upload
|
||||
// endpoint and the subdesigns inspection endpoint; consumed by the wizard's
|
||||
// sub-design picker step.
|
||||
|
||||
Reference in New Issue
Block a user