Skip to content

Commit

Permalink
Merge pull request #108 from tscircuit/layerflip
Browse files Browse the repository at this point in the history
Support Flipping Components to Bottom Layer
  • Loading branch information
seveibar authored Sep 22, 2024
2 parents 3fadd89 + 1012081 commit b9f8cc6
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 24 deletions.
7 changes: 6 additions & 1 deletion lib/Circuit.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AnySoupElement } from "@tscircuit/soup"
import type { LayerRef } from "circuit-json"
import type { PrimitiveComponent } from "./components/base-components/PrimitiveComponent"
import type { SoupUtilObjects } from "@tscircuit/soup-util"
import { su } from "@tscircuit/soup-util"
Expand Down Expand Up @@ -36,11 +37,15 @@ export class Circuit {
/**
* Get the main board for this Circuit.
*/
_getBoard(): PrimitiveComponent & { boardThickness: number } {
_getBoard(): PrimitiveComponent & {
boardThickness: number
allLayers: LayerRef[]
} {
return this.children.find(
(c) => c.componentName === "Board",
) as PrimitiveComponent & {
boardThickness: number
allLayers: LayerRef[]
}
}

Expand Down
8 changes: 0 additions & 8 deletions lib/components/base-components/NormalComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,6 @@ export class NormalComponent<
if (!footprint) return
if (typeof footprint === "string") {
const fpSoup = fp.string(footprint).soup()
// Adjust the layer of the footprint to match the layer of the component
if (this.props.layer) {
for (const elm of fpSoup) {
if ("layer" in elm) {
elm.layer = this.props.layer
}
}
}
const fpComponents = createComponentsFromSoup(fpSoup)
this.addAll(fpComponents)
}
Expand Down
61 changes: 59 additions & 2 deletions lib/components/base-components/PrimitiveComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import { Renderable, type RenderPhase } from "./Renderable"
import {
applyToPoint,
compose,
flipY,
identity,
rotate,
translate,
type Matrix,
} from "transformation-matrix"
import { isMatchingSelector } from "lib/utils/selector-matching"
import type { LayoutBuilder } from "@tscircuit/layout"
import type { LayerRef } from "circuit-json"

export interface BaseComponentConfig {
schematicSymbolName?: BaseSymbolName | null
Expand Down Expand Up @@ -154,6 +156,30 @@ export abstract class PrimitiveComponent<
)
}

// If this is a primitive, and the parent primitive container is flipped,
// we flip it's position
if (this.isPcbPrimitive) {
const primitiveContainer = this.getPrimitiveContainer()
if (primitiveContainer) {
const isFlipped = primitiveContainer._parsedProps.layer === "bottom"
const containerCenter =
primitiveContainer._getGlobalPcbPositionBeforeLayout()

if (isFlipped) {
const flipOperation = compose(
translate(containerCenter.x, containerCenter.y),
flipY(),
translate(-containerCenter.x, -containerCenter.y),
)
return compose(
this.parent?._computePcbGlobalTransformBeforeLayout() ?? identity(),
flipY(),
this.computePcbPropsTransform(),
)
}
}
}

return compose(
this.parent?._computePcbGlobalTransformBeforeLayout() ?? identity(),
this.computePcbPropsTransform(),
Expand Down Expand Up @@ -183,6 +209,30 @@ export abstract class PrimitiveComponent<
}
}

/**
* Determine if this pcb primitive should be flipped because the primitive
* container is flipped
*
* TODO use footprint.originalLayer instead of assuming everything is defined
* relative to the top layer
*/
_getPcbPrimitiveFlippedHelpers(): {
isFlipped: boolean
maybeFlipLayer: (layer: LayerRef) => LayerRef
} {
const container = this.getPrimitiveContainer()
const isFlipped = !container
? false
: container._parsedProps.layer === "bottom"
const maybeFlipLayer = (layer: LayerRef) => {
if (isFlipped) {
return layer === "top" ? "bottom" : "top"
}
return layer
}
return { isFlipped, maybeFlipLayer }
}

/**
* Set the position of this component from the layout solver. This method
* should operate using CircuitJson associated with this component, like
Expand Down Expand Up @@ -424,9 +474,16 @@ export abstract class PrimitiveComponent<

getAvailablePcbLayers(): string[] {
if (this.isPcbPrimitive) {
if (this.props.layer) return [this.props.layer]
const { maybeFlipLayer } = this._getPcbPrimitiveFlippedHelpers()
if ("layer" in this._parsedProps) {
const layer = maybeFlipLayer(this._parsedProps.layer ?? "top")
return [layer]
}
if ("layers" in this._parsedProps) {
return this._parsedProps.layers
}
if (this.componentName === "PlatedHole") {
return ["top", "bottom"] // TODO derive layers from parent
return this.root?._getBoard()?.allLayers ?? ["top", "bottom"]
}
return []
}
Expand Down
8 changes: 8 additions & 0 deletions lib/components/normal-components/Board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ export class Board extends Group<typeof boardProps> {
return 1.4 // TODO use prop
}

/**
* Get all available layers for the board
*/
get allLayers() {
// TODO use the board numLayers prop
return ["top", "bottom", "inner1", "inner2"]
}

doInitialPcbComponentRender(): void {
const { db } = this.root!
const { _parsedProps: props } = this
Expand Down
20 changes: 17 additions & 3 deletions lib/components/primitive-components/SilkscreenText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,36 @@ import { silkscreenTextProps } from "@tscircuit/props"
export class SilkscreenText extends PrimitiveComponent<
typeof silkscreenTextProps
> {
isPcbPrimitive = true
doInitialPcbPrimitiveRender(): void {
const { db } = this.root!
const { _parsedProps: props } = this
const container = this.getPrimitiveContainer()!

const position = this._getGlobalPcbPositionBeforeLayout()
const { maybeFlipLayer } = this._getPcbPrimitiveFlippedHelpers()

// TODO handle layer flipping
db.pcb_silkscreen_text.insert({
anchor_alignment: props.anchorAlignment,
anchor_position: {
x: props.pcbX ?? 0,
y: props.pcbY ?? 0,
x: position.x,
y: position.y,
},
font: props.font ?? "tscircuit2024",
font_size: props.fontSize ?? 1,
layer: "top",
layer: maybeFlipLayer(props.layer ?? "top") as "top" | "bottom",
text: props.text ?? "",
pcb_component_id: container.pcb_component_id!,
})
}

getPcbSize(): { width: number; height: number } {
const { _parsedProps: props } = this
const fontSize = props.fontSize ?? 1
const text = props.text ?? ""
const textWidth = text.length * fontSize
const textHeight = fontSize
return { width: textWidth * fontSize, height: textHeight * fontSize }
}
}
22 changes: 15 additions & 7 deletions lib/components/primitive-components/SmtPad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { smtPadProps } from "@tscircuit/props"
import type { Port } from "./Port"
import type { RenderPhaseFn } from "../base-components/Renderable"
import type { LayerRef, PCBSMTPad } from "@tscircuit/soup"
import { decomposeTSR } from "transformation-matrix"
import {
applyToPoint,
compose,
decomposeTSR,
flipX,
flipY,
translate,
} from "transformation-matrix"

export class SmtPad extends PrimitiveComponent<typeof smtPadProps> {
pcb_smtpad_id: string | null = null
Expand Down Expand Up @@ -49,20 +56,21 @@ export class SmtPad extends PrimitiveComponent<typeof smtPadProps> {
}
}

getAvailablePcbLayers(): string[] {
return this.props.layer ? [this.props.layer as LayerRef] : []
}

doInitialPcbPrimitiveRender(): void {
const { db } = this.root!
const { _parsedProps: props } = this
if (!props.portHints) return
const container = this.getPrimitiveContainer()
const position = this._getGlobalPcbPositionBeforeLayout()
const containerCenter = container?._getGlobalPcbPositionBeforeLayout()
const decomposedMat = decomposeTSR(
this._computePcbGlobalTransformBeforeLayout(),
)
const isRotated90 =
Math.abs(decomposedMat.rotation.angle * (180 / Math.PI) - 90) < 0.01

const { maybeFlipLayer } = this._getPcbPrimitiveFlippedHelpers()

let pcb_smtpad: PCBSMTPad | null = null
const pcb_component_id =
this.parent?.pcb_component_id ??
Expand All @@ -71,7 +79,7 @@ export class SmtPad extends PrimitiveComponent<typeof smtPadProps> {
pcb_smtpad = db.pcb_smtpad.insert({
pcb_component_id,
pcb_port_id: this.matchedPort?.pcb_port_id!, // port likely isn't matched
layer: props.layer ?? "top",
layer: maybeFlipLayer(props.layer ?? "top"),
shape: "circle",

// @ts-ignore: no idea why this is triggering
Expand All @@ -86,7 +94,7 @@ export class SmtPad extends PrimitiveComponent<typeof smtPadProps> {
pcb_smtpad = db.pcb_smtpad.insert({
pcb_component_id,
pcb_port_id: this.matchedPort?.pcb_port_id!, // port likely isn't matched
layer: props.layer ?? "top",
layer: maybeFlipLayer(props.layer ?? "top"),
shape: "rect",

...(isRotated90
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions tests/components/normal-components/chip-layer-flip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { test, expect } from "bun:test"
import { getTestFixture } from "tests/fixtures/get-test-fixture"

test("chip with manual footprint flips when layer is set to bottom", async () => {
const { project } = getTestFixture()

const footprint = (
<footprint>
<smtpad
shape="rect"
width="0.5mm"
height="0.5mm"
pcbX={1}
pcbY={1}
portHints={["1"]}
/>
<smtpad
shape="rect"
width="0.3mm"
height="0.3mm"
pcbX={1}
pcbY={-1}
portHints={["2"]}
/>
<smtpad
shape="rect"
width="0.1mm"
height="0.1mm"
pcbX={-0.5}
pcbY={0.1}
portHints={["3"]}
/>
<silkscreentext
pcbX={1}
pcbY={0}
anchorAlignment="center"
fontSize={0.2}
text="hi"
/>
</footprint>
)

project.add(
<board width="7mm" height="3mm">
<chip name="U1" pcbX={-2} layer="bottom" footprint={footprint} />
<chip name="U2" pcbX={2} layer="top" footprint={footprint} />
</board>,
)

project.render()

// Check if all SMT pads are on the bottom layer
const smtPads = project.db.pcb_smtpad
.list()
.filter((pad) => pad.layer === "bottom")
expect(smtPads.length).toBe(3)

// Check if the positions are mirrored
const padPositions = smtPads.map((pad) => ({ x: pad.x, y: pad.y }))
expect(padPositions).toContainEqual({ x: -3, y: 1 })
expect(padPositions).toContainEqual({ x: -3, y: -1 })

// Use snapshot to verify the overall layout
expect(project).toMatchPcbSnapshot(import.meta.path)
})
Loading

0 comments on commit b9f8cc6

Please sign in to comment.