Summary
manualEdits.pcb_placements lands a manually-placed component at the wrong absolute position when its enclosing <group> has a non-zero pcbX/pcbY. The group anchor gets applied twice — once when computing the parent's transform, again via translate(manualPlacement.x, manualPlacement.y) — even though manualPlacement is already in subcircuit-absolute coordinates.
Repro
<board width="40mm" height="20mm" manualEdits={{
pcb_placements: [
{ selector: "C1", center: { x: 11.4, y: 4.3 }, relative_to: "group_center" },
],
}}>
<group name="region" pcbX={16} pcbY={0}>
<chip name="U1" footprint="soic8" pinLabels={{ pin1: "VCC", pin8: "GND" }} />
<capacitor name="C1" capacitance="100nF" footprint="0402"
connections={{ pin1: "U1.VCC", pin2: "U1.GND" }} />
</group>
</board>
Expected: C1.center = (11.4, 4.3) — the user dragged it there.
Actual: C1.center = (27.4, 4.3). The +16 region anchor was added on top of the manual position.
(With the static-marking fix from #TBD applied, the bug becomes more visible: C1.center = (43.4, 4.3) — the anchor is added a third time by applyPackOutput. Without the static fix, the packer overwrites the wrong position with its own different wrong position, masking this bug.)
Cause
In lib/components/base-components/PrimitiveComponent.ts's _computePcbGlobalTransformBeforeLayout:
const manualPlacement = this.getSubcircuit()._getPcbManualPlacementForComponent(this)
if (manualPlacement && this.props.pcbX === undefined && this.props.pcbY === undefined) {
return compose(
this.parent?._computePcbGlobalTransformBeforeLayout() ?? identity(),
compose(
translate(manualPlacement.x, manualPlacement.y),
rotate(rotation * Math.PI / 180),
),
)
}
manualPlacement comes from _getPcbManualPlacementForComponent:
const center = applyToPoint(
this._computePcbGlobalTransformBeforeLayout(), // subcircuit's transform
position.center,
)
return center
For a board subcircuit (transform = identity), the returned center is just position.center — the value the user wrote into manual-edits.json, which the IDE recorded in board-absolute coordinates. So manualPlacement is already in the same frame as the final intended position.
But then _computePcbGlobalTransformBeforeLayout composes it on top of the parent group's transform. For a chip directly under the board (parent = board, transform = identity), this happens to work. For a component inside a positioned group, the parent's translate is applied AND the absolute manual position is applied as a translate, double-counting the group anchor.
Fix sketch
When a manualPlacement exists, the component's transform should be the manualPlacement directly, NOT composed with the parent's transform:
if (manualPlacement && this.props.pcbX === undefined && this.props.pcbY === undefined) {
const rotation = this._getPcbRotationBeforeLayout() ?? 0
return compose(
translate(manualPlacement.x, manualPlacement.y),
rotate(rotation * Math.PI / 180),
)
}
(The same applies to the schematic equivalent at line ~1815 if it has the same shape.)
Impact
Anyone using the IDE drag-and-pin flow for components inside positioned regions sees their drags land at the wrong place after rebuild. This is the dominant pattern for any board with multiple chip regions — every cap, resistor, etc. inside a regional group becomes mis-placed.
The companion fix #TBD (this PR's parent issue) makes the packer respect manualEdits placements; this issue is what makes those placements land where the user actually dragged them. Both are needed for the IDE drag workflow to be correct end-to-end.
🤖 Generated with Claude Code
Summary
manualEdits.pcb_placementslands a manually-placed component at the wrong absolute position when its enclosing<group>has a non-zeropcbX/pcbY. The group anchor gets applied twice — once when computing the parent's transform, again viatranslate(manualPlacement.x, manualPlacement.y)— even thoughmanualPlacementis already in subcircuit-absolute coordinates.Repro
Expected:
C1.center = (11.4, 4.3)— the user dragged it there.Actual:
C1.center = (27.4, 4.3). The +16 region anchor was added on top of the manual position.(With the static-marking fix from #TBD applied, the bug becomes more visible:
C1.center = (43.4, 4.3)— the anchor is added a third time byapplyPackOutput. Without the static fix, the packer overwrites the wrong position with its own different wrong position, masking this bug.)Cause
In
lib/components/base-components/PrimitiveComponent.ts's_computePcbGlobalTransformBeforeLayout:manualPlacementcomes from_getPcbManualPlacementForComponent:For a board subcircuit (transform = identity), the returned
centeris justposition.center— the value the user wrote intomanual-edits.json, which the IDE recorded in board-absolute coordinates. SomanualPlacementis already in the same frame as the final intended position.But then
_computePcbGlobalTransformBeforeLayoutcomposes it on top of the parent group's transform. For a chip directly under the board (parent = board, transform = identity), this happens to work. For a component inside a positioned group, the parent's translate is applied AND the absolute manual position is applied as a translate, double-counting the group anchor.Fix sketch
When a manualPlacement exists, the component's transform should be the manualPlacement directly, NOT composed with the parent's transform:
(The same applies to the schematic equivalent at line ~1815 if it has the same shape.)
Impact
Anyone using the IDE drag-and-pin flow for components inside positioned regions sees their drags land at the wrong place after rebuild. This is the dominant pattern for any board with multiple chip regions — every cap, resistor, etc. inside a regional group becomes mis-placed.
The companion fix #TBD (this PR's parent issue) makes the packer respect
manualEditsplacements; this issue is what makes those placements land where the user actually dragged them. Both are needed for the IDE drag workflow to be correct end-to-end.🤖 Generated with Claude Code