diff --git a/generate_preview.ts b/generate_preview.ts
new file mode 100644
index 0000000..7ead802
--- /dev/null
+++ b/generate_preview.ts
@@ -0,0 +1,174 @@
+import { SingleInnerPartitionPackingSolver } from "./lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver"
+import type { PartitionInputProblem } from "./lib/types/InputProblem"
+
+// 构造包含 5 个解耦电容的 Mock Input
+const problem: PartitionInputProblem = {
+ isPartition: true,
+ partitionType: "decoupling_caps",
+ chipMap: {
+ C1: {
+ chipId: "C1",
+ pins: ["C1_P1", "C1_P2"],
+ size: { x: 1.0, y: 0.5 },
+ availableRotations: [0, 180],
+ },
+ C2: {
+ chipId: "C2",
+ pins: ["C2_P1", "C2_P2"],
+ size: { x: 1.2, y: 0.6 },
+ availableRotations: [0, 180],
+ },
+ C3: {
+ chipId: "C3",
+ pins: ["C3_P1", "C3_P2"],
+ size: { x: 0.8, y: 0.4 },
+ availableRotations: [0, 180],
+ },
+ C4: {
+ chipId: "C4",
+ pins: ["C4_P1", "C4_P2"],
+ size: { x: 1.5, y: 0.7 },
+ availableRotations: [0, 180],
+ },
+ C5: {
+ chipId: "C5",
+ pins: ["C5_P1", "C5_P2"],
+ size: { x: 0.9, y: 0.45 },
+ availableRotations: [0, 180],
+ },
+ },
+ chipPinMap: {
+ C1_P1: { pinId: "C1_P1", offset: { x: 0, y: 0.25 }, side: "y+" },
+ C1_P2: { pinId: "C1_P2", offset: { x: 0, y: -0.25 }, side: "y-" },
+ C2_P1: { pinId: "C2_P1", offset: { x: 0, y: 0.3 }, side: "y+" },
+ C2_P2: { pinId: "C2_P2", offset: { x: 0, y: -0.3 }, side: "y-" },
+ C3_P1: { pinId: "C3_P1", offset: { x: 0, y: 0.2 }, side: "y+" },
+ C3_P2: { pinId: "C3_P2", offset: { x: 0, y: -0.2 }, side: "y-" },
+ C4_P1: { pinId: "C4_P1", offset: { x: 0, y: 0.35 }, side: "y+" },
+ C4_P2: { pinId: "C4_P2", offset: { x: 0, y: -0.35 }, side: "y-" },
+ C5_P1: { pinId: "C5_P1", offset: { x: 0, y: 0.225 }, side: "y+" },
+ C5_P2: { pinId: "C5_P2", offset: { x: 0, y: -0.225 }, side: "y-" },
+ },
+ netMap: {
+ VCC: { netId: "VCC", isPositiveVoltageSource: true },
+ GND: { netId: "GND", isGround: true },
+ },
+ pinStrongConnMap: {},
+ netConnMap: {
+ "C1_P1-VCC": true,
+ "C1_P2-GND": true,
+ "C2_P1-VCC": true,
+ "C2_P2-GND": true,
+ "C3_P1-VCC": true,
+ "C3_P2-GND": true,
+ "C4_P1-VCC": true,
+ "C4_P2-GND": true,
+ "C5_P1-VCC": true,
+ "C5_P2-GND": true,
+ },
+ chipGap: 0.2,
+ partitionGap: 2,
+ decouplingCapsGap: 0.3,
+}
+
+const solver = new SingleInnerPartitionPackingSolver({
+ partitionInputProblem: problem,
+ pinIdToStronglyConnectedPins: {},
+})
+
+solver.step()
+
+const layout = solver.layout!
+const placements = layout.chipPlacements
+
+// 计算总宽度用于 SVG 视图框
+const chipIds = Object.keys(problem.chipMap).sort()
+let totalWidth = 0
+let maxHeight = 0
+for (const id of chipIds) {
+ const chip = problem.chipMap[id]!
+ totalWidth += chip.size.x
+ totalWidth += problem.decouplingCapsGap!
+ maxHeight = Math.max(maxHeight, chip.size.y)
+}
+totalWidth -= problem.decouplingCapsGap!
+
+const viewBoxWidth = totalWidth + 2
+const viewBoxHeight = maxHeight + 1
+
+// 生成 SVG
+const svgParts: string[] = []
+svgParts.push(
+ ``)
+
+const htmlContent = `
+
+
+ Decoupling Caps Layout Preview
+
+
+
+
+
Decoupling Capacitors Linear Layout Preview
+
+
Algorithm: _layoutDecouplingCapsLinear()
+
Gap: 0.3 | Coordinate System: Center-based
+
Sorting: chipId localeCompare (deterministic)
+
+
${svgParts.join("\n")}
+
Layout JSON
+
${JSON.stringify(layout, null, 2)}
+
+
+`
+
+await Bun.write("preview.html", htmlContent)
+console.log("✅ preview.html generated successfully!")
+console.log("📐 Layout Summary:")
+for (const id of chipIds) {
+ const p = placements[id]!
+ console.log(
+ ` ${id}: x=${p.x.toFixed(2)}, y=${p.y.toFixed(2)}, rotation=${p.ccwRotationDegrees}°`,
+ )
+}
diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts
index 88db103..d895f88 100644
--- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts
+++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts
@@ -38,6 +38,15 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver {
}
override _step() {
+ // Injection point: if decoupling caps, intercept and use linear layout
+ if (
+ this.partitionInputProblem.partitionType === "decoupling_caps" &&
+ !this.solved
+ ) {
+ this._layoutDecouplingCapsLinear()
+ return
+ }
+
// Initialize PackSolver2 if not already created
if (!this.activeSubSolver) {
const packInput = this.createPackInput()
@@ -64,6 +73,52 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver {
}
}
+ private _layoutDecouplingCapsLinear() {
+ const input = this.partitionInputProblem
+
+ // Step 1: Deterministic sort by chipId for consistent results across environments
+ const chips = Object.values(input.chipMap).sort((a, b) =>
+ a.chipId.localeCompare(b.chipId),
+ )
+ const gap = input.decouplingCapsGap ?? input.chipGap ?? 0.2
+
+ const chipPlacements: Record = {}
+
+ // Step 2: Pre-calculate total width for center alignment
+ let totalWidth = 0
+ let maxHeight = 0
+ for (let i = 0; i < chips.length; i++) {
+ totalWidth += chips[i]!.size.x
+ if (i < chips.length - 1) {
+ totalWidth += gap
+ }
+ maxHeight = Math.max(maxHeight, chips[i]!.size.y)
+ }
+
+ // Step 3: Linear coordinate assignment (center-based coordinate system)
+ let currentX = -(totalWidth / 2)
+ for (const chip of chips) {
+ const halfWidth = chip.size.x / 2
+ currentX += halfWidth
+
+ // Move to chip center
+ chipPlacements[chip.chipId] = {
+ x: currentX,
+ y: 0, // horizontal alignment
+ ccwRotationDegrees: chip.availableRotations?.[0] ?? 0,
+ }
+
+ currentX += halfWidth + gap
+ }
+
+ // Step 4: Package output
+ this.layout = {
+ chipPlacements,
+ groupPlacements: {},
+ }
+ this.solved = true
+ }
+
private createPackInput(): PackInput {
// Fall back to filtered mapping (weak + strong)
const pinToNetworkMap = createFilteredNetworkMapping({
diff --git a/package.json b/package.json
index 1fd506e..cb83a2d 100644
--- a/package.json
+++ b/package.json
@@ -31,5 +31,8 @@
},
"peerDependencies": {
"typescript": "^5"
+ },
+ "dependencies": {
+ "circuit-to-svg": "^0.0.332"
}
}
diff --git a/preview.html b/preview.html
new file mode 100644
index 0000000..eb7215d
--- /dev/null
+++ b/preview.html
@@ -0,0 +1,74 @@
+
+
+
+ Decoupling Caps Layout Preview
+
+
+
+
+
Decoupling Capacitors Linear Layout Preview
+
+
Algorithm: _layoutDecouplingCapsLinear()
+
Gap: 0.3 | Coordinate System: Center-based
+
Sorting: chipId localeCompare (deterministic)
+
+
+
Layout JSON
+
{
+ "chipPlacements": {
+ "C1": {
+ "x": -2.8,
+ "y": 0,
+ "ccwRotationDegrees": 0
+ },
+ "C2": {
+ "x": -1.4,
+ "y": 0,
+ "ccwRotationDegrees": 0
+ },
+ "C3": {
+ "x": -0.09999999999999998,
+ "y": 0,
+ "ccwRotationDegrees": 0
+ },
+ "C4": {
+ "x": 1.35,
+ "y": 0,
+ "ccwRotationDegrees": 0
+ },
+ "C5": {
+ "x": 2.8500000000000005,
+ "y": 0,
+ "ccwRotationDegrees": 0
+ }
+ },
+ "groupPlacements": {}
+}
+
+
+
\ No newline at end of file
diff --git a/tests/LinearDecouplingLayout.test.ts b/tests/LinearDecouplingLayout.test.ts
new file mode 100644
index 0000000..3b134db
--- /dev/null
+++ b/tests/LinearDecouplingLayout.test.ts
@@ -0,0 +1,91 @@
+import { test, expect } from "bun:test"
+import { SingleInnerPartitionPackingSolver } from "../lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver"
+import type { PartitionInputProblem } from "../lib/types/InputProblem"
+
+test("LinearDecouplingLayout - 3 chips horizontal alignment", () => {
+ // 构造包含 3 个不同尺寸芯片的 PartitionInputProblem
+ const problem: PartitionInputProblem = {
+ isPartition: true,
+ partitionType: "decoupling_caps",
+ chipMap: {
+ C1: {
+ chipId: "C1",
+ pins: ["C1_P1", "C1_P2"],
+ size: { x: 1.0, y: 0.5 },
+ availableRotations: [0, 180],
+ },
+ C2: {
+ chipId: "C2",
+ pins: ["C2_P1", "C2_P2"],
+ size: { x: 1.2, y: 0.6 },
+ availableRotations: [0, 180],
+ },
+ C3: {
+ chipId: "C3",
+ pins: ["C3_P1", "C3_P2"],
+ size: { x: 0.8, y: 0.4 },
+ availableRotations: [0, 180],
+ },
+ },
+ chipPinMap: {
+ C1_P1: { pinId: "C1_P1", offset: { x: 0, y: 0.25 }, side: "y+" },
+ C1_P2: { pinId: "C1_P2", offset: { x: 0, y: -0.25 }, side: "y-" },
+ C2_P1: { pinId: "C2_P1", offset: { x: 0, y: 0.3 }, side: "y+" },
+ C2_P2: { pinId: "C2_P2", offset: { x: 0, y: -0.3 }, side: "y-" },
+ C3_P1: { pinId: "C3_P1", offset: { x: 0, y: 0.2 }, side: "y+" },
+ C3_P2: { pinId: "C3_P2", offset: { x: 0, y: -0.2 }, side: "y-" },
+ },
+ netMap: {
+ VCC: { netId: "VCC", isPositiveVoltageSource: true },
+ GND: { netId: "GND", isGround: true },
+ },
+ pinStrongConnMap: {},
+ netConnMap: {
+ "C1_P1-VCC": true,
+ "C1_P2-GND": true,
+ "C2_P1-VCC": true,
+ "C2_P2-GND": true,
+ "C3_P1-VCC": true,
+ "C3_P2-GND": true,
+ },
+ chipGap: 0.2,
+ partitionGap: 2,
+ decouplingCapsGap: 0.3,
+ }
+
+ const solver = new SingleInnerPartitionPackingSolver({
+ partitionInputProblem: problem,
+ pinIdToStronglyConnectedPins: {},
+ })
+
+ // 执行布局
+ solver.step()
+
+ // 断言:布局已解决
+ expect(solver.solved).toBe(true)
+ expect(solver.layout).not.toBeNull()
+
+ const placements = solver.layout!.chipPlacements
+
+ // 断言:所有芯片的 y 坐标必须为 0
+ expect(placements.C1!.y).toBe(0)
+ expect(placements.C2!.y).toBe(0)
+ expect(placements.C3!.y).toBe(0)
+
+ // 断言:芯片按 chipId 排序 (C1, C2, C3)
+ // C1 在左,C2 在中,C3 在右
+ expect(placements.C1!.x).toBeLessThan(placements.C2!.x)
+ expect(placements.C2!.x).toBeLessThan(placements.C3!.x)
+
+ // 断言:芯片间的间距必须精确等于 gap (0.3)
+ // C1 和 C2 之间的间距
+ const gap1 = placements.C2!.x - placements.C1!.x - 1.0 / 2 - 1.2 / 2
+ expect(Math.abs(gap1 - 0.3)).toBeLessThan(0.0001)
+
+ // C2 和 C3 之间的间距
+ const gap2 = placements.C3!.x - placements.C2!.x - 1.2 / 2 - 0.8 / 2
+ expect(Math.abs(gap2 - 0.3)).toBeLessThan(0.0001)
+
+ // 输出布局快照
+ console.log("Layout Snapshot:", JSON.stringify(solver.layout, null, 2))
+})