@tscircuit/checks 0.0.187 → 0.0.189

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,6 +44,26 @@ and output an array of arrays for any issues found.
44
44
  | [`runAllRoutingChecks`](./lib/run-all-checks.ts) | Runs all routing checks currently enabled (`checkEachPcbPortConnectedToPcbTraces`, `checkSourceTracesHavePcbTraces`, `checkEachPcbTraceNonOverlapping`, `checkPadTraceClearance`, `checkViaTraceClearance`, same/different net via spacing, and `checkPcbTracesOutOfBoard`). Trace-obstacle pairs are classified before aggregation, so each pair produces one overlap or clearance diagnostic, never both. |
45
45
  | [`runAllChecks`](./lib/run-all-checks.ts) | Runs placement, schematic, netlist, pin specification, and routing checks and returns a combined list of issues. |
46
46
 
47
+ ## Consolidated placement overlaps
48
+
49
+ `runAllPlacementChecks` and `runAllChecks` report one placement conflict per
50
+ component pair when footprint overlap causes multiple footprint, pad clearance,
51
+ and courtyard diagnostics. The message names the components, counts the conflicts,
52
+ and suggests moving them apart. Separate component pairs, clearance-only issues,
53
+ standalone elements, and unrelated routing diagnostics remain separate.
54
+
55
+ The result uses the existing `pcb_footprint_overlap_error` type and retains the
56
+ union of affected pad and hole IDs for rendering. The exported
57
+ `PcbComponentOverlapError` interface adds `pcb_component_ids` and `related_errors`
58
+ with the original diagnostics, including measured clearances. These extra context
59
+ fields are provided by checks; older Circuit JSON schema parsers may strip them.
60
+
61
+ Individual checks still return detailed diagnostics. Use
62
+ `runAllPlacementChecks(circuitJson, { consolidateOverlaps: false })` to obtain raw
63
+ aggregate results, for example to apply exclusions before calling
64
+ `consolidatePcbOverlapErrors(circuitJson, errors)`. Consolidation does not mutate
65
+ its inputs and can be applied again when combining runners.
66
+
47
67
  ## Implementation Details
48
68
 
49
69
  > [!NOTE]
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as circuit_json from 'circuit-json';
2
- import { AnyCircuitElement, PcbPortNotConnectedError, PcbTraceError, PcbPlacementError, PcbComponentOutsideBoardError, PcbViaClearanceError, PcbTraceWarning, PcbTraceMissingError, PcbFootprintOverlapError, PcbComponentMissingCourtyardWarning, PcbTraceTooLongWarning, PcbPadPadClearanceError, PcbPadTraceClearanceError, PcbViaTraceClearanceError, SourceComponentMisconfiguredError, SourceComponentPinsUnderspecifiedWarning, SourceNoPowerPinDefinedWarning, SourceNoGroundPinDefinedWarning, SchematicComponentStylingWarning, PcbConnectorNotInAccessibleOrientationWarning, SourceConfusingNetNameWarning } from 'circuit-json';
2
+ import { AnyCircuitElement, PcbPortNotConnectedError, PcbTraceError, PcbPlacementError, PcbComponentOutsideBoardError, PcbViaClearanceError, PcbTraceWarning, PcbTraceMissingError, PcbFootprintOverlapError, PcbPadPadClearanceError, PcbCourtyardOverlapError, PcbComponentMissingCourtyardWarning, PcbTraceTooLongWarning, PcbPadTraceClearanceError, PcbViaTraceClearanceError, SourceComponentMisconfiguredError, SourceComponentPinsUnderspecifiedWarning, SourceNoPowerPinDefinedWarning, SourceNoGroundPinDefinedWarning, SchematicComponentStylingWarning, PcbConnectorNotInAccessibleOrientationWarning, SourceConfusingNetNameWarning } from 'circuit-json';
3
3
  import { ConnectivityMap } from 'circuit-json-to-connectivity-map';
4
4
 
5
5
  declare function checkEachPcbPortConnectedToPcbTraces(circuitJson: AnyCircuitElement[]): PcbPortNotConnectedError[];
@@ -77,11 +77,27 @@ interface TraceBoardCheckConfig {
77
77
  */
78
78
  declare function checkPcbTracesOutOfBoard(circuitJson: AnyCircuitElement[], config?: TraceBoardCheckConfig): PcbTraceError[];
79
79
 
80
+ /** Additional diagnostic context supplied by checks; the base error remains
81
+ * compatible with existing Circuit JSON renderers. */
82
+ interface PcbComponentOverlapError extends PcbFootprintOverlapError {
83
+ pcb_component_ids?: string[];
84
+ related_errors?: PlacementOverlapError[];
85
+ }
86
+ type PlacementOverlapError = PcbComponentOverlapError | PcbPadPadClearanceError | PcbCourtyardOverlapError;
87
+ /**
88
+ * Consolidate placement conflicts for the same exact pair of components, only
89
+ * when a footprint overlap was detected. Keep raw details and all affected
90
+ * pad/hole IDs on one renderer-compatible error. Unrelated checks, standalone
91
+ * elements, same-component clearance errors and clearance-only pairs survive.
92
+ * Does not mutate inputs; safe to apply again when combining check runners.
93
+ */
94
+ declare function consolidatePcbOverlapErrors<T extends AnyCircuitElement>(circuitJson: AnyCircuitElement[], errors: T[]): (T | PcbComponentOverlapError)[];
95
+
80
96
  /**
81
97
  * Check for overlapping PCB components
82
98
  * Returns errors for components that overlap inappropriately
83
99
  */
84
- declare function checkPcbComponentOverlap(circuitJson: AnyCircuitElement[]): PcbFootprintOverlapError[];
100
+ declare function checkPcbComponentOverlap(circuitJson: AnyCircuitElement[]): PcbComponentOverlapError[];
85
101
 
86
102
  /** Returns a warning for every PCB component without a courtyard. */
87
103
  declare function checkPcbComponentsMissingCourtyard(circuitJson: AnyCircuitElement[]): PcbComponentMissingCourtyardWarning[];
@@ -183,7 +199,9 @@ declare function checkSchematicComponentMissingReferenceDesignatorText(circuitJs
183
199
  */
184
200
  declare function checkSchematicComponentPortsOutsideBody(circuitJson: AnyCircuitElement[]): SchematicComponentStylingWarning[];
185
201
 
186
- declare function runAllPlacementChecks(circuitJson: AnyCircuitElement[]): Promise<(circuit_json.PcbPlacementError | circuit_json.PcbComponentOutsideBoardError | circuit_json.PcbFootprintOverlapError | circuit_json.PcbComponentMissingCourtyardWarning | circuit_json.PcbPadPadClearanceError | circuit_json.PcbConnectorNotInAccessibleOrientationWarning | circuit_json.PcbCourtyardOverlapError)[]>;
202
+ declare function runAllPlacementChecks(circuitJson: AnyCircuitElement[], { consolidateOverlaps }?: {
203
+ consolidateOverlaps?: boolean | undefined;
204
+ }): Promise<(circuit_json.PcbPlacementError | circuit_json.PcbComponentOutsideBoardError | PcbComponentOverlapError | circuit_json.PcbPadPadClearanceError | circuit_json.PcbCourtyardOverlapError | circuit_json.PcbComponentMissingCourtyardWarning | circuit_json.PcbConnectorNotInAccessibleOrientationWarning)[]>;
187
205
  declare function runAllNetlistChecks(circuitJson: AnyCircuitElement[]): Promise<({
188
206
  type: "source_pin_must_be_connected_error";
189
207
  source_pin_must_be_connected_error_id: string;
@@ -195,8 +213,8 @@ declare function runAllNetlistChecks(circuitJson: AnyCircuitElement[]): Promise<
195
213
  } | circuit_json.SourceComponentMisconfiguredError | circuit_json.SourceConfusingNetNameWarning)[]>;
196
214
  declare function runAllSchematicChecks(circuitJson: AnyCircuitElement[]): Promise<circuit_json.SchematicComponentStylingWarning[]>;
197
215
  declare function runAllPinSpecificationChecks(circuitJson: AnyCircuitElement[]): Promise<(circuit_json.SourceComponentPinsUnderspecifiedWarning | circuit_json.SourceNoPowerPinDefinedWarning | circuit_json.SourceNoGroundPinDefinedWarning)[]>;
198
- declare function runAllRoutingChecks(circuitJson: AnyCircuitElement[]): Promise<(circuit_json.PcbPortNotConnectedError | circuit_json.PcbTraceError | circuit_json.PcbViaClearanceError | circuit_json.PcbTraceMissingError | circuit_json.PcbTraceTooLongWarning | circuit_json.PcbPadPadClearanceError | circuit_json.PcbPadTraceClearanceError | circuit_json.PcbViaTraceClearanceError)[]>;
199
- declare function runAllChecks(circuitJson: AnyCircuitElement[]): Promise<(circuit_json.PcbPortNotConnectedError | circuit_json.PcbTraceError | circuit_json.PcbPlacementError | circuit_json.PcbComponentOutsideBoardError | circuit_json.PcbViaClearanceError | circuit_json.PcbTraceMissingError | circuit_json.PcbFootprintOverlapError | circuit_json.PcbComponentMissingCourtyardWarning | circuit_json.PcbTraceTooLongWarning | circuit_json.PcbPadPadClearanceError | circuit_json.PcbPadTraceClearanceError | circuit_json.PcbViaTraceClearanceError | {
216
+ declare function runAllRoutingChecks(circuitJson: AnyCircuitElement[]): Promise<(circuit_json.PcbPortNotConnectedError | circuit_json.PcbTraceError | circuit_json.PcbViaClearanceError | circuit_json.PcbTraceMissingError | circuit_json.PcbPadPadClearanceError | circuit_json.PcbTraceTooLongWarning | circuit_json.PcbPadTraceClearanceError | circuit_json.PcbViaTraceClearanceError)[]>;
217
+ declare function runAllChecks(circuitJson: AnyCircuitElement[]): Promise<(circuit_json.PcbPortNotConnectedError | circuit_json.PcbTraceError | circuit_json.PcbPlacementError | circuit_json.PcbComponentOutsideBoardError | circuit_json.PcbViaClearanceError | circuit_json.PcbTraceMissingError | PcbComponentOverlapError | circuit_json.PcbPadPadClearanceError | circuit_json.PcbCourtyardOverlapError | circuit_json.PcbComponentMissingCourtyardWarning | circuit_json.PcbTraceTooLongWarning | circuit_json.PcbPadTraceClearanceError | circuit_json.PcbViaTraceClearanceError | {
200
218
  type: "source_pin_must_be_connected_error";
201
219
  source_pin_must_be_connected_error_id: string;
202
220
  error_type: "source_pin_must_be_connected_error";
@@ -204,7 +222,7 @@ declare function runAllChecks(circuitJson: AnyCircuitElement[]): Promise<(circui
204
222
  source_component_id: string;
205
223
  source_port_id: string;
206
224
  subcircuit_id?: string;
207
- } | circuit_json.SourceComponentMisconfiguredError | circuit_json.SourceComponentPinsUnderspecifiedWarning | circuit_json.SourceNoPowerPinDefinedWarning | circuit_json.SourceNoGroundPinDefinedWarning | circuit_json.SchematicComponentStylingWarning | circuit_json.SourceConfusingNetNameWarning | circuit_json.PcbConnectorNotInAccessibleOrientationWarning | circuit_json.PcbCourtyardOverlapError)[]>;
225
+ } | circuit_json.SourceComponentMisconfiguredError | circuit_json.SourceComponentPinsUnderspecifiedWarning | circuit_json.SourceNoPowerPinDefinedWarning | circuit_json.SourceNoGroundPinDefinedWarning | circuit_json.SchematicComponentStylingWarning | circuit_json.SourceConfusingNetNameWarning | circuit_json.PcbConnectorNotInAccessibleOrientationWarning)[]>;
208
226
 
209
227
  declare function checkConnectorAccessibleOrientation(circuitJson: AnyCircuitElement[]): PcbConnectorNotInAccessibleOrientationWarning[];
210
228
 
@@ -218,4 +236,4 @@ declare function checkTestPointAccessibility(circuitJson: AnyCircuitElement[]):
218
236
  /** Warn once per name when its source nets belong to multiple electrical islands. */
219
237
  declare function checkSameNameNetsAreConnected(circuitJson: AnyCircuitElement[]): SourceConfusingNetNameWarning[];
220
238
 
221
- export { NetManager, checkAllPinsInComponentAreUnderspecified, checkConnectorAccessibleOrientation, checkCopperToBoardEdgeClearance, checkDifferentNetViaSpacing, checkEachPcbPortConnectedToPcbTraces, checkEachPcbTraceNonOverlapping, checkNoGroundPinDefined, checkNoPowerPinDefined, checkPadPadClearance, checkPadTraceClearance, checkPcbComponentOverCutout, checkPcbComponentOverlap, checkPcbComponentsMissingCourtyard, checkPcbComponentsOutOfBoard, checkPcbCopperOverKeepout, checkPcbTraceLengths, checkPcbTraceViaCounts, checkPcbTracesOutOfBoard, checkPinMustBeConnected, checkSameNameNetsAreConnected, checkSameNetViaSpacing, checkSchematicComponentExcessiveVerticalPadding, checkSchematicComponentMissingReferenceDesignatorText, checkSchematicComponentPortsOutsideBody, checkSourceTracesHavePcbTraces, checkSourceTracesMatchPcbTraceThickness, checkTestPointAccessibility, checkTracesAreContiguous, checkTwoTerminalSwitchContactsOnDifferentNets, checkViaPadClearance, checkViaTraceClearance, checkViasInPads, checkViasOffBoard, dedupePcbDrcErrors, runAllChecks, runAllNetlistChecks, runAllPinSpecificationChecks, runAllPlacementChecks, runAllRoutingChecks, runAllSchematicChecks };
239
+ export { NetManager, type PcbComponentOverlapError, checkAllPinsInComponentAreUnderspecified, checkConnectorAccessibleOrientation, checkCopperToBoardEdgeClearance, checkDifferentNetViaSpacing, checkEachPcbPortConnectedToPcbTraces, checkEachPcbTraceNonOverlapping, checkNoGroundPinDefined, checkNoPowerPinDefined, checkPadPadClearance, checkPadTraceClearance, checkPcbComponentOverCutout, checkPcbComponentOverlap, checkPcbComponentsMissingCourtyard, checkPcbComponentsOutOfBoard, checkPcbCopperOverKeepout, checkPcbTraceLengths, checkPcbTraceViaCounts, checkPcbTracesOutOfBoard, checkPinMustBeConnected, checkSameNameNetsAreConnected, checkSameNetViaSpacing, checkSchematicComponentExcessiveVerticalPadding, checkSchematicComponentMissingReferenceDesignatorText, checkSchematicComponentPortsOutsideBody, checkSourceTracesHavePcbTraces, checkSourceTracesMatchPcbTraceThickness, checkTestPointAccessibility, checkTracesAreContiguous, checkTwoTerminalSwitchContactsOnDifferentNets, checkViaPadClearance, checkViaTraceClearance, checkViasInPads, checkViasOffBoard, consolidatePcbOverlapErrors, dedupePcbDrcErrors, runAllChecks, runAllNetlistChecks, runAllPinSpecificationChecks, runAllPlacementChecks, runAllRoutingChecks, runAllSchematicChecks };
package/dist/index.js CHANGED
@@ -799,23 +799,6 @@ var getTraceSegments = (circuitJson) => {
799
799
  return segments;
800
800
  });
801
801
  };
802
- var getTraceCenter = (segment) => {
803
- const routePoints = segment._pcbTrace.route.flatMap((routePoint) => {
804
- if (routePoint.route_type === "through_pad") {
805
- return [routePoint.start, routePoint.end];
806
- }
807
- return [{ x: routePoint.x, y: routePoint.y }];
808
- });
809
- const firstPoint = routePoints[0];
810
- const lastPoint = routePoints[routePoints.length - 1];
811
- if (!firstPoint || !lastPoint) {
812
- return midpoint(
813
- { x: segment.x1, y: segment.y1 },
814
- { x: segment.x2, y: segment.y2 }
815
- );
816
- }
817
- return midpoint(firstPoint, lastPoint);
818
- };
819
802
  var getCenterBetweenCopperEdges = ({
820
803
  tracePoint,
821
804
  obstaclePoint,
@@ -828,6 +811,15 @@ var getCenterBetweenCopperEdges = ({
828
811
  if (distance3 === 0) return midpoint(tracePoint, obstaclePoint);
829
812
  const unitX = dx / distance3;
830
813
  const unitY = dy / distance3;
814
+ if (distance3 <= traceRadius + obstacleRadius) {
815
+ const overlapStart = Math.max(-traceRadius, distance3 - obstacleRadius);
816
+ const overlapEnd = Math.min(traceRadius, distance3 + obstacleRadius);
817
+ const offset = (overlapStart + overlapEnd) / 2;
818
+ return {
819
+ x: tracePoint.x + unitX * offset,
820
+ y: tracePoint.y + unitY * offset
821
+ };
822
+ }
831
823
  const traceEdge = {
832
824
  x: tracePoint.x + unitX * traceRadius,
833
825
  y: tracePoint.y + unitY * traceRadius
@@ -887,6 +879,16 @@ var getTraceObstacleClearance = (segment, obstacle) => {
887
879
  };
888
880
  };
889
881
  var isTraceObstacleOverlap = (gap) => gap <= 0;
882
+ var getViaPadClearanceCenter = (via, pad) => getTraceObstacleClearance(
883
+ {
884
+ x1: via.x,
885
+ y1: via.y,
886
+ x2: via.x,
887
+ y2: via.y,
888
+ thickness: via.outer_diameter
889
+ },
890
+ pad
891
+ ).center;
890
892
 
891
893
  // lib/data-structures/SpatialIndex.ts
892
894
  var SpatialObjectIndex = class {
@@ -3508,7 +3510,11 @@ function checkPcbComponentOverlap(circuitJson) {
3508
3510
  type: "pcb_footprint_overlap_error",
3509
3511
  pcb_error_id: `pcb_footprint_overlap_${id1}_${id2}`,
3510
3512
  error_type: "pcb_footprint_overlap_error",
3511
- message: `${elem1.type} ${elem1Description} overlaps with ${elem2.type} ${elem2Description}`
3513
+ message: `${elem1.type} ${elem1Description} overlaps with ${elem2.type} ${elem2Description}`,
3514
+ pcb_component_ids: [
3515
+ elem1.pcb_component_id,
3516
+ elem2.pcb_component_id
3517
+ ].filter((id) => Boolean(id))
3512
3518
  };
3513
3519
  if (elem1.type === "pcb_smtpad" || elem2.type === "pcb_smtpad") {
3514
3520
  error.pcb_smtpad_ids = [];
@@ -3840,7 +3846,7 @@ function checkPadTraceClearance(circuitJson, {
3840
3846
  if (!getLayersOfPcbElement(pad).includes(segment.layer)) continue;
3841
3847
  if (connMap.areIdsConnected(segment.pcb_trace_id, padId)) continue;
3842
3848
  const pairId = `${padId}_${segment.pcb_trace_id}`;
3843
- const { gap } = getTraceObstacleClearance(segment, pad);
3849
+ const { gap, center } = getTraceObstacleClearance(segment, pad);
3844
3850
  if (isTraceObstacleOverlap(gap)) {
3845
3851
  errors.delete(pairId);
3846
3852
  overlappingPairIds.add(pairId);
@@ -3857,7 +3863,7 @@ function checkPadTraceClearance(circuitJson, {
3857
3863
  pcb_trace_id: segment.pcb_trace_id,
3858
3864
  minimum_clearance: minClearance,
3859
3865
  actual_clearance: gap,
3860
- center: getTraceCenter(segment)
3866
+ center
3861
3867
  };
3862
3868
  const current = errors.get(pairId);
3863
3869
  if (!current || gap < current.gap) {
@@ -3892,7 +3898,7 @@ function checkViaTraceClearance(circuitJson, {
3892
3898
  if (connMap.areIdsConnected(segment.pcb_trace_id, via.pcb_via_id))
3893
3899
  continue;
3894
3900
  const pairId = `${via.pcb_via_id}_${segment.pcb_trace_id}`;
3895
- const { gap } = getTraceObstacleClearance(segment, via);
3901
+ const { gap, center } = getTraceObstacleClearance(segment, via);
3896
3902
  if (isTraceObstacleOverlap(gap)) {
3897
3903
  errors.delete(pairId);
3898
3904
  overlappingPairIds.add(pairId);
@@ -3909,7 +3915,7 @@ function checkViaTraceClearance(circuitJson, {
3909
3915
  pcb_trace_id: segment.pcb_trace_id,
3910
3916
  minimum_clearance: minClearance,
3911
3917
  actual_clearance: gap,
3912
- center: getTraceCenter(segment)
3918
+ center
3913
3919
  };
3914
3920
  const current = errors.get(pairId);
3915
3921
  if (!current || gap < current.gap) {
@@ -3962,8 +3968,6 @@ function checkViaPadClearance(circuitJson, {
3962
3968
  if (connMap.areIdsConnected(via.pcb_via_id, padId)) continue;
3963
3969
  const gap = getPadToPadGap(via, pad);
3964
3970
  if (gap + EPSILON >= requiredClearance) continue;
3965
- const viaCenter = getPadCenter(via);
3966
- const padCenter = getPadCenter(pad);
3967
3971
  errors.push({
3968
3972
  type: "pcb_pad_pad_clearance_error",
3969
3973
  pcb_pad_pad_clearance_error_id: `via_pad_clearance_${via.pcb_via_id}_${padId}`,
@@ -3972,10 +3976,7 @@ function checkViaPadClearance(circuitJson, {
3972
3976
  pcb_pad_ids: [via.pcb_via_id, padId],
3973
3977
  minimum_clearance: requiredClearance,
3974
3978
  actual_clearance: gap,
3975
- center: {
3976
- x: (viaCenter.x + padCenter.x) / 2,
3977
- y: (viaCenter.y + padCenter.y) / 2
3978
- }
3979
+ center: getViaPadClearanceCenter(via, pad)
3979
3980
  });
3980
3981
  }
3981
3982
  }
@@ -4556,6 +4557,118 @@ function checkSchematicComponentPortsOutsideBody(circuitJson) {
4556
4557
  return warnings;
4557
4558
  }
4558
4559
 
4560
+ // lib/consolidate-pcb-overlap-errors.ts
4561
+ import { getPrimaryId as getPrimaryId9 } from "@tscircuit/circuit-json-util";
4562
+ function consolidatePcbOverlapErrors(circuitJson, errors) {
4563
+ const ownerByElementId = /* @__PURE__ */ new Map();
4564
+ const elementById = /* @__PURE__ */ new Map();
4565
+ for (const element of circuitJson) {
4566
+ const id = getPrimaryId9(element);
4567
+ elementById.set(id, element);
4568
+ if ("pcb_component_id" in element && element.pcb_component_id) {
4569
+ ownerByElementId.set(id, element.pcb_component_id);
4570
+ }
4571
+ }
4572
+ const rawErrors = errors.flatMap(
4573
+ (error) => error.type === "pcb_footprint_overlap_error" ? error.related_errors ?? [error] : [error]
4574
+ );
4575
+ const groups = /* @__PURE__ */ new Map();
4576
+ const keyByError = /* @__PURE__ */ new Map();
4577
+ for (const error of rawErrors) {
4578
+ let componentIds;
4579
+ if (error.type === "pcb_footprint_overlap_error") {
4580
+ if (error.pcb_keepout_ids?.length) continue;
4581
+ const overlap = error;
4582
+ const elementIds = [
4583
+ ...error.pcb_smtpad_ids ?? [],
4584
+ ...error.pcb_plated_hole_ids ?? [],
4585
+ ...error.pcb_hole_ids ?? []
4586
+ ];
4587
+ if (elementIds.some((id) => !ownerByElementId.has(id))) continue;
4588
+ componentIds = overlap.pcb_component_ids ?? elementIds.map((id) => ownerByElementId.get(id));
4589
+ } else if (error.type === "pcb_pad_pad_clearance_error") {
4590
+ if (error.pcb_pad_ids.some((id) => !ownerByElementId.has(id))) continue;
4591
+ componentIds = error.pcb_pad_ids.map((id) => ownerByElementId.get(id));
4592
+ } else if (error.type === "pcb_courtyard_overlap_error") {
4593
+ componentIds = error.pcb_component_ids;
4594
+ } else {
4595
+ continue;
4596
+ }
4597
+ componentIds = [...new Set(componentIds)].sort();
4598
+ if (componentIds.length !== 2) continue;
4599
+ const key = JSON.stringify(componentIds);
4600
+ keyByError.set(error, key);
4601
+ const group = groups.get(key) ?? [];
4602
+ group.push(error);
4603
+ groups.set(key, group);
4604
+ }
4605
+ const summaries = /* @__PURE__ */ new Map();
4606
+ for (const [key, group] of groups) {
4607
+ const overlaps = group.filter(
4608
+ (e) => e.type === "pcb_footprint_overlap_error"
4609
+ );
4610
+ if (group.length < 2 || overlaps.length === 0) continue;
4611
+ const componentIds = JSON.parse(key);
4612
+ const names = componentIds.map((id) => {
4613
+ const component = elementById.get(id);
4614
+ const source = component?.type === "pcb_component" ? elementById.get(component.source_component_id) : void 0;
4615
+ return source?.type === "source_component" ? source.name : id;
4616
+ });
4617
+ const counts = [
4618
+ [overlaps.length, "footprint overlap"],
4619
+ [
4620
+ group.filter((e) => e.type === "pcb_pad_pad_clearance_error").length,
4621
+ "pad clearance violation"
4622
+ ],
4623
+ [
4624
+ group.filter((e) => e.type === "pcb_courtyard_overlap_error").length,
4625
+ "courtyard conflict"
4626
+ ]
4627
+ ];
4628
+ const details = counts.filter(([count]) => count > 0).map(([count, label]) => `${count} ${label}${count === 1 ? "" : "s"}`);
4629
+ const summary = {
4630
+ type: "pcb_footprint_overlap_error",
4631
+ error_type: "pcb_footprint_overlap_error",
4632
+ pcb_error_id: `pcb_component_overlap_${componentIds.join("_")}`,
4633
+ pcb_component_ids: componentIds,
4634
+ message: `${names.join(" overlaps ")}: ${details.join(", ")}. Move the components apart.`,
4635
+ related_errors: group
4636
+ };
4637
+ if (group.some((error) => error.is_fatal)) summary.is_fatal = true;
4638
+ const affectedIds = /* @__PURE__ */ new Set();
4639
+ for (const error of group) {
4640
+ if (error.type === "pcb_footprint_overlap_error") {
4641
+ for (const id of [
4642
+ ...error.pcb_smtpad_ids ?? [],
4643
+ ...error.pcb_plated_hole_ids ?? [],
4644
+ ...error.pcb_hole_ids ?? []
4645
+ ])
4646
+ affectedIds.add(id);
4647
+ } else if (error.type === "pcb_pad_pad_clearance_error") {
4648
+ for (const id of error.pcb_pad_ids) affectedIds.add(id);
4649
+ }
4650
+ }
4651
+ for (const [field, type] of [
4652
+ ["pcb_smtpad_ids", "pcb_smtpad"],
4653
+ ["pcb_plated_hole_ids", "pcb_plated_hole"],
4654
+ ["pcb_hole_ids", "pcb_hole"]
4655
+ ]) {
4656
+ const ids = [...affectedIds].filter((id) => elementById.get(id)?.type === type).sort();
4657
+ if (ids.length) summary[field] = ids;
4658
+ }
4659
+ summaries.set(key, summary);
4660
+ }
4661
+ const emitted = /* @__PURE__ */ new Set();
4662
+ return rawErrors.flatMap((error) => {
4663
+ const key = keyByError.get(error);
4664
+ const summary = key === void 0 ? void 0 : summaries.get(key);
4665
+ if (!summary || key === void 0) return [error];
4666
+ if (emitted.has(key)) return [];
4667
+ emitted.add(key);
4668
+ return [summary];
4669
+ });
4670
+ }
4671
+
4559
4672
  // lib/check-same-name-nets-are-connected.ts
4560
4673
  function checkSameNameNetsAreConnected(circuitJson) {
4561
4674
  const parents = /* @__PURE__ */ new Map();
@@ -4884,8 +4997,8 @@ function checkTestPointAccessibility(circuitJson) {
4884
4997
  }
4885
4998
 
4886
4999
  // lib/run-all-checks.ts
4887
- async function runAllPlacementChecks(circuitJson) {
4888
- return [
5000
+ async function runAllPlacementChecks(circuitJson, { consolidateOverlaps = true } = {}) {
5001
+ const errors = [
4889
5002
  ...checkCopperToBoardEdgeClearance(circuitJson),
4890
5003
  ...checkViasInPads(circuitJson),
4891
5004
  ...checkPcbComponentsOutOfBoard(circuitJson),
@@ -4898,6 +5011,7 @@ async function runAllPlacementChecks(circuitJson) {
4898
5011
  ...checkConnectorAccessibleOrientation(circuitJson),
4899
5012
  ...checkTestPointAccessibility(circuitJson)
4900
5013
  ];
5014
+ return consolidateOverlaps ? consolidatePcbOverlapErrors(circuitJson, errors) : errors;
4901
5015
  }
4902
5016
  async function runAllNetlistChecks(circuitJson) {
4903
5017
  return [
@@ -4980,6 +5094,7 @@ export {
4980
5094
  checkViaTraceClearance,
4981
5095
  checkViasInPads,
4982
5096
  checkViasOffBoard,
5097
+ consolidatePcbOverlapErrors,
4983
5098
  dedupePcbDrcErrors,
4984
5099
  runAllChecks,
4985
5100
  runAllNetlistChecks,