diff --git a/lib/components/base-components/PrimitiveComponent/PrimitiveComponent.ts b/lib/components/base-components/PrimitiveComponent/PrimitiveComponent.ts index d6ff03b6f..dd5ba2fec 100644 --- a/lib/components/base-components/PrimitiveComponent/PrimitiveComponent.ts +++ b/lib/components/base-components/PrimitiveComponent/PrimitiveComponent.ts @@ -318,6 +318,22 @@ export abstract class PrimitiveComponent< return false } + /** + * Check if this component has a user-defined PCB position. + * Position can be specified via pcbX/pcbY or edge-based props. + */ + _hasUserDefinedPcbPosition(): boolean { + const props = this._parsedProps + return ( + props.pcbX !== undefined || + props.pcbY !== undefined || + props.pcbLeftEdgeX !== undefined || + props.pcbRightEdgeX !== undefined || + props.pcbTopEdgeY !== undefined || + props.pcbBottomEdgeY !== undefined + ) + } + resolvePcbCoordinate( rawValue: unknown, axis: "pcbX" | "pcbY", diff --git a/lib/components/normal-components/Subpanel.ts b/lib/components/normal-components/Subpanel.ts index 25f8fa083..9d3f024fd 100644 --- a/lib/components/normal-components/Subpanel.ts +++ b/lib/components/normal-components/Subpanel.ts @@ -1,11 +1,13 @@ import { subpanelProps } from "@tscircuit/props" import { distance } from "circuit-json" +import type { Matrix } from "transformation-matrix" +import { compose, identity, translate } from "transformation-matrix" import { DEFAULT_TAB_LENGTH, DEFAULT_TAB_WIDTH, generatePanelTabsAndMouseBites, } from "../../utils/panels/generate-panel-tabs-and-mouse-bites" -import { packBoardsIntoGrid } from "../../utils/panels/pack-boards-into-grid" +import { packIntoGrid } from "../../utils/panels/pack-into-grid" import type { PrimitiveComponent } from "../base-components/PrimitiveComponent" import { Group } from "../primitive-components/Group/Group" import { Board } from "./Board" @@ -55,6 +57,25 @@ export class Subpanel extends Group { _cachedGridWidth = 0 _cachedGridHeight = 0 + _panelPositionOffset: { x: number; y: number } | null = null + + override _computePcbGlobalTransformBeforeLayout(): Matrix { + // If we have a panel-computed offset, incorporate it into the transform + if (this._panelPositionOffset) { + // Get parent transform (typically the Panel) + const parentTransform = + this.parent?._computePcbGlobalTransformBeforeLayout?.() ?? identity() + + // Compose parent transform with panel offset translation + // The panel offset is relative to the panel center + return compose( + parentTransform, + translate(this._panelPositionOffset.x, this._panelPositionOffset.y), + ) + } + // Otherwise, fall back to the default behavior + return super._computePcbGlobalTransformBeforeLayout() + } /** * Get all board instances from this subpanel and nested subpanels @@ -99,42 +120,34 @@ export class Subpanel extends Group { if (this.root?.pcbDisabled) return const layoutMode = this._parsedProps.layoutMode ?? "none" - - const childBoardInstances = this._getDirectBoardChildren() + const gridItems = this.children.filter( + (c) => c instanceof Board || c instanceof Subpanel, + ) as (Board | Subpanel)[] // Warn if boards have manual positioning when panel layout is automatic if (layoutMode !== "none") { - for (const board of childBoardInstances) { - const hasPcbX = board._parsedProps.pcbX !== undefined - const hasPcbY = board._parsedProps.pcbY !== undefined - if (hasPcbX || hasPcbY) { - const properties = [] - if (hasPcbX) properties.push("pcbX") - if (hasPcbY) properties.push("pcbY") - const propertyNames = properties.join(" and ") - + for (const child of gridItems) { + if (!(child instanceof Board)) continue + if (child._hasUserDefinedPcbPosition()) { this.root!.db.source_property_ignored_warning.insert({ - source_component_id: board.source_component_id!, - property_name: propertyNames, - message: `Board has manual positioning (${propertyNames}) but ${this._errorComponentName} layout mode is "${layoutMode}". Manual positioning will be ignored.`, + source_component_id: child.source_component_id!, + property_name: "pcbX/pcbY", + message: `Board has manual positioning but ${this._errorComponentName} layout mode is "${layoutMode}". Manual positioning will be ignored.`, error_type: "source_property_ignored_warning", }) } } } - // Error if multiple boards without pcbX/pcbY when layoutMode is "none" - if (layoutMode === "none" && childBoardInstances.length > 1) { - const boardsWithoutPosition = childBoardInstances.filter((board) => { - const hasPcbX = board._parsedProps.pcbX !== undefined - const hasPcbY = board._parsedProps.pcbY !== undefined - return !hasPcbX && !hasPcbY - }) - - if (boardsWithoutPosition.length > 1) { + // Error if multiple items without position when layoutMode is "none" + if (layoutMode === "none" && gridItems.length > 1) { + const unpositionedItems = gridItems.filter( + (c) => !c._hasUserDefinedPcbPosition(), + ) + if (unpositionedItems.length > 1) { this.root!.db.pcb_placement_error.insert({ error_type: "pcb_placement_error", - message: `Multiple boards in ${this._errorComponentName} without pcbX/pcbY positions. When layoutMode="none", each board must have explicit pcbX and pcbY coordinates to avoid overlapping. Either set pcbX/pcbY on each board, or use layoutMode="grid" for automatic positioning.`, + message: `Multiple boards/subpanels in ${this._errorComponentName} without positions. When layoutMode="none", each item must have explicit positioning. Use layoutMode="grid" for automatic positioning.`, }) } } @@ -181,8 +194,9 @@ export class Subpanel extends Group { availablePanelHeight = panelHeight - edgePaddingTop - edgePaddingBottom } - const { positions, gridWidth, gridHeight } = packBoardsIntoGrid({ - boards: childBoardInstances, + // Pack all layoutable children into a grid + const { positions, gridWidth, gridHeight } = packIntoGrid({ + items: gridItems, row: this._parsedProps.row, col: this._parsedProps.col, cellWidth: this._parsedProps.cellWidth, @@ -195,41 +209,51 @@ export class Subpanel extends Group { this._cachedGridWidth = gridWidth this._cachedGridHeight = gridHeight - // Set subpanel position offset on each board (relative to subpanel center) - for (const { board, pos } of positions) { - board._panelPositionOffset = pos + // Set panel position offset on each item (board or subpanel) + for (const { item, pos } of positions) { + item._panelPositionOffset = pos } } doInitialPanelLayout() { if (this.root?.pcbDisabled) return const { db } = this.root! - - const childBoardInstances = this._getDirectBoardChildren() - const layoutMode = this._parsedProps.layoutMode ?? "none" if (layoutMode === "grid") { - // Update display offsets for boards (positions are already correct) - for (const board of childBoardInstances) { - if (!board.pcb_board_id || !board._panelPositionOffset) continue - db.pcb_board.update(board.pcb_board_id, { - position_mode: "relative_to_panel_anchor", - display_offset_x: `${board._panelPositionOffset.x}mm`, - display_offset_y: `${board._panelPositionOffset.y}mm`, - }) + // Update display offsets for all boards (direct and nested in subpanels) + for (const child of this.children) { + if (child instanceof Board) { + if (!child.pcb_board_id || !child._panelPositionOffset) continue + db.pcb_board.update(child.pcb_board_id, { + position_mode: "relative_to_panel_anchor", + display_offset_x: `${child._panelPositionOffset.x}mm`, + display_offset_y: `${child._panelPositionOffset.y}mm`, + }) + } else if (child instanceof Subpanel && child._panelPositionOffset) { + // Update all boards inside this subpanel with combined offset + for (const board of child._getAllBoardInstances()) { + if (!board.pcb_board_id) continue + const boardOffset = board._panelPositionOffset ?? { x: 0, y: 0 } + db.pcb_board.update(board.pcb_board_id, { + position_mode: "relative_to_panel_anchor", + display_offset_x: `${child._panelPositionOffset.x + boardOffset.x}mm`, + display_offset_y: `${child._panelPositionOffset.y + boardOffset.y}mm`, + }) + } + } } - this._updatePanelDimensions() } else { - // layoutMode is "none" or "pack" - use explicit positions + // layoutMode is "none" or "pack" - use positions relative to panel const panelGlobalPos = this._getGlobalPcbPositionBeforeLayout() - for (const board of childBoardInstances) { - const boardDb = db.pcb_board.get(board.pcb_board_id!) + for (const board of this._getDirectBoardChildren()) { + if (!board.pcb_board_id) continue + const boardDb = db.pcb_board.get(board.pcb_board_id) if (!boardDb) continue const relativeX = boardDb.center.x - panelGlobalPos.x const relativeY = boardDb.center.y - panelGlobalPos.y - db.pcb_board.update(board.pcb_board_id!, { + db.pcb_board.update(board.pcb_board_id, { position_mode: "relative_to_panel_anchor", display_offset_x: `${relativeX}mm`, display_offset_y: `${relativeY}mm`, diff --git a/lib/utils/panels/pack-boards-into-grid.ts b/lib/utils/panels/pack-boards-into-grid.ts deleted file mode 100644 index 196693565..000000000 --- a/lib/utils/panels/pack-boards-into-grid.ts +++ /dev/null @@ -1,240 +0,0 @@ -import type { CircuitJsonUtilObjects } from "@tscircuit/circuit-json-util" -import { distance } from "circuit-json" -import type { Board } from "lib/components/normal-components/Board" -import { getBoardDimensionsFromProps } from "./get-board-dimensions-from-props" - -interface PackingOptions { - row?: number - col?: number - cellWidth?: string | number - cellHeight?: string | number - boardGap: number - availablePanelWidth?: number - availablePanelHeight?: number -} - -interface BoardWithDims { - board: Board - width: number - height: number -} - -/** - * Calculate optimal grid rows and cols that fit within available panel dimensions. - * The goal is to find a grid configuration where boards don't exceed panel boundaries. - */ -function calculateOptimalGrid({ - boardsWithDims, - availableWidth, - availableHeight, - boardGap, - minCellWidth, - minCellHeight, -}: { - boardsWithDims: BoardWithDims[] - availableWidth: number - availableHeight: number - boardGap: number - minCellWidth: number - minCellHeight: number -}): { rows: number; cols: number } { - const boardCount = boardsWithDims.length - - if (boardCount === 0) { - return { rows: 0, cols: 0 } - } - - const maxBoardWidth = Math.max( - ...boardsWithDims.map((b) => b.width), - minCellWidth, - ) - const maxBoardHeight = Math.max( - ...boardsWithDims.map((b) => b.height), - minCellHeight, - ) - - const maxCols = Math.max( - 1, - Math.floor((availableWidth + boardGap) / (maxBoardWidth + boardGap)), - ) - const maxRows = Math.max( - 1, - Math.floor((availableHeight + boardGap) / (maxBoardHeight + boardGap)), - ) - - let bestCols = maxCols - let bestRows = Math.ceil(boardCount / bestCols) - - if (bestRows > maxRows) { - bestRows = maxRows - bestCols = Math.ceil(boardCount / bestRows) - - if (bestCols > maxCols) { - bestCols = maxCols - bestRows = Math.ceil(boardCount / bestCols) - } - } - - return { - rows: Math.max(1, bestRows), - cols: Math.max(1, bestCols), - } -} - -export const packBoardsIntoGrid = ({ - boards, - db, - row, - col, - cellWidth, - cellHeight, - boardGap, - availablePanelWidth, - availablePanelHeight, -}: { boards: Board[]; db?: CircuitJsonUtilObjects } & PackingOptions): { - positions: Array<{ board: Board; pos: { x: number; y: number } }> - gridWidth: number - gridHeight: number -} => { - const boardsWithDims: BoardWithDims[] = boards - .map((board) => { - let width: number | undefined - let height: number | undefined - - // Try to get dimensions from database if available - if (db && board.pcb_board_id) { - const pcbBoard = db.pcb_board.get(board.pcb_board_id) - if (pcbBoard?.width !== undefined && pcbBoard?.height !== undefined) { - width = pcbBoard.width - height = pcbBoard.height - } - } - - // Fall back to props-based dimensions - if (width === undefined || height === undefined) { - const propsDims = getBoardDimensionsFromProps(board) - width = propsDims.width - height = propsDims.height - } - - // Skip boards with unknown dimensions - if (width === 0 && height === 0) { - return null - } - - return { board, width, height } - }) - .filter((b): b is BoardWithDims => b !== null) - - if (boardsWithDims.length === 0) { - return { - positions: [], - gridWidth: 0, - gridHeight: 0, - } - } - - const explicitRow = row - const explicitCol = col - - let cols: number - let rows: number - - if (explicitCol !== undefined) { - cols = explicitCol - rows = explicitRow ?? Math.ceil(boardsWithDims.length / cols) - } else if (explicitRow !== undefined) { - rows = explicitRow - cols = Math.ceil(boardsWithDims.length / rows) - } else if ( - availablePanelWidth !== undefined && - availablePanelHeight !== undefined - ) { - const result = calculateOptimalGrid({ - boardsWithDims, - availableWidth: availablePanelWidth, - availableHeight: availablePanelHeight, - boardGap, - minCellWidth: cellWidth ? distance.parse(cellWidth) : 0, - minCellHeight: cellHeight ? distance.parse(cellHeight) : 0, - }) - cols = result.cols - rows = result.rows - } else { - cols = Math.ceil(Math.sqrt(boardsWithDims.length)) - rows = Math.ceil(boardsWithDims.length / cols) - } - - // Initialize column widths and row heights to 0 - const colWidths = Array(cols).fill(0) - const rowHeights = Array(rows).fill(0) - - // Determine the max width for each column and max height for each row - boardsWithDims.forEach((b, i) => { - const colIdx = i % cols - const rowIdx = Math.floor(i / cols) - if (rowIdx < rowHeights.length && b.height > rowHeights[rowIdx]) { - rowHeights[rowIdx] = b.height - } - if (colIdx < colWidths.length && b.width > colWidths[colIdx]) { - colWidths[colIdx] = b.width - } - }) - - // Apply cellWidth and cellHeight as minimums - const minCellWidth = cellWidth ? distance.parse(cellWidth) : 0 - const minCellHeight = cellHeight ? distance.parse(cellHeight) : 0 - - for (let i = 0; i < colWidths.length; i++) { - colWidths[i] = Math.max(colWidths[i], minCellWidth) - } - for (let i = 0; i < rowHeights.length; i++) { - rowHeights[i] = Math.max(rowHeights[i], minCellHeight) - } - - const totalGridWidth = - colWidths.reduce((a, b) => a + b, 0) + - (cols > 1 ? (cols - 1) * boardGap : 0) - const totalGridHeight = - rowHeights.reduce((a, b) => a + b, 0) + - (rows > 1 ? (rows - 1) * boardGap : 0) - - const startX = -totalGridWidth / 2 - const startY = -totalGridHeight / 2 - - const rowYOffsets = [startY] - for (let i = 1; i < rows; i++) { - rowYOffsets.push(rowYOffsets[i - 1] + rowHeights[i - 1] + boardGap) - } - - const colXOffsets = [startX] - for (let i = 1; i < cols; i++) { - colXOffsets.push(colXOffsets[i - 1] + colWidths[i - 1] + boardGap) - } - - const positions: Array<{ board: Board; pos: { x: number; y: number } }> = [] - - boardsWithDims.forEach((b, i) => { - const colIdx = i % cols - const rowIdx = Math.floor(i / cols) - - if (rowIdx >= rowYOffsets.length || colIdx >= colXOffsets.length) return - - const cellX = colXOffsets[colIdx] - const cellY = rowYOffsets[rowIdx] - - const currentCellWidth = colWidths[colIdx] - const currentCellHeight = rowHeights[rowIdx] - - // Center the board within its dynamic cell - const boardX = cellX + currentCellWidth / 2 - const boardY = cellY + currentCellHeight / 2 - - positions.push({ - board: b.board, - pos: { x: boardX, y: boardY }, - }) - }) - - return { positions, gridWidth: totalGridWidth, gridHeight: totalGridHeight } -} diff --git a/lib/utils/panels/pack-into-grid.ts b/lib/utils/panels/pack-into-grid.ts new file mode 100644 index 000000000..9e94f86df --- /dev/null +++ b/lib/utils/panels/pack-into-grid.ts @@ -0,0 +1,253 @@ +import type { CircuitJsonUtilObjects } from "@tscircuit/circuit-json-util" +import { distance } from "circuit-json" +import type { Board } from "lib/components/normal-components/Board" +import type { Subpanel } from "lib/components/normal-components/Subpanel" +import { getBoardDimensionsFromProps } from "./get-board-dimensions-from-props" + +export type LayoutableItem = Board | Subpanel + +export interface GridPackingOptions { + row?: number + col?: number + cellWidth?: string | number + cellHeight?: string | number + boardGap: number + availablePanelWidth?: number + availablePanelHeight?: number +} + +/** + * Pack an array of layoutable items (Boards or Subpanels) into a grid. + * Returns positions for each item relative to the grid center. + */ +export function packIntoGrid({ + items, + db, + row, + col, + cellWidth, + cellHeight, + boardGap, + availablePanelWidth, + availablePanelHeight, +}: { + items: LayoutableItem[] + db?: CircuitJsonUtilObjects +} & GridPackingOptions): { + positions: Array<{ item: LayoutableItem; pos: { x: number; y: number } }> + gridWidth: number + gridHeight: number +} { + // Get dimensions for each item + const itemsWithDims = items + .map((item) => { + const dims = getItemDimensions(item, db) + return { item, width: dims.width, height: dims.height } + }) + .filter((item) => !(item.width === 0 && item.height === 0)) + + if (itemsWithDims.length === 0) { + return { positions: [], gridWidth: 0, gridHeight: 0 } + } + + // Determine grid dimensions + let cols: number + let rows: number + const minCellWidth = cellWidth ? distance.parse(cellWidth) : 0 + const minCellHeight = cellHeight ? distance.parse(cellHeight) : 0 + + if (col !== undefined) { + cols = col + rows = row ?? Math.ceil(itemsWithDims.length / cols) + } else if (row !== undefined) { + rows = row + cols = Math.ceil(itemsWithDims.length / rows) + } else if ( + availablePanelWidth !== undefined && + availablePanelHeight !== undefined + ) { + // Calculate optimal grid to fit within available space + const maxItemWidth = Math.max( + ...itemsWithDims.map((b) => b.width), + minCellWidth, + ) + const maxItemHeight = Math.max( + ...itemsWithDims.map((b) => b.height), + minCellHeight, + ) + + const maxCols = Math.max( + 1, + Math.floor((availablePanelWidth + boardGap) / (maxItemWidth + boardGap)), + ) + const maxRows = Math.max( + 1, + Math.floor( + (availablePanelHeight + boardGap) / (maxItemHeight + boardGap), + ), + ) + + cols = maxCols + rows = Math.ceil(itemsWithDims.length / cols) + + if (rows > maxRows) { + rows = maxRows + cols = Math.ceil(itemsWithDims.length / rows) + if (cols > maxCols) { + cols = maxCols + rows = Math.ceil(itemsWithDims.length / cols) + } + } + + cols = Math.max(1, cols) + rows = Math.max(1, rows) + } else { + cols = Math.ceil(Math.sqrt(itemsWithDims.length)) + rows = Math.ceil(itemsWithDims.length / cols) + } + + // Calculate column widths and row heights + const colWidths = Array(cols).fill(0) + const rowHeights = Array(rows).fill(0) + + itemsWithDims.forEach((item, i) => { + const colIdx = i % cols + const rowIdx = Math.floor(i / cols) + if (rowIdx < rowHeights.length && item.height > rowHeights[rowIdx]) { + rowHeights[rowIdx] = item.height + } + if (colIdx < colWidths.length && item.width > colWidths[colIdx]) { + colWidths[colIdx] = item.width + } + }) + + // Apply minimum cell dimensions + for (let i = 0; i < colWidths.length; i++) { + colWidths[i] = Math.max(colWidths[i], minCellWidth) + } + for (let i = 0; i < rowHeights.length; i++) { + rowHeights[i] = Math.max(rowHeights[i], minCellHeight) + } + + // Calculate total grid size + const gridWidth = + colWidths.reduce((a, b) => a + b, 0) + + (cols > 1 ? (cols - 1) * boardGap : 0) + const gridHeight = + rowHeights.reduce((a, b) => a + b, 0) + + (rows > 1 ? (rows - 1) * boardGap : 0) + + // Calculate offsets (grid centered at origin) + const startX = -gridWidth / 2 + const startY = -gridHeight / 2 + + const colXOffsets = [startX] + for (let i = 1; i < cols; i++) { + colXOffsets.push(colXOffsets[i - 1] + colWidths[i - 1] + boardGap) + } + + const rowYOffsets = [startY] + for (let i = 1; i < rows; i++) { + rowYOffsets.push(rowYOffsets[i - 1] + rowHeights[i - 1] + boardGap) + } + + // Calculate positions for each item + const positions: Array<{ + item: LayoutableItem + pos: { x: number; y: number } + }> = [] + + itemsWithDims.forEach((itemWithDims, i) => { + const colIdx = i % cols + const rowIdx = Math.floor(i / cols) + + if (rowIdx >= rowYOffsets.length || colIdx >= colXOffsets.length) return + + // Center item within its cell + const x = colXOffsets[colIdx] + colWidths[colIdx] / 2 + const y = rowYOffsets[rowIdx] + rowHeights[rowIdx] / 2 + + positions.push({ item: itemWithDims.item, pos: { x, y } }) + }) + + return { positions, gridWidth, gridHeight } +} + +/** + * Get dimensions of a Board or Subpanel + */ +function getItemDimensions( + item: LayoutableItem, + db?: CircuitJsonUtilObjects, +): { width: number; height: number } { + if (item.componentName === "Board") { + const board = item as Board + if (db && board.pcb_board_id) { + const pcbBoard = db.pcb_board.get(board.pcb_board_id) + if (pcbBoard?.width !== undefined && pcbBoard?.height !== undefined) { + return { width: pcbBoard.width, height: pcbBoard.height } + } + } + return getBoardDimensionsFromProps(board) + } + + if (item.componentName === "Subpanel") { + const subpanel = item as Subpanel + const props = subpanel._parsedProps + + if (props.width !== undefined && props.height !== undefined) { + return { + width: distance.parse(props.width), + height: distance.parse(props.height), + } + } + + const directBoards = subpanel._getDirectBoardChildren() + + if (directBoards.length === 0) { + const allBoards = subpanel._getAllBoardInstances() + if (allBoards.length === 0) return { width: 0, height: 0 } + + let minX = Infinity + let minY = Infinity + let maxX = -Infinity + let maxY = -Infinity + + for (const board of allBoards) { + const dims = getBoardDimensionsFromProps(board) + if (dims.width === 0 || dims.height === 0) continue + const offset = board._panelPositionOffset ?? { x: 0, y: 0 } + minX = Math.min(minX, offset.x - dims.width / 2) + maxX = Math.max(maxX, offset.x + dims.width / 2) + minY = Math.min(minY, offset.y - dims.height / 2) + maxY = Math.max(maxY, offset.y + dims.height / 2) + } + + if (minX === Infinity) return { width: 0, height: 0 } + return { width: maxX - minX, height: maxY - minY } + } + + if (directBoards.length === 1) { + return getBoardDimensionsFromProps(directBoards[0]) + } + + if (subpanel._cachedGridWidth > 0 && subpanel._cachedGridHeight > 0) { + const edgePadding = distance.parse(props.edgePadding ?? 5) + return { + width: subpanel._cachedGridWidth + edgePadding * 2, + height: subpanel._cachedGridHeight + edgePadding * 2, + } + } + + let totalWidth = 0 + let totalHeight = 0 + for (const board of directBoards) { + const dims = getBoardDimensionsFromProps(board) + totalWidth = Math.max(totalWidth, dims.width) + totalHeight = Math.max(totalHeight, dims.height) + } + return { width: totalWidth, height: totalHeight } + } + + return { width: 0, height: 0 } +} diff --git a/package.json b/package.json index 724bc5dce..27ed0807e 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@tscircuit/alphabet": "0.0.18", "@tscircuit/capacity-autorouter": "^0.0.299", "@tscircuit/checks": "0.0.100", - "@tscircuit/circuit-json-util": "^0.0.80", + "@tscircuit/circuit-json-util": "^0.0.81", "@tscircuit/common": "^0.0.20", "@tscircuit/copper-pour-solver": "^0.0.20", "@tscircuit/footprinter": "^0.0.316", diff --git a/tests/repros/__snapshots__/repro97-subpanel-grid-pcb.snap.svg b/tests/repros/__snapshots__/repro97-subpanel-grid-pcb.snap.svg new file mode 100644 index 000000000..c87ab01cc --- /dev/null +++ b/tests/repros/__snapshots__/repro97-subpanel-grid-pcb.snap.svg @@ -0,0 +1 @@ +A1A2B1A3B2A4 \ No newline at end of file diff --git a/tests/repros/repro83-panel-group-pcbxy-conflict.test.tsx b/tests/repros/repro83-panel-group-pcbxy-conflict.test.tsx index c2721fa1a..46e2ca41c 100644 --- a/tests/repros/repro83-panel-group-pcbxy-conflict.test.tsx +++ b/tests/repros/repro83-panel-group-pcbxy-conflict.test.tsx @@ -1,4 +1,4 @@ -import { test, expect } from "bun:test" +import { expect, test } from "bun:test" import { getTestFixture } from "tests/fixtures/get-test-fixture" test("repro83: panel with multiple boards without pcbX/pcbY and layoutMode=none should produce pcb_placement_error", () => { @@ -20,7 +20,7 @@ test("repro83: panel with multiple boards without pcbX/pcbY and layoutMode=none const errors = circuit.db.pcb_placement_error.list() expect(errors.length).toBe(1) expect(errors[0].message).toContain( - "Multiple boards in panel without pcbX/pcbY positions", + "Multiple boards/subpanels in panel without positions", ) expect(errors[0].message).toContain('layoutMode="none"') expect(errors[0].message).toContain('layoutMode="grid"') diff --git a/tests/repros/repro97-subpanel-grid.test.tsx b/tests/repros/repro97-subpanel-grid.test.tsx new file mode 100644 index 000000000..520a72bce --- /dev/null +++ b/tests/repros/repro97-subpanel-grid.test.tsx @@ -0,0 +1,111 @@ +import { expect, test } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +// Simple board with corner holes and a center note +const SmallBoard = ({ label }: { label: string }) => ( + + + + + + + +) + +// Wider board variant +const WideBoard = ({ label }: { label: string }) => ( + + + + + + + +) + +test("subpanels in grid layout", async () => { + const { circuit } = getTestFixture() + + circuit.add( + + + + + + + + + + + + + + + + + + + + , + ) + + await circuit.renderUntilSettled() + + expect(circuit).toMatchPcbSnapshot(import.meta.path) +})