Skip to content
Open
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
1 change: 1 addition & 0 deletions src/any_circuit_element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const any_circuit_element = z.union([
pcb.pcb_trace_error,
pcb.pcb_trace_missing_error,
pcb.pcb_placement_error,
pcb.pcb_placement_hint,
pcb.pcb_panelization_placement_error,
pcb.pcb_port_not_matched_error,
pcb.pcb_port_not_connected_error,
Expand Down
3 changes: 3 additions & 0 deletions src/pcb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export * from "./pcb_via"
export * from "./pcb_board"
export * from "./pcb_panel"
export * from "./pcb_placement_error"
export * from "./pcb_placement_hint"
export * from "./pcb_panelization_placement_error"
export * from "./pcb_trace_hint"
export * from "./pcb_silkscreen_line"
Expand Down Expand Up @@ -82,6 +83,7 @@ import type { PcbNet } from "./pcb_net"
import type { PcbBoard } from "./pcb_board"
import type { PcbPanel } from "./pcb_panel"
import type { PcbPlacementError } from "./pcb_placement_error"
import type { PcbPlacementHint } from "./pcb_placement_hint"
import type { PcbPanelizationPlacementError } from "./pcb_panelization_placement_error"
import type { PcbMissingFootprintError } from "./pcb_missing_footprint_error"
import type { ExternalFootprintLoadError } from "./external_footprint_load_error"
Expand Down Expand Up @@ -144,6 +146,7 @@ export type PcbCircuitElement =
| PcbBoard
| PcbPanel
| PcbPlacementError
| PcbPlacementHint
| PcbPanelizationPlacementError
| PcbTraceHint
| PcbSilkscreenLine
Expand Down
23 changes: 21 additions & 2 deletions src/pcb/pcb_placement_error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@ export const pcb_placement_error = base_circuit_json_error
.extend({
type: z.literal("pcb_placement_error"),
pcb_placement_error_id: getZodPrefixedIdWithDefault("pcb_placement_error"),
error_type: z.literal("pcb_placement_error").default("pcb_placement_error"),
error_type: z
.enum([
"pcb_placement_error",
"distance_exceeded",
"wrong_pad_orientation",
"decoupling_trace_too_long",
])
.default("pcb_placement_error"),
pcb_component_id: z.string().optional(),
pcb_placement_hint_id: z.string().optional(),
actual_distance: z.number().optional(),
max_distance: z.number().optional(),
subcircuit_id: z.string().optional(),
})
.describe("Defines a placement error on the PCB")
Expand All @@ -24,7 +35,15 @@ type InferredPcbPlacementError = z.infer<typeof pcb_placement_error>
export interface PcbPlacementError extends BaseCircuitJsonError {
type: "pcb_placement_error"
pcb_placement_error_id: string
error_type: "pcb_placement_error"
error_type:
| "pcb_placement_error"
| "distance_exceeded"
| "wrong_pad_orientation"
| "decoupling_trace_too_long"
pcb_component_id?: string
pcb_placement_hint_id?: string
actual_distance?: number
max_distance?: number
subcircuit_id?: string
}

Expand Down
48 changes: 48 additions & 0 deletions src/pcb/pcb_placement_hint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { z } from "zod"
import { distance } from "src/units"
import { getZodPrefixedIdWithDefault } from "src/common"
import { expectTypesMatch } from "src/utils/expect-types-match"

/**
* A placement hint that tells the autoplacer to position a component near
* a specific target pin, optionally with a facing pad constraint.
*/
export interface PcbPlacementHint {
type: "pcb_placement_hint"
pcb_placement_hint_id: string
pcb_component_id: string

/** The target port this component should be placed near */
target_pcb_port_id: string

/** The port of this component that should face the target */
facing_pcb_port_id?: string

/** Max center-to-center distance in mm. Default: 5mm */
max_distance?: number

/** Whether the placer has satisfied this hint */
is_satisfied?: boolean

subcircuit_id?: string
}

export const pcb_placement_hint = z
.object({
type: z.literal("pcb_placement_hint"),
pcb_placement_hint_id: getZodPrefixedIdWithDefault("pcb_placement_hint"),
pcb_component_id: z.string(),
target_pcb_port_id: z.string(),
facing_pcb_port_id: z.string().optional(),
max_distance: distance.optional(),
is_satisfied: z.boolean().optional(),
subcircuit_id: z.string().optional(),
})
.describe(
"A placement hint that tells the autoplacer to position a component near a specific target pin",
)

export type PcbPlacementHintInput = z.input<typeof pcb_placement_hint>
type InferredPcbPlacementHint = z.infer<typeof pcb_placement_hint>

expectTypesMatch<PcbPlacementHint, InferredPcbPlacementHint>(true)
13 changes: 13 additions & 0 deletions src/source/base/source_component_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ export interface SourceComponentBase {
internally_connected_source_port_ids?: string[][]
source_group_id?: string
subcircuit_id?: string

/** Selector for the pin/pad this component should be placed near */
place_near_selector?: string
/** Resolved source_port_id of the target pin */
place_near_port_id?: string
/** source_port_id of this component's pad that should face the target */
facing_pad_port_id?: string
/** Max center-to-center distance to the target pin (mm). Default: 5mm */
place_near_max_distance?: number
}

export const source_component_base = z.object({
Expand All @@ -35,6 +44,10 @@ export const source_component_base = z.object({
internally_connected_source_port_ids: z.array(z.array(z.string())).optional(),
source_group_id: z.string().optional(),
subcircuit_id: z.string().optional(),
place_near_selector: z.string().optional(),
place_near_port_id: z.string().optional(),
facing_pad_port_id: z.string().optional(),
place_near_max_distance: z.number().optional(),
})

type InferredSourceComponentBase = z.infer<typeof source_component_base>
Expand Down
23 changes: 23 additions & 0 deletions tests/pcb_placement_error_subtypes1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { test, expect } from "bun:test"
import { pcb_placement_error } from "src/pcb/pcb_placement_error"

test("pcb_placement_error parses distance_exceeded error", () => {
const data = {
type: "pcb_placement_error",
pcb_placement_error_id: "pcb_placement_error_1",
error_type: "distance_exceeded",
message: "C1 is 8mm from U1.VCC, max allowed is 5mm",
pcb_component_id: "pcb_component_1",
pcb_placement_hint_id: "pcb_placement_hint_1",
actual_distance: 0.008,
max_distance: 0.005,
}

const parsed = pcb_placement_error.parse(data)

expect(parsed.error_type).toBe("distance_exceeded")
expect(parsed.pcb_component_id).toBe("pcb_component_1")
expect(parsed.pcb_placement_hint_id).toBe("pcb_placement_hint_1")
expect(parsed.actual_distance).toBeCloseTo(0.008)
expect(parsed.max_distance).toBeCloseTo(0.005)
})
17 changes: 17 additions & 0 deletions tests/pcb_placement_error_subtypes2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { test, expect } from "bun:test"
import { pcb_placement_error } from "src/pcb/pcb_placement_error"

test("pcb_placement_error parses wrong_pad_orientation error", () => {
const data = {
type: "pcb_placement_error",
pcb_placement_error_id: "pcb_placement_error_2",
error_type: "wrong_pad_orientation",
message: "C1 facing pad is the farthest from target (270° rule violated)",
pcb_component_id: "pcb_component_1",
pcb_placement_hint_id: "pcb_placement_hint_1",
}

const parsed = pcb_placement_error.parse(data)

expect(parsed.error_type).toBe("wrong_pad_orientation")
})
16 changes: 16 additions & 0 deletions tests/pcb_placement_error_subtypes3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { test, expect } from "bun:test"
import { pcb_placement_error } from "src/pcb/pcb_placement_error"

test("pcb_placement_error parses decoupling_trace_too_long error", () => {
const data = {
type: "pcb_placement_error",
pcb_placement_error_id: "pcb_placement_error_3",
error_type: "decoupling_trace_too_long",
message: "Decoupling trace for C1 is 12mm, max allowed is 5mm",
pcb_component_id: "pcb_component_1",
}

const parsed = pcb_placement_error.parse(data)

expect(parsed.error_type).toBe("decoupling_trace_too_long")
})
14 changes: 14 additions & 0 deletions tests/pcb_placement_error_subtypes4.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { test, expect } from "bun:test"
import { pcb_placement_error } from "src/pcb/pcb_placement_error"

test("pcb_placement_error defaults to pcb_placement_error type", () => {
const data = {
type: "pcb_placement_error",
pcb_placement_error_id: "pcb_placement_error_4",
message: "Generic placement error",
}

const parsed = pcb_placement_error.parse(data)

expect(parsed.error_type).toBe("pcb_placement_error")
})
24 changes: 24 additions & 0 deletions tests/pcb_placement_hint1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { test, expect } from "bun:test"
import { pcb_placement_hint } from "src/pcb/pcb_placement_hint"

test("pcb_placement_hint parses with all fields", () => {
const data = {
type: "pcb_placement_hint",
pcb_placement_hint_id: "pcb_placement_hint_1",
pcb_component_id: "pcb_component_1",
target_pcb_port_id: "pcb_port_5",
facing_pcb_port_id: "pcb_port_10",
max_distance: "2mm",
is_satisfied: false,
subcircuit_id: "subcircuit_1",
}

const parsed = pcb_placement_hint.parse(data)

expect(parsed.type).toBe("pcb_placement_hint")
expect(parsed.pcb_component_id).toBe("pcb_component_1")
expect(parsed.target_pcb_port_id).toBe("pcb_port_5")
expect(parsed.facing_pcb_port_id).toBe("pcb_port_10")
expect(parsed.max_distance).toBeCloseTo(2)
expect(parsed.is_satisfied).toBe(false)
})
19 changes: 19 additions & 0 deletions tests/pcb_placement_hint2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { test, expect } from "bun:test"
import { pcb_placement_hint } from "src/pcb/pcb_placement_hint"

test("pcb_placement_hint parses without optional fields", () => {
const data = {
type: "pcb_placement_hint",
pcb_component_id: "pcb_component_1",
target_pcb_port_id: "pcb_port_5",
}

const parsed = pcb_placement_hint.parse(data)

expect(parsed.type).toBe("pcb_placement_hint")
expect(parsed.pcb_component_id).toBe("pcb_component_1")
expect(parsed.target_pcb_port_id).toBe("pcb_port_5")
expect(parsed.facing_pcb_port_id).toBeUndefined()
expect(parsed.max_distance).toBeUndefined()
expect(parsed.is_satisfied).toBeUndefined()
})
13 changes: 13 additions & 0 deletions tests/pcb_placement_hint3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { test, expect } from "bun:test"
import { any_circuit_element } from "src/any_circuit_element"

test("any_circuit_element includes pcb_placement_hint", () => {
const data = {
type: "pcb_placement_hint",
pcb_placement_hint_id: "pcb_placement_hint_1",
pcb_component_id: "pcb_component_1",
target_pcb_port_id: "pcb_port_5",
}

expect(() => any_circuit_element.parse(data)).not.toThrow()
})
21 changes: 21 additions & 0 deletions tests/source_component_placement_fields.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test, expect } from "bun:test"
import { source_component_base } from "src/source/base/source_component_base"

test("source_component_base accepts placement fields", () => {
const data = {
type: "source_component",
source_component_id: "source_component_1",
name: "C1",
place_near_selector: "U1.VCC",
place_near_port_id: "source_port_5",
facing_pad_port_id: "source_port_10",
place_near_max_distance: 0.005,
}

const parsed = source_component_base.parse(data)

expect(parsed.place_near_selector).toBe("U1.VCC")
expect(parsed.place_near_port_id).toBe("source_port_5")
expect(parsed.facing_pad_port_id).toBe("source_port_10")
expect(parsed.place_near_max_distance).toBeCloseTo(0.005)
})
Comment on lines +1 to +21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file violates the rule that numbered test files should only be used when splitting files with multiple tests. The file 'source_component_placement_fields1.test.ts' contains only one test() function but uses numbered naming (1). Since it has AT MOST one test, it should be named without the number suffix, like 'source_component_placement_fields.test.ts'.

Spotted by Graphite (based on custom rule: Custom rule)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

23 changes: 23 additions & 0 deletions tests/source_component_placement_fields_inheritance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { test, expect } from "bun:test"
import { source_simple_capacitor } from "src/source/source_simple_capacitor"

test("source_simple_capacitor inherits placement fields", () => {
const data = {
type: "source_component",
ftype: "simple_capacitor",
source_component_id: "source_component_1",
name: "C1",
capacitance: "100nF",
place_near_selector: "U1.VCC",
facing_pad_port_id: "source_port_10",
place_near_max_distance: 0.002,
max_decoupling_trace_length: "5mm",
}

const parsed = source_simple_capacitor.parse(data)

expect(parsed.place_near_selector).toBe("U1.VCC")
expect(parsed.facing_pad_port_id).toBe("source_port_10")
expect(parsed.place_near_max_distance).toBeCloseTo(0.002)
expect(parsed.max_decoupling_trace_length).toBeCloseTo(5)
})
Comment on lines +1 to +23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file violates the rule that numbered test files should only be used when splitting files with multiple tests. The file 'source_component_placement_fields2.test.ts' contains only one test() function but uses numbered naming (2). Since it has AT MOST one test, it should be named without the number suffix, like 'source_component_placement_fields_inheritance.test.ts'.

Spotted by Graphite (based on custom rule: Custom rule)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines +4 to +23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file name is inconsistent with the project's naming pattern. The file name 'source_component_placement_fields_inheritance.test.ts' is very long and uses underscores throughout, while other test files in the same directory use shorter, more concise names. The rule states that file names should be consistent with the project and generally use kebab-case. This file should be renamed to follow a more consistent pattern, such as 'source-component-placement-inheritance.test.ts' or split into a more appropriately named file.

Spotted by Graphite (based on custom rule: Custom rule)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All existing test files in this repo use snake_case (e.g. pcb_board.test.ts, cad_component_anchor_alignment.test.ts). Our files follow the same convention. The kebab-case suggestion doesn't match this repo's pattern.

17 changes: 17 additions & 0 deletions tests/source_component_placement_fields_optional.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { test, expect } from "bun:test"
import { source_component_base } from "src/source/base/source_component_base"

test("placement fields are optional on source_component_base", () => {
const data = {
type: "source_component",
source_component_id: "source_component_1",
name: "R1",
}

const parsed = source_component_base.parse(data)

expect(parsed.place_near_selector).toBeUndefined()
expect(parsed.place_near_port_id).toBeUndefined()
expect(parsed.facing_pad_port_id).toBeUndefined()
expect(parsed.place_near_max_distance).toBeUndefined()
})
Loading