Repository: tscircuit/pcb-viewer
Pull request #245: Feat:Highlight DRC Errors (PcbTraceError)
Author: login: Abse2001
Labels: name: 🙋 Bounty claim
name: 💰 Rewarded
Created: 2025-04-26T17:32:17Z | Merged: 2025-04-27T00:10:23Z

/claim #243
/closes #243 


https://github.com/user-attachments/assets/222942a1-e91c-4541-bf3e-a73d9b7e924e



Linked issue #243: Highlight DRC Errors (PcbTraceError)
Spec: https://github.com/tscircuit/circuit-json?tab=readme-ov-file#pcbtraceerror

/bounty $40
Linked issue comments: user: login: algora-pbc[bot]
created_at: 2025-04-25T18:07:53Z
body: ## 💎 $40 bounty [• tscircuit](https://algora.io/tscircuit)
### Steps to solve:
1. **Start working**: Comment `/attempt #243` with your implementation plan
2. **Submit work**: Create a pull request including `/claim #243` in the PR body to claim the bounty
3. **Receive payment**: 100% of the bounty is received 2-5 days post-reward. [Make sure you are eligible for payouts](https://algora.io/docs/payments#supported-countries-regions)

### ❗ Important guidelines:
- To claim a bounty, you need to **provide a short demo video** of your changes in your pull request
- If anything is unclear, **ask for clarification** before starting as this will help avoid potential rework
- Low quality AI PRs will not receive review and will be closed
- Do not ask to be assigned unless you've contributed before

Thank you for contributing to tscircuit/pcb-viewer!

| Attempt | Started (UTC) | Solution | Actions |
| --- | --- | --- | --- |
| 🟢 @Abse2001 | Apr 26, 2025, 02:35:06 PM | #245 | [Reward](https://algora.io/claims/GXikHprypVWg7ry7) |
author_association: NONE
user: login: seveibar
created_at: 2025-04-25T18:08:35Z
body: The user should be able to hide DRC errors with the toolbar
author_association: CONTRIBUTOR
user: login: Abse2001
created_at: 2025-04-26T14:35:04Z
body: /attempt #243
author_association: CONTRIBUTOR
user: login: algora-pbc[bot]
created_at: 2025-04-27T00:10:33Z
body: 🎉🎈 @Abse2001 has been awarded **$40** by **tscircuit**! 🎈🎊
author_association: NONE

Diff:
diff --git a/src/components/CanvasElementsRenderer.tsx b/src/components/CanvasElementsRenderer.tsx
index f362d045..f5457176 100644
--- a/src/components/CanvasElementsRenderer.tsx
+++ b/src/components/CanvasElementsRenderer.tsx
@@ -46,43 +46,46 @@ export const CanvasElementsRenderer = (props: CanvasElementsRendererProps) => {
 
   const [hoverState, setHoverState] = useState({
     drawingObjectIdsWithMouseOver: new Set<string>(),
-    primitiveIdsInMousedOverNet: [] as string[]
-  });
+    primitiveIdsInMousedOverNet: [] as string[],
+  })
 
   const primitives = useMemo(() => {
     return addInteractionMetadataToPrimitives({
       primitivesWithoutInteractionMetadata,
       drawingObjectIdsWithMouseOver: hoverState.drawingObjectIdsWithMouseOver,
       primitiveIdsInMousedOverNet: hoverState.primitiveIdsInMousedOverNet,
-    });
-  }, [primitivesWithoutInteractionMetadata, hoverState]);
+    })
+  }, [primitivesWithoutInteractionMetadata, hoverState])
 
-  const onMouseOverPrimitives = useCallback((primitivesHoveredOver: Primitive[]) => {
-    const primitiveIdsInMousedOverNet: string[] = []
-    for (const primitive of primitivesHoveredOver) {
-      if (primitive._element) {
-        const connectedPrimitivesList = connectivityMap.getNetConnectedToId(
-          "pcb_port_id" in primitive._element
-            ? primitive._element?.pcb_port_id!
-            : "pcb_trace_id" in primitive._element
-              ? primitive._element?.pcb_trace_id!
-              : "",
-        )
-        primitiveIdsInMousedOverNet.push(
-          ...connectivityMap.getIdsConnectedToNet(connectedPrimitivesList!),
-        )
+  const onMouseOverPrimitives = useCallback(
+    (primitivesHoveredOver: Primitive[]) => {
+      const primitiveIdsInMousedOverNet: string[] = []
+      for (const primitive of primitivesHoveredOver) {
+        if (primitive._element) {
+          const connectedPrimitivesList = connectivityMap.getNetConnectedToId(
+            "pcb_port_id" in primitive._element
+              ? primitive._element?.pcb_port_id!
+              : "pcb_trace_id" in primitive._element
+                ? primitive._element?.pcb_trace_id!
+                : "",
+          )
+          primitiveIdsInMousedOverNet.push(
+            ...connectivityMap.getIdsConnectedToNet(connectedPrimitivesList!),
+          )
+        }
       }
-    }
 
-    const drawingObjectIdsWithMouseOver = new Set(
-      primitivesHoveredOver.map((p) => p._pcb_drawing_object_id),
-    )
+      const drawingObjectIdsWithMouseOver = new Set(
+        primitivesHoveredOver.map((p) => p._pcb_drawing_object_id),
+      )
 
-    setHoverState({
-      drawingObjectIdsWithMouseOver,
-      primitiveIdsInMousedOverNet
-    });
-  }, [connectivityMap]);
+      setHoverState({
+        drawingObjectIdsWithMouseOver,
+        primitiveIdsInMousedOverNet,
+      })
+    },
+    [connectivityMap],
+  )
 
   return (
     <MouseElementTracker
diff --git a/src/components/ErrorOverlay.tsx b/src/components/ErrorOverlay.tsx
index a1bd5d6a..9ed8c3f2 100644
--- a/src/components/ErrorOverlay.tsx
+++ b/src/components/ErrorOverlay.tsx
@@ -1,8 +1,10 @@
 import { css } from "@emotion/css"
+import { su } from "@tscircuit/soup-util"
 import type { AnyCircuitElement, PcbPort, PcbTraceError } from "circuit-json"
 import { zIndexMap } from "lib/util/z-index-map"
 import { useRef } from "react"
 import { type Matrix, applyToPoint, identity } from "transformation-matrix"
+import { useGlobalStore } from "global-store" // adjust the import path as needed
 
 interface Props {
   transform?: Matrix
@@ -61,9 +63,51 @@ const ErrorSVG = ({
   </svg>
 )
 
+const RouteSVG = ({
+  points,
+  errorCenter,
+}: {
+  points: { x: number; y: number }[]
+  errorCenter: { x: number; y: number }
+}) => (
+  <svg
+    style={{
+      position: "absolute",
+      left: 0,
+      top: 0,
+      pointerEvents: "none",
+      mixBlendMode: "difference",
+      zIndex: zIndexMap.errorOverlay,
+    }}
+    width="100%"
+    height="100%"
+  >
+    {points.length > 1 && (
+      <polyline
+        points={points.map((pt) => `${pt.x},${pt.y}`).join(" ")}
+        fill="none"
+        stroke="red"
+        strokeWidth={1.5}
+        strokeDasharray="2,2"
+      />
+    )}
+    <rect
+      x={errorCenter.x - 5}
+      y={errorCenter.y - 5}
+      width={10}
+      height={10}
+      transform={`rotate(45 ${errorCenter.x} ${errorCenter.y})`}
+      fill="red"
+    />
+  </svg>
+)
+
 export const ErrorOverlay = ({ children, transform, elements }: Props) => {
   if (!transform) transform = identity()
   const containerRef = useRef<HTMLDivElement | null>(null)
+  const isShowingDRCErrors = useGlobalStore(
+    (state) => state.is_showing_drc_errors,
+  )
 
   return (
     <div style={{ position: "relative" }} ref={containerRef}>
@@ -71,8 +115,7 @@ export const ErrorOverlay = ({ children, transform, elements }: Props) => {
       {elements
         ?.filter((el): el is PcbTraceError => el.type === "pcb_trace_error")
         .map((el: PcbTraceError) => {
-          const { pcb_port_ids } = el
-
+          const { pcb_port_ids, pcb_trace_id } = el
           const port1 = elements.find(
             (el): el is PcbPort =>
               el.type === "pcb_port" && el.pcb_port_id === pcb_port_ids?.[0],
@@ -81,44 +124,45 @@ export const ErrorOverlay = ({ children, transform, elements }: Props) => {
             (el): el is PcbPort =>
               el.type === "pcb_port" && el.pcb_port_id === pcb_port_ids?.[1],
           )
+          const trace = su(elements).pcb_trace.get(pcb_trace_id)
 
-          if (!port1 || !port2) return null
+          // port-based errors always show
+          if (port1 && port2) {
+            const screenPort1 = applyToPoint(transform, {
+              x: port1.x,
+              y: port1.y,
+            })
+            const screenPort2 = applyToPoint(transform, {
+              x: port2.x,
+              y: port2.y,
+            })
 
-          const screenPort1 = applyToPoint(transform, {
-            x: port1.x,
-            y: port1.y,
-          })
-          const screenPort2 = applyToPoint(transform, {
-            x: port2.x,
-            y: port2.y,
-          })
+            const canLineBeDrawn = !(
+              isNaN(screenPort1.x) ||
+              isNaN(screenPort1.y) ||
+              isNaN(screenPort2.x) ||
+              isNaN(screenPort2.y)
+            )
 
-          const canLineBeDrawn = !(
-            isNaN(screenPort1.x) ||
-            isNaN(screenPort1.y) ||
-            isNaN(screenPort2.x) ||
-            isNaN(screenPort2.y)
-          )
+            const errorCenter = {
+              x: (screenPort1.x + screenPort2.x) / 2,
+              y: (screenPort1.y + screenPort2.y) / 2,
+            }
 
-          const errorCenter = {
-            x: (screenPort1.x + screenPort2.x) / 2,
-            y: (screenPort1.y + screenPort2.y) / 2,
-          }
+            if (isNaN(errorCenter.x) || isNaN(errorCenter.y)) {
+              return null
+            }
 
-          if (isNaN(errorCenter.x) || isNaN(errorCenter.y)) {
-            return null
-          }
-
-          return (
-            <>
-              <ErrorSVG
-                screenPort1={screenPort1}
-                screenPort2={screenPort2}
-                errorCenter={errorCenter}
-                canLineBeDrawn={canLineBeDrawn}
-              />
-              <div
-                className={css`
+            return (
+              <>
+                <ErrorSVG
+                  screenPort1={screenPort1}
+                  screenPort2={screenPort2}
+                  errorCenter={errorCenter}
+                  canLineBeDrawn={canLineBeDrawn}
+                />
+                <div
+                  className={css`
                   position: absolute;
                   z-index: 100;
                   left: ${errorCenter.x}px;
@@ -147,19 +191,67 @@ export const ErrorOverlay = ({ children, transform, elements }: Props) => {
                     display: flex;
                   }
                 `}
-              >
-                <div className="error-message">{el.message}</div>
-                <div
-                  className={css`
+                >
+                  <div className="error-message">{el.message}</div>
+                  <div
+                    className={css`
                     width: 10px;
                     height: 10px;
                     transform: translate(0, 5px) rotate(45deg);
                     background-color: red;
                   `}
-                />
-              </div>
-            </>
-          )
+                  />
+                </div>
+              </>
+            )
+          }
+          // fallback: only show if global DRC errors enabled and route exists
+          if (trace?.route && isShowingDRCErrors) {
+            const screenPoints = trace.route.map((pt) =>
+              applyToPoint(transform!, { x: pt.x, y: pt.y }),
+            )
+            if (screenPoints.some((pt) => isNaN(pt.x) || isNaN(pt.y)))
+              return null
+            const mid = Math.floor(screenPoints.length / 2)
+            const errorCenter = screenPoints[mid]
+            return (
+              <>
+                <RouteSVG points={screenPoints} errorCenter={errorCenter} />
+                <div
+                  className={css`
+                    position: absolute;
+                    z-index: ${zIndexMap.errorOverlay + 1};
+                    left: ${errorCenter.x}px;
+                    top: ${errorCenter.y}px;
+                    color: red;
+                    text-align: center;
+                    font-family: sans-serif;
+                    font-size: 12px;
+                    display: flex;
+                    flex-direction: column;
+                    align-items: center;
+                    cursor: pointer;
+                    transform: translate(-50%, -100%);
+
+                    & .error-message { opacity: 0; pointer-events: none; width: 200px; transition: opacity 0.2s; margin-bottom: 10px; }
+                    &:hover .error-message { opacity: 1; display: flex; }
+                  `}
+                >
+                  <div className="error-message">{el.message}</div>
+                  <div
+                    className={css`
+                      width: 10px;
+                      height: 10px;
+                      transform: translate(0, 5px) rotate(45deg);
+                      background-color: red;
+                    `}
+                  />
+                </div>
+              </>
+            )
+          }
+
+          return null
         })}
     </div>
   )
diff --git a/src/components/ToolbarOverlay.tsx b/src/components/ToolbarOverlay.tsx
index 24747bb4..ee0051c6 100644
--- a/src/components/ToolbarOverlay.tsx
+++ b/src/components/ToolbarOverlay.tsx
@@ -122,12 +122,14 @@ export const ToolbarOverlay = ({ children, elements }: Props) => {
     is_showing_rats_nest,
     is_showing_multiple_traces_length,
     is_showing_autorouting,
+    is_showing_drc_errors,
   ] = useGlobalStore((s) => [
     s.in_move_footprint_mode,
     s.in_draw_trace_mode,
     s.is_showing_rats_nest,
     s.is_showing_multiple_traces_length,
     s.is_showing_autorouting,
+    s.is_showing_drc_errors,
   ])
   const setEditMode = useGlobalStore((s) => s.setEditMode)
   const setIsShowingRatsNest = useGlobalStore((s) => s.setIsShowingRatsNest)
@@ -137,6 +139,7 @@ export const ToolbarOverlay = ({ children, elements }: Props) => {
   const setIsShowingAutorouting = useGlobalStore(
     (s) => s.setIsShowingAutorouting,
   )
+  const setIsShowingDrcErrors = useGlobalStore((s) => s.setIsShowingDrcErrors)
 
   useHotKey("1", () => selectLayer("top"))
   useHotKey("2", () => selectLayer("bottom"))
@@ -331,6 +334,13 @@ export const ToolbarOverlay = ({ children, elements }: Props) => {
                     setIsShowingAutorouting(!is_showing_autorouting)
                   }}
                 />
+                <CheckboxMenuItem
+                  label="Show DRC Errors"
+                  checked={is_showing_drc_errors}
+                  onClick={() => {
+                    setIsShowingDrcErrors(!is_showing_drc_errors)
+                  }}
+                />
               </div>
             )}
           </div>
diff --git a/src/global-store.ts b/src/global-store.ts
index 57858d99..f9eb233a 100644
--- a/src/global-store.ts
+++ b/src/global-store.ts
@@ -19,6 +19,7 @@ export interface State {
   is_moving_component: boolean
   is_drawing_trace: boolean
   is_showing_autorouting: boolean
+  is_showing_drc_errors: boolean
 
   is_showing_multiple_traces_length: boolean
   is_showing_rats_nest: boolean
@@ -31,6 +32,7 @@ export interface State {
   setIsMouseOverContainer: (is_focused: boolean) => void
   setIsShowingAutorouting: (is_showing: boolean) => void
   setIsShowingMultipleTracesLength: (is_showing: boolean) => void
+  setIsShowingDrcErrors: (is_showing: boolean) => void
 }
 
 export type StateProps = {
@@ -56,6 +58,7 @@ export const createStore = (initialState: Partial<StateProps> = {}) =>
         is_showing_multiple_traces_length: false,
         is_showing_rats_nest: false,
         is_showing_autorouting: true,
+        is_showing_drc_errors: true,
         ...initialState,
 
         selectLayer: (layer) => set({ selected_layer: layer }),
@@ -79,6 +82,8 @@ export const createStore = (initialState: Partial<StateProps> = {}) =>
           set({ is_showing_multiple_traces_length: is_showing }),
         setIsShowingAutorouting: (is_showing) =>
           set({ is_showing_autorouting: is_showing }),
+        setIsShowingDrcErrors: (is_showing) =>
+          set({ is_showing_drc_errors: is_showing }),
       }) as const,
   )
 
diff --git a/src/stories/DRC-errors-highlight.stories.tsx b/src/stories/DRC-errors-highlight.stories.tsx
new file mode 100644
index 00000000..19744641
--- /dev/null
+++ b/src/stories/DRC-errors-highlight.stories.tsx
@@ -0,0 +1,60 @@
+import { PCBViewer } from "PCBViewer"
+import { Circuit } from "@tscircuit/core"
+import type { Meta } from "@storybook/react"
+import { useEffect, useMemo, useState } from "react"
+
+export const DRCerrors: React.FC = () => {
+  const [circuitJson, setCircuitJson] = useState(0)
+
+  const circuit = useMemo(() => {
+    const circuit = new Circuit()
+
+    circuit.add(
+      <board
+        width="10mm"
+        height="10mm"
+        autorouter={{
+          local: true,
+          groupMode: "subcircuit",
+        }}
+      >
+        <resistor
+          name="R1"
+          resistance="10k"
+          pcbX={-2}
+          pcbY={1}
+          footprint="0402"
+        />
+        <resistor
+          name="R2"
+          resistance="10k"
+          footprint="0402"
+          layer="bottom"
+          pcbX={2}
+          pcbY={0}
+        />
+        <trace from=".R1 > .pin1" to=".R2 > .pin1" />
+      </board>,
+    )
+
+    setCircuitJson(circuit.getCircuitJson() as any)
+
+    return circuit
+  }, [])
+
+  // Use useEffect for side effects after render
+  useEffect(() => {
+    circuit.renderUntilSettled().then(() => {
+      setCircuitJson(circuit.getCircuitJson() as any)
+    })
+  }, [circuit])
+
+  return <PCBViewer circuitJson={circuitJson as any} />
+}
+
+const meta: Meta<typeof DRCerrors> = {
+  title: "DRC errors",
+  component: DRCerrors,
+}
+
+export default meta


Reviews:
user: login: seveibar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T19:13:20Z
user: login: seveibar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T19:14:16Z
user: login: seveibar
body: amazing /tip $10

but for the sake of AI slinging code around we should fix the casing
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T19:14:58Z

Review comments:
diff_hunk: @@ -122,12 +122,14 @@ export const ToolbarOverlay = ({ children, elements }: Props) => {
     is_showing_rats_nest,
     is_showing_multiple_traces_length,
     is_showing_autorouting,
+    is_showing_DRC_errors,
path: src/components/ToolbarOverlay.tsx
user: login: seveibar
body: ```suggestion
    is_showing_drc_errors,
```
created_at: 2025-04-26T19:13:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -331,6 +334,13 @@ export const ToolbarOverlay = ({ children, elements }: Props) => {
                     setIsShowingAutorouting(!is_showing_autorouting)
                   }}
                 />
+                <CheckboxMenuItem
+                  label="Show DRC Errors"
+                  checked={is_showing_DRC_errors}
+                  onClick={() => {
+                    setIsShowingDRCErrors(!is_showing_DRC_errors)
path: src/components/ToolbarOverlay.tsx
user: login: seveibar
body: ```suggestion
                    setIsShowingDrcErrors(!is_showing_drc_errors)
```

we use google-casing https://github.com/tscircuit/handbook/blob/main/guides/code.md#4-casing
created_at: 2025-04-26T19:14:16Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-26T17:32:21Z
body: [vc]: #YMOyNGRtw3DSxuoI7jDDbnyIYQMBG5GWOa13qeakTCQ=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJwY2Itdmlld2VyIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RzY2lyY3VpdC9wY2Itdmlld2VyLzI5RGFKVUE0RXNESjVUZkh5TkpCRmROVmpZeEQiLCJwcmV2aWV3VXJsIjoicGNiLXZpZXdlci1naXQtZm9yay1hYnNlMjAwMS1tYWluLXRzY2lyY3VpdC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InBjYi12aWV3ZXItZ2l0LWZvcmstYWJzZTIwMDEtbWFpbi10c2NpcmN1aXQudmVyY2VsLmFwcCJ9LCJyb290RGlyZWN0b3J5IjpudWxsfV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **pcb-viewer** | ✅ Ready ([Inspect](https://vercel.com/tscircuit/pcb-viewer/29DaJUA4EsDJ5TfHyNJBFdNVjYxD)) | [Visit Preview](https://pcb-viewer-git-fork-abse2001-main-tscircuit.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/pcb-viewer-git-fork-abse2001-main-tscircuit.vercel.app?via=pr-comment-feedback-link) | Apr 27, 2025 0:07am |


author_association: NONE
user: login: algora-pbc[bot]
created_at: 2025-04-26T19:15:06Z
body: 🎉🎈 @Abse2001 has been awarded **$10** by **tscircuit**! 🎈🎊
author_association: NONE