Skip to content

fix(layout): inherit pack bounds from ancestor when group lacks width/height#2274

Open
gsdali wants to merge 1 commit intotscircuit:mainfrom
gsdali:investigate-large-chip-off-board
Open

fix(layout): inherit pack bounds from ancestor when group lacks width/height#2274
gsdali wants to merge 1 commit intotscircuit:mainfrom
gsdali:investigate-large-chip-off-board

Conversation

@gsdali
Copy link
Copy Markdown

@gsdali gsdali commented May 8, 2026

Summary

Closes #2272.

When a <group> runs the auto-packer on its children but doesn't
itself specify width and height, the packer was invoked with
bounds = undefined. Candidate positions outside the actual board
were never rejected, and components packed in an unbounded column
going far past the board edges. On a 50x14 mm board with a
region_mcu group at (-18, 0) holding ~10 small SMTs and one
12x12 mm chip — exactly the configuration in the linked issue —
the small siblings landed at Y = -2, -4, -6, ..., -22, entirely
off the bottom edge.

Repro

<board width="50mm" height="14mm">
  <group name="region_mcu" pcbX={-18} pcbY={0}>
    {/* large chip — 12x12 mm pad cluster */}
    <chip
      name="U_BIG"
      pinLabels={{ pin1: ["A"] }}
      footprint={
        <footprint>
          <smtpad shape="rect" width="12mm" height="12mm" pcbX="0mm" pcbY="0mm" portHints={["pin1"]} />
        </footprint>
      }
    />
    {/* nine small siblings — caps + resistors, no positions */}
    <capacitor name="C1" capacitance="100nF" footprint="0402" />
    <capacitor name="C2" capacitance="100nF" footprint="0402" />
    {/* etc. */}
  </group>
</board>

Before the fix:

U_BIG: { x: -18, y: 0 }
C1:    { x: -18, y: -1.94 }
C2:    { x: -18, y: -3.88 }
...
R5:    { x: -18, y: -22.99 }   <-- 16 mm below the board's bottom edge

The packer's SingleComponentPackSolver correctly rejects
out-of-bounds candidates when bounds is set, but in this case
the bounds argument was undefined because the inner group had no
own width/height.

Fix

When the packed group has no own width/height, walk up the
parent chain to the nearest ancestor that does (typically the
board), accumulate pcbX/pcbY offsets along the way, and translate
the ancestor's bounds into this group's local packing frame. The
packer then enforces them as it would for any explicitly-bounded
group.

} else {
  let cumulativeOffsetX = 0
  let cumulativeOffsetY = 0
  let ancestor: any = group.parent
  while (ancestor) {
    const apProps = ancestor._parsedProps
    if (typeof apProps?.pcbX === "number") cumulativeOffsetX += apProps.pcbX
    if (typeof apProps?.pcbY === "number") cumulativeOffsetY += apProps.pcbY
    if (apProps?.width !== undefined && apProps?.height !== undefined) {
      const widthMm = length.parse(apProps.width)
      const heightMm = length.parse(apProps.height)
      const groupOwnX = typeof props.pcbX === "number" ? props.pcbX : 0
      const groupOwnY = typeof props.pcbY === "number" ? props.pcbY : 0
      const totalOffsetX = cumulativeOffsetX + groupOwnX
      const totalOffsetY = cumulativeOffsetY + groupOwnY
      bounds = {
        minX: -widthMm / 2 - totalOffsetX,
        maxX:  widthMm / 2 - totalOffsetX,
        minY: -heightMm / 2 - totalOffsetY,
        maxY:  heightMm / 2 - totalOffsetY,
      }
      break
    }
    ancestor = ancestor.parent
  }
}

Total diff: ~50 source lines + a new test file.

Tests

tests/components/pcb/large-chip-off-board-repro.test.tsx mirrors
the issue's repro and asserts every resulting pcb_component
lands within the board's bounds:

for (const pcbComp of circuit.db.pcb_component.list()) {
  expect(pcbComp.center.x).toBeGreaterThanOrEqual(-25)
  expect(pcbComp.center.x).toBeLessThanOrEqual(25)
  expect(pcbComp.center.y).toBeGreaterThanOrEqual(-7)
  expect(pcbComp.center.y).toBeLessThanOrEqual(7)
}

Pre-fix: assertion fails on small siblings going off Y bound.
Post-fix: all 10 components placed within bounds.

Wider suites:

  • tests/components/pcb/ (19 tests, including the new one) — all pass
  • tests/components/primitive-components/ (150 tests) — all pass

Test plan

  • New repro test passes locally; fails on main (pre-fix) at
    the expect(...y).toBeGreaterThanOrEqual(-7) line for the
    pushed-off siblings
  • PCB / primitive-component suites green
  • CI green
  • Reviewer confirms inner-group packing of an MCU-region-style
    board no longer requires hand-pinning the chip with explicit
    pcbX/pcbY to avoid the off-board column

Related / future

The wider problem of "auto-placer doesn't always make optimal
choices when a dominant component competes with many small
siblings" is broader than this specific bug. This PR fixes the
extreme case where the packer effectively had infinite room to
work in. Smarter ordering / scoring is out of scope here.

🤖 Generated with Claude Code

…/height

Closes tscircuit#2272.

When a `<group>` runs the auto-packer on its children but doesn't
itself specify `width` and `height`, the packer was invoked with
`bounds = undefined` — so candidate positions outside the actual
board were never rejected. Components packed in an unbounded
column going far past the board edges. On a 50x14 mm board with
a `region_mcu` group at (-18, 0) holding ~10 small SMTs and one
12x12 mm chip, the small siblings ended up at Y = -2, -4, -6, ...,
-22 — entirely off the bottom edge.

Fix: when the packed group has no own `width/height`, walk up the
parent chain to the nearest ancestor that does (typically the
board), accumulate `pcbX/pcbY` offsets along the way, and translate
the ancestor's bounds into this group's local packing frame. The
packer then enforces them as it would for any explicitly-bounded
group.

Test: `tests/components/pcb/large-chip-off-board-repro.test.tsx`
reproduces the original symptom — 12x12 mm chip + 9 small SMTs in
a region group with no width/height — and asserts every resulting
pcb_component lands within the board's bounds. Before the fix the
small siblings packed at X=-18, Y down to -22.99 (16+ mm below the
board's bottom edge); after the fix every component is at
|x| <= 25 and |y| <= 7.

Wider tests/components/pcb (19 tests) and
tests/components/primitive-components (150 tests) suites still
pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tscircuit-core-benchmarks Ready Ready Preview, Comment May 8, 2026 0:27am

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-packer pushes large chip off-board when group has many small siblings

1 participant