Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions lib/PackSolver/PhasedPackSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,15 +749,64 @@ export class PhasedPackSolver extends BaseSolver {
selectedComponent.pads = bestTrial.pads
this.phaseData.selectedRotation = selectedComponent
} else if (rotationTrials.length > 0) {
// If no valid trials without overlap, pick the best overlapping one
// If no valid trials without overlap, find the best trial and push it further away
const bestTrial = rotationTrials.reduce((best, current) =>
current.cost < best.cost ? current : best,
)

// Calculate center of mass of packed components to determine direction to move away
const packedCenterX =
this.packedComponents.length > 0
? this.packedComponents.reduce((sum, c) => sum + c.center.x, 0) /
this.packedComponents.length
: 0
const packedCenterY =
this.packedComponents.length > 0
? this.packedComponents.reduce((sum, c) => sum + c.center.y, 0) /
this.packedComponents.length
: 0

// Calculate direction vector from packed center to best trial position
const dirX = bestTrial.center.x - packedCenterX
const dirY = bestTrial.center.y - packedCenterY
const dirLength = Math.sqrt(dirX * dirX + dirY * dirY)

// Normalize direction vector and push component further out
// Use a multiple of the component's largest dimension to ensure sufficient separation
const componentBounds = getComponentBounds(newPackedComponent)
const componentWidth = componentBounds.maxX - componentBounds.minX
const componentHeight = componentBounds.maxY - componentBounds.minY
const componentSize = Math.max(componentWidth, componentHeight)
// Calculate push distance: component size + gap + safety margin for body bounds
const componentSizeMultiplier = 2.0 // How many component sizes to push away
const safetyMargin = 2.0 // Additional margin for body bounds (mm)
const pushDistance =
componentSize * componentSizeMultiplier +
(this.packInput.minGap ?? 0) +
safetyMargin

let newX = bestTrial.center.x
let newY = bestTrial.center.y

if (dirLength > 0) {
const normalizedDirX = dirX / dirLength
const normalizedDirY = dirY / dirLength
newX = packedCenterX + normalizedDirX * (dirLength + pushDistance)
newY = packedCenterY + normalizedDirY * (dirLength + pushDistance)
} else {
// If at same position, push in a default direction
newX = bestTrial.center.x + pushDistance
newY = bestTrial.center.y
}

const selectedComponent = { ...newPackedComponent }
selectedComponent.center = bestTrial.center
selectedComponent.center = { x: newX, y: newY }
selectedComponent.ccwRotationOffset = bestTrial.ccwRotationOffset
selectedComponent.pads = bestTrial.pads

// Recalculate pad centers with new position
setPackedComponentPadCenters(selectedComponent)

this.phaseData.selectedRotation = selectedComponent
} else {
this.phaseData.selectedRotation = undefined
Expand Down
154 changes: 129 additions & 25 deletions lib/PackSolver/checkOverlapWithPackedComponents.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,155 @@
import type { PackedComponent } from "../types"
import type { PackedComponent, ComponentBodyBounds } from "../types"
import { transformComponentBodyBounds } from "./transformComponentBodyBounds"

export interface CheckOverlapWithPackedComponentsParams {
component: PackedComponent
packedComponents: PackedComponent[]
minGap: number
}

interface RectBounds {
left: number
right: number
bottom: number
top: number
}

// Helper function to check rectangle overlap
function checkRectOverlap(
rect1: ComponentBodyBounds | RectBounds,
rect2: ComponentBodyBounds | RectBounds,
): boolean {
const r1 =
"minX" in rect1
? {
left: rect1.minX,
right: rect1.maxX,
bottom: rect1.minY,
top: rect1.maxY,
}
: rect1
const r2 =
"minX" in rect2
? {
left: rect2.minX,
right: rect2.maxX,
bottom: rect2.minY,
top: rect2.maxY,
}
: rect2

return (
r1.right >= r2.left &&
r2.right >= r1.left &&
r1.top >= r2.bottom &&
r2.top >= r1.bottom
)
}

// Helper function to create pad bounds
function getPadBounds(pad: {
absoluteCenter: { x: number; y: number }
size: { x: number; y: number }
}): RectBounds {
return {
left: pad.absoluteCenter.x - pad.size.x / 2,
right: pad.absoluteCenter.x + pad.size.x / 2,
bottom: pad.absoluteCenter.y - pad.size.y / 2,
top: pad.absoluteCenter.y + pad.size.y / 2,
}
}

export function checkOverlapWithPackedComponents({
component,
packedComponents,
minGap,
}: CheckOverlapWithPackedComponentsParams): boolean {
// Use proper rectangle-to-rectangle collision detection
for (const componentPad of component.pads) {
// Cache the transformed body bounds for the component being placed
const comp1Body = component.bodyBounds
? transformComponentBodyBounds(component)
: null

// Check body bounds overlaps if available
if (comp1Body) {
for (const packedComponent of packedComponents) {
for (const packedPad of packedComponent.pads) {
// Calculate rectangle bounds
const comp1Bounds = {
left: componentPad.absoluteCenter.x - componentPad.size.x / 2,
right: componentPad.absoluteCenter.x + componentPad.size.x / 2,
bottom: componentPad.absoluteCenter.y - componentPad.size.y / 2,
top: componentPad.absoluteCenter.y + componentPad.size.y / 2,
}
// Cache the transformed body bounds for each packed component
const comp2Body = packedComponent.bodyBounds
? transformComponentBodyBounds(packedComponent)
: null

const comp2Bounds = {
left: packedPad.absoluteCenter.x - packedPad.size.x / 2,
right: packedPad.absoluteCenter.x + packedPad.size.x / 2,
bottom: packedPad.absoluteCenter.y - packedPad.size.y / 2,
top: packedPad.absoluteCenter.y + packedPad.size.y / 2,
if (comp2Body) {
// Check for body-to-body rectangle overlap
if (checkRectOverlap(comp1Body, comp2Body)) {
return true // Component body overlap detected
}

// Check for rectangle overlap (rectangles overlap if they overlap in BOTH X and Y)
// Check if body gap is sufficient (only if no overlap)
const xOverlap =
comp1Bounds.right > comp2Bounds.left &&
comp2Bounds.right > comp1Bounds.left
comp1Body.maxX >= comp2Body.minX && comp2Body.maxX >= comp1Body.minX
const yOverlap =
comp1Bounds.top > comp2Bounds.bottom &&
comp2Bounds.top > comp1Bounds.bottom
comp1Body.maxY >= comp2Body.minY && comp2Body.maxY >= comp1Body.minY

if (!xOverlap || !yOverlap) {
const xGap = xOverlap
? 0
: Math.min(
Math.abs(comp1Body.minX - comp2Body.maxX),
Math.abs(comp2Body.minX - comp1Body.maxX),
)
const yGap = yOverlap
? 0
: Math.min(
Math.abs(comp1Body.minY - comp2Body.maxY),
Math.abs(comp2Body.minY - comp1Body.maxY),
)

if (xOverlap && yOverlap) {
return true // Actual rectangle overlap detected
const actualGap = Math.max(xGap, yGap)
if (actualGap < minGap) {
return true // Insufficient body gap
}
}
}

// Check if this component's body overlaps with the packed component's pads
for (const packedPad of packedComponent.pads) {
if (checkRectOverlap(comp1Body, getPadBounds(packedPad))) {
return true // Component body overlapping with pad
}
}

// Check if the packed component's body overlaps with this component's pads
if (comp2Body) {
for (const componentPad of component.pads) {
if (checkRectOverlap(comp2Body, getPadBounds(componentPad))) {
return true // Packed component body overlapping with this component's pad
}
}
}
}
}

// Finally check pad-to-pad overlap as before
for (const componentPad of component.pads) {
const comp1Bounds = getPadBounds(componentPad)

for (const packedComponent of packedComponents) {
for (const packedPad of packedComponent.pads) {
const comp2Bounds = getPadBounds(packedPad)

// Check for rectangle overlap
if (checkRectOverlap(comp1Bounds, comp2Bounds)) {
return true // Pad overlap detected
}

// If no overlap, check if gap is sufficient
const xOverlap =
comp1Bounds.right >= comp2Bounds.left &&
comp2Bounds.right >= comp1Bounds.left
const yOverlap =
comp1Bounds.top >= comp2Bounds.bottom &&
comp2Bounds.top >= comp1Bounds.bottom

if (!xOverlap || !yOverlap) {
// Calculate minimum gap in both dimensions
const xGap = xOverlap
? 0
: Math.min(
Expand All @@ -58,7 +163,6 @@ export function checkOverlapWithPackedComponents({
Math.abs(comp2Bounds.bottom - comp1Bounds.top),
)

// The actual gap is the minimum of the non-overlapping dimensions
const actualGap = Math.max(xGap, yGap)
if (actualGap < minGap) {
return true // Insufficient gap
Expand Down
68 changes: 68 additions & 0 deletions lib/PackSolver/transformComponentBodyBounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { PackedComponent, ComponentBodyBounds } from "../types"

/**
* Rotate a point around the origin by the given angle (in radians)
*/
function rotatePoint(point: { x: number; y: number }, angleRad: number) {
const cos = Math.cos(angleRad)
const sin = Math.sin(angleRad)
return {
x: point.x * cos - point.y * sin,
y: point.x * sin + point.y * cos,
}
}

/**
* Transform component body bounds to account for rotation and translation.
* This mirrors the transformation logic used for pad positions.
*/
export function transformComponentBodyBounds(
component: PackedComponent,
): ComponentBodyBounds | null {
if (!component.bodyBounds) {
return null
}

const originalBounds = component.bodyBounds
const rotationAngle =
((component.ccwRotationDegrees ?? component.ccwRotationOffset ?? 0) *
Math.PI) /
180

// Get the four corners of the original body bounds rectangle
const corners = [
{ x: originalBounds.minX, y: originalBounds.minY }, // Bottom-left
{ x: originalBounds.maxX, y: originalBounds.minY }, // Bottom-right
{ x: originalBounds.maxX, y: originalBounds.maxY }, // Top-right
{ x: originalBounds.minX, y: originalBounds.maxY }, // Top-left
]

// Transform each corner: rotate around origin, then translate to component center
const transformedCorners = corners.map((corner) => {
const rotatedCorner = rotatePoint(corner, rotationAngle)
return {
x: component.center.x + rotatedCorner.x,
y: component.center.y + rotatedCorner.y,
}
})

// Find the axis-aligned bounding box of the transformed corners
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity

for (const corner of transformedCorners) {
minX = Math.min(minX, corner.x)
maxX = Math.max(maxX, corner.x)
minY = Math.min(minY, corner.y)
maxY = Math.max(maxY, corner.y)
}

return {
minX,
maxX,
minY,
maxY,
}
}
42 changes: 42 additions & 0 deletions lib/plumbing/convertCircuitJsonToPackOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,53 @@ const buildPackedComponent = (
},
}))

/* ----- extract component body bounds from silkscreen ----- */
let bodyBounds
try {
let bodyMinX = Infinity
let bodyMinY = Infinity
let bodyMaxX = -Infinity
let bodyMaxY = -Infinity

// Look for silkscreen paths for this component
for (const pc of pcbComponents) {
const silkscreenPaths = db.pcb_silkscreen_path.list({
pcb_component_id: pc.pcb_component_id,
})

for (const path of silkscreenPaths) {
if (path.route && Array.isArray(path.route)) {
for (const point of path.route) {
if (typeof point.x === "number" && typeof point.y === "number") {
bodyMinX = Math.min(bodyMinX, point.x)
bodyMaxX = Math.max(bodyMaxX, point.x)
bodyMinY = Math.min(bodyMinY, point.y)
bodyMaxY = Math.max(bodyMaxY, point.y)
}
}
}
}
}

// Only set bodyBounds if we found valid silkscreen data
if (bodyMinX !== Infinity) {
bodyBounds = {
minX: bodyMinX,
maxX: bodyMaxX,
minY: bodyMinY,
maxY: bodyMaxY,
}
}
} catch (error) {
// Silently fall back to pad-only detection if we can't extract body bounds
}

return {
componentId,
center,
ccwRotationOffset: 0,
pads,
bodyBounds,
} as PackedComponent
}

Expand Down
1 change: 1 addition & 0 deletions lib/plumbing/convertPackOutputToPackInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const convertPackOutputToPackInput = (packed: PackOutput): PackInput => {
const strippedComponents = packed.components.map((pc) => ({
componentId: pc.componentId,
availableRotationDegrees: pc.availableRotationDegrees, // Preserve rotation constraints
bodyBounds: pc.bodyBounds, // Preserve component body bounds for overlap detection
pads: pc.pads.map(({ absoluteCenter: _ac, ...rest }) => rest),
}))

Expand Down
Loading