diff --git a/lib/components/normal-components/Crystal.ts b/lib/components/normal-components/Crystal.ts index 9a4940c95..97d9cb87d 100644 --- a/lib/components/normal-components/Crystal.ts +++ b/lib/components/normal-components/Crystal.ts @@ -1,10 +1,20 @@ import { type CrystalPinLabels, crystalProps } from "@tscircuit/props" +import type { + SourcePort, + SourceSimpleCrystalInput, + SourceTrace, +} from "circuit-json" +import { formatSiUnit } from "format-si-unit" import { type BaseSymbolName, type Ftype } from "lib/utils/constants" import { NormalComponent } from "../base-components/NormalComponent/NormalComponent" -import type { SourceSimpleCrystalInput } from "circuit-json" -import { formatSiUnit } from "format-si-unit" type CrystalPorts = CrystalPinLabels +type SourcePortId = SourcePort["source_port_id"] +type SubcircuitConnectivityMapKey = NonNullable< + SourceTrace["subcircuit_connectivity_map_key"] +> + +const DEFAULT_CRYSTAL_MAX_TRACE_LENGTH_MM = 10 export class Crystal extends NormalComponent< typeof crystalProps, @@ -69,4 +79,50 @@ export class Crystal extends NormalComponent< this.source_component_id = source_component.source_component_id } + + override doInitialSourceDesignRuleChecks(): void { + super.doInitialSourceDesignRuleChecks() + if (!this.source_component_id) return + + const { db } = this.root! + const maximumTraceLength = + this._parsedProps.maxTraceLength ?? DEFAULT_CRYSTAL_MAX_TRACE_LENGTH_MM + const crystalSourcePortIds = new Set() + const crystalConnectivityMapKeys = new Set() + + for (const sourcePort of db.source_port.list()) { + if (sourcePort.source_component_id !== this.source_component_id) continue + + crystalSourcePortIds.add(sourcePort.source_port_id) + if (sourcePort.subcircuit_connectivity_map_key) { + crystalConnectivityMapKeys.add( + sourcePort.subcircuit_connectivity_map_key, + ) + } + } + + for (const sourceTrace of db.source_trace.list()) { + const isDirectlyConnectedToCrystal = + sourceTrace.connected_source_port_ids.some((sourcePortId) => + crystalSourcePortIds.has(sourcePortId), + ) + const isOnCrystalNet = + sourceTrace.subcircuit_connectivity_map_key !== undefined && + crystalConnectivityMapKeys.has( + sourceTrace.subcircuit_connectivity_map_key, + ) + + if (!isDirectlyConnectedToCrystal && !isOnCrystalNet) continue + if ( + typeof sourceTrace.max_length === "number" && + sourceTrace.max_length <= maximumTraceLength + ) { + continue + } + + db.source_trace.update(sourceTrace.source_trace_id, { + max_length: maximumTraceLength, + }) + } + } } diff --git a/lib/components/primitive-components/Group/Group.ts b/lib/components/primitive-components/Group/Group.ts index 358da096e..7e902c6ab 100644 --- a/lib/components/primitive-components/Group/Group.ts +++ b/lib/components/primitive-components/Group/Group.ts @@ -88,7 +88,6 @@ import type { ISubcircuit } from "./Subcircuit/ISubcircuit" import { addPortIdsToTracesAtJumperPads } from "./add-port-ids-to-traces-at-jumper-pads" import { getSourceTraceIdForRoutedTrace } from "./get-source-trace-id-for-routed-trace" import { insertAutoplacedJumpers } from "./insert-autoplaced-jumpers" -import { insertPcbTraceTooLongWarnings } from "./insert-pcb-trace-too-long-warnings" import { deleteExistingPcbTracesReplacedBy, getExistingPcbTracesForReroute, @@ -2061,11 +2060,6 @@ export class Group = typeof groupProps> }) } } - - insertPcbTraceTooLongWarnings({ - db, - subcircuitId: this.subcircuit_id!, - }) } } diff --git a/lib/components/primitive-components/Group/Group_doInitialSourceAddConnectivityMapKey.ts b/lib/components/primitive-components/Group/Group_doInitialSourceAddConnectivityMapKey.ts index 62a8f5c8b..dded34eb8 100644 --- a/lib/components/primitive-components/Group/Group_doInitialSourceAddConnectivityMapKey.ts +++ b/lib/components/primitive-components/Group/Group_doInitialSourceAddConnectivityMapKey.ts @@ -1,8 +1,8 @@ -import { ConnectivityMap } from "circuit-json-to-connectivity-map" -import type { Group } from "./Group" import type { SourceTrace } from "circuit-json" +import { ConnectivityMap } from "circuit-json-to-connectivity-map" import type { TraceI } from "../Trace/TraceI" import type { Via } from "../Via" +import type { Group } from "./Group" export function Group_doInitialSourceAddConnectivityMapKey(group: Group) { if (!group.isSubcircuit) return diff --git a/lib/components/primitive-components/Group/insert-pcb-trace-too-long-warnings.ts b/lib/components/primitive-components/Group/insert-pcb-trace-too-long-warnings.ts deleted file mode 100644 index d94759da3..000000000 --- a/lib/components/primitive-components/Group/insert-pcb-trace-too-long-warnings.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { CircuitJsonUtilObjects } from "@tscircuit/circuit-json-util" -import { getTraceLength } from "../Trace/trace-utils/compute-trace-length" - -export const insertPcbTraceTooLongWarnings = ({ - db, - subcircuitId, -}: { - db: CircuitJsonUtilObjects - subcircuitId: string -}) => { - for (const warning of db.pcb_trace_too_long_warning.list()) { - if (warning.subcircuit_id === subcircuitId) { - db.pcb_trace_too_long_warning.delete( - warning.pcb_trace_too_long_warning_id, - ) - } - } - - const pcbTraces = db.pcb_trace - .list() - .filter((pcbTrace) => pcbTrace.subcircuit_id === subcircuitId) - - for (const pcbTrace of pcbTraces) { - if (!pcbTrace.source_trace_id) continue - - const sourceTrace = db.source_trace.get(pcbTrace.source_trace_id) - const maximumTraceLength = sourceTrace?.max_length - if (maximumTraceLength === undefined) continue - - const actualTraceLength = - pcbTrace.trace_length ?? getTraceLength(pcbTrace.route) - if (actualTraceLength <= maximumTraceLength) continue - - db.pcb_trace_too_long_warning.insert({ - warning_type: "pcb_trace_too_long_warning", - message: `PCB trace is ${actualTraceLength.toFixed(2)}mm long, exceeding the ${maximumTraceLength}mm maximum`, - pcb_trace_id: pcbTrace.pcb_trace_id, - source_trace_id: pcbTrace.source_trace_id, - actual_trace_length: actualTraceLength, - maximum_trace_length: maximumTraceLength, - subcircuit_id: subcircuitId, - }) - } -} diff --git a/lib/components/primitive-components/Trace/trace-utils/get-max-length-from-connected-components.ts b/lib/components/primitive-components/Trace/trace-utils/get-max-length-from-connected-components.ts index b92290e1d..573a0a46a 100644 --- a/lib/components/primitive-components/Trace/trace-utils/get-max-length-from-connected-components.ts +++ b/lib/components/primitive-components/Trace/trace-utils/get-max-length-from-connected-components.ts @@ -18,19 +18,9 @@ export const getMaxLengthFromConnectedComponents = ( return sourceComponent.max_decoupling_trace_length } - if (sourceComponent?.ftype === "simple_crystal") { - const crystalProps = ( - port.parent as unknown as { - _parsedProps?: { maxTraceLength?: number } - } - )?._parsedProps - - return crystalProps?.maxTraceLength ?? 10 - } - return null }) - .filter((length): length is number => length !== null) + .filter((length): length is number => typeof length === "number") if (componentMaxLengths.length === 0) return undefined return Math.min(...componentMaxLengths) diff --git a/package.json b/package.json index bab70ff3d..51ff75554 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@tscircuit/alphabet": "0.0.25", "@tscircuit/breakout-point-solver": "github:tscircuit/breakout-point-solver#bac9629", "@tscircuit/capacity-autorouter": "^0.0.710", - "@tscircuit/checks": "0.0.145", + "@tscircuit/checks": "0.0.146", "@tscircuit/circuit-json-util": "^0.0.101", "@tscircuit/common": "^0.0.20", "@tscircuit/copper-pour-solver": "0.0.39", diff --git a/tests/components/normal-components/crystal-custom-trace-length.test.tsx b/tests/components/normal-components/crystal-custom-trace-length.test.tsx new file mode 100644 index 000000000..a945e5596 --- /dev/null +++ b/tests/components/normal-components/crystal-custom-trace-length.test.tsx @@ -0,0 +1,40 @@ +import { expect, test } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +test("configured crystal maximum propagates without replacing stricter limits", () => { + const { circuit } = getTestFixture() + + circuit.add( + + + + + + + + + + + + , + ) + + circuit.render() + + const sourceTraces = circuit.db.source_trace.list() + expect( + Object.fromEntries( + sourceTraces.map((trace) => [trace.display_name, trace.max_length]), + ), + ).toEqual({ + ".Y1 > .pin1 to net.XTAL_OUT": 5, + ".U1 > .XTAL_OUT to net.XTAL_OUT": 5, + ".C1 > .pin1 to net.XTAL_OUT": 3, + ".C2 > .pin1 to net.UNRELATED": undefined, + }) +}) diff --git a/tests/components/normal-components/crystal-trace-too-long-warning.test.tsx b/tests/components/normal-components/crystal-trace-too-long-warning.test.tsx index 899f44677..feae9baf5 100644 --- a/tests/components/normal-components/crystal-trace-too-long-warning.test.tsx +++ b/tests/components/normal-components/crystal-trace-too-long-warning.test.tsx @@ -1,46 +1,55 @@ import { expect, test } from "bun:test" import { getTestFixture } from "tests/fixtures/get-test-fixture" -test("crystal traces warn above the default or configured maximum length", () => { +test("traces on a crystal net inherit its maximum length and warn when too long", async () => { const { circuit } = getTestFixture() circuit.add( - + + - - - - + - - + + + , ) - circuit.render() + await circuit.renderUntilSettled() const sourceTraces = circuit.db.source_trace.list() - expect(sourceTraces.map((trace) => trace.max_length)).toEqual([10, 20]) + expect(sourceTraces.map((trace) => trace.max_length)).toEqual([10, 10, 10]) const warnings = circuit.db.pcb_trace_too_long_warning.list() - expect(warnings).toHaveLength(1) - expect(warnings[0]).toMatchObject({ - source_trace_id: sourceTraces[0].source_trace_id, - maximum_trace_length: 10, - }) - expect(warnings[0].actual_trace_length).toBeGreaterThan(10) + expect(warnings).toHaveLength(2) + expect(warnings.map((warning) => warning.source_trace_id).sort()).toEqual( + [sourceTraces[0].source_trace_id, sourceTraces[1].source_trace_id].sort(), + ) + expect( + warnings.every( + (warning) => + warning.maximum_trace_length === 10 && + warning.actual_trace_length > warning.maximum_trace_length, + ), + ).toBe(true) })