Add gated Placement F2 pack skeleton for satellite xy.
Propose positions only from PCB footprints plus numeric decoupling max_distance_mm; otherwise skip with an explicit reason. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,11 +9,12 @@ import { PipelineStepper } from "@/components/progress/pipeline-stepper";
|
||||
import { usePlacementProgress } from "@/hooks/use-placement-progress";
|
||||
import {
|
||||
cancelPlacementPipeline,
|
||||
fetchPlacementPack,
|
||||
fetchPlacementPlan,
|
||||
fetchProject,
|
||||
startPlacementPipeline,
|
||||
} from "@/lib/api";
|
||||
import type { PlacementPlan } from "@/lib/types";
|
||||
import type { PlacementPack, PlacementPlan } from "@/lib/types";
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle2,
|
||||
@@ -33,6 +34,7 @@ export default function PlacementPage({
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const [placementStatus, setPlacementStatus] = useState<string>("draft");
|
||||
const [plan, setPlan] = useState<PlacementPlan | null>(null);
|
||||
const [pack, setPack] = useState<PlacementPack | null>(null);
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
const [starting, setStarting] = useState(false);
|
||||
const [statusLoaded, setStatusLoaded] = useState(false);
|
||||
@@ -67,6 +69,9 @@ export default function PlacementPage({
|
||||
fetchPlacementPlan(id)
|
||||
.then(setPlan)
|
||||
.catch(() => setPlan(null));
|
||||
fetchPlacementPack(id)
|
||||
.then(setPack)
|
||||
.catch(() => setPack(null));
|
||||
}, [id, alreadyDone, done, placementStatus, statusLoaded]);
|
||||
|
||||
const handleCancel = async () => {
|
||||
@@ -190,6 +195,15 @@ export default function PlacementPage({
|
||||
<span className="text-muted-foreground">
|
||||
· {summary?.domains ?? plan?.domains.length ?? "?"} domains,{" "}
|
||||
{summary?.groups ?? plan?.groups.length ?? "?"} IC groups
|
||||
{pack
|
||||
? pack.status === "packed"
|
||||
? ` · pack ${pack.placements.length} xy`
|
||||
: ` · pack skipped (${pack.skip_reason ?? "—"})`
|
||||
: summary?.pack_status
|
||||
? summary.pack_status === "packed"
|
||||
? ` · pack ${summary.pack_count ?? 0} xy`
|
||||
: ` · pack skipped (${summary.pack_skip_reason ?? "—"})`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
@@ -246,6 +260,37 @@ export default function PlacementPage({
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{pack && (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Pack (F2)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
{pack.status === "skipped" ? (
|
||||
<p className="text-muted-foreground">
|
||||
Skipped: {pack.skip_reason ?? "—"}. Needs uploaded{" "}
|
||||
<code className="text-xs">.kicad_pcb</code> and numeric{" "}
|
||||
<code className="text-xs">max_distance_mm</code> on
|
||||
decoupling rules.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{pack.placements.map((p) => (
|
||||
<p
|
||||
key={`${p.anchor_ref}-${p.ref}`}
|
||||
className="text-xs text-muted-foreground"
|
||||
>
|
||||
<span className="text-foreground font-medium">{p.ref}</span>
|
||||
{" "}near {p.anchor_ref} ≤ {p.max_distance_mm} mm → (
|
||||
{p.proposed_x}, {p.proposed_y}) {p.layer || ""}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,11 @@ const PLACEMENT_STAGES = [
|
||||
title: "Write placement plan",
|
||||
description: "Save placement_plan.json (no millimetres)",
|
||||
},
|
||||
{
|
||||
id: "pack",
|
||||
title: "Pack satellites",
|
||||
description: "Propose xy only with PCB + numeric layout_rules",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const STAGE_INDEX: Record<string, number> = Object.fromEntries(
|
||||
@@ -41,7 +46,13 @@ export function usePlacementProgress(projectId: string | null, enabled = true) {
|
||||
const [done, setDone] = useState(false);
|
||||
const [cancelled, setCancelled] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [summary, setSummary] = useState<{ domains?: number; groups?: number } | null>(null);
|
||||
const [summary, setSummary] = useState<{
|
||||
domains?: number;
|
||||
groups?: number;
|
||||
pack_status?: string;
|
||||
pack_count?: number;
|
||||
pack_skip_reason?: string | null;
|
||||
} | null>(null);
|
||||
const [started, setStarted] = useState(false);
|
||||
const esRef = useRef<EventSource | null>(null);
|
||||
const terminalRef = useRef(false);
|
||||
@@ -62,6 +73,10 @@ export function usePlacementProgress(projectId: string | null, enabled = true) {
|
||||
setSummary({
|
||||
domains: Number(data.domains) || 0,
|
||||
groups: Number(data.groups) || 0,
|
||||
pack_status: typeof data.pack_status === "string" ? data.pack_status : undefined,
|
||||
pack_count: Number(data.pack_count) || 0,
|
||||
pack_skip_reason:
|
||||
typeof data.pack_skip_reason === "string" ? data.pack_skip_reason : null,
|
||||
});
|
||||
setDone(true);
|
||||
terminalRef.current = true;
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
NetlistPreviewDesignator,
|
||||
PauseCheckpoint,
|
||||
PlacementPlan,
|
||||
PlacementPack,
|
||||
Project,
|
||||
SkippedComponent,
|
||||
ValidationReport,
|
||||
@@ -618,6 +619,15 @@ export async function fetchPlacementPlan(projectId: string): Promise<PlacementPl
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchPlacementPack(projectId: string): Promise<PlacementPack> {
|
||||
const res = await authFetch(`${BASE}/api/pipeline/${projectId}/placement/pack`);
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ detail: "Placement pack not found" }));
|
||||
throw new Error(err.detail || "Placement pack not found");
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// --- Logs ---
|
||||
|
||||
export async function fetchProjectLogs(
|
||||
|
||||
@@ -330,6 +330,24 @@ export interface PlacementPlan {
|
||||
groups: PlacementIcGroup[];
|
||||
}
|
||||
|
||||
export interface PlacementProposal {
|
||||
ref: string;
|
||||
anchor_ref: string;
|
||||
rule_kind: string;
|
||||
max_distance_mm: number;
|
||||
proposed_x: number;
|
||||
proposed_y: number;
|
||||
layer?: string;
|
||||
basis?: string;
|
||||
}
|
||||
|
||||
export interface PlacementPack {
|
||||
objective?: string;
|
||||
status: "packed" | "skipped";
|
||||
skip_reason?: string | null;
|
||||
placements: PlacementProposal[];
|
||||
}
|
||||
|
||||
// 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