Skip to content
Merged
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,17 @@ export default () => (
/>
)
```

Right-click a schematic and select **Run Style Analysis** to analyze the current
Circuit JSON across all sheets. A modal displays one annotated SVG per placement
or style issue, or a message when no issues are found. Analysis runs locally in
the browser and loads on demand. Close the modal and run the command again after
editing the circuit to get fresh results.

The analyzer is fetched at runtime from
`https://jscdn.tscircuit.com/@tscircuit/circuit-json-schematic-placement-analysis/latest/dist/browser.js`.
It is not included in the viewer bundle. This requires a published analyzer
release and network access to jscdn; the host page's Content Security Policy must
allow scripts from that origin. jscdn caches `latest` for up to ten minutes, and
the browser reuses an imported module until the page reloads. Circuit JSON stays
in the browser. The analyzer's GitHub dev dependency is used only by tests.
152 changes: 136 additions & 16 deletions bun.lock

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions examples/example36-style-analysis.fixture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { SchematicViewer } from "../lib/components/SchematicViewer"
import { renderToCircuitJson } from "../lib/dev/render-to-circuit-json"

const circuitJson = renderToCircuitJson(
<board width="10mm" height="10mm">
<resistor name="R1" resistance="1k" schX={0} />
<resistor name="R2" resistance="2k" schX={0.2} />
<capacitor name="C1" capacitance="1uF" schX={3} />
</board>,
)

export default () => (
<div>
<p>
Right-click the schematic and choose Run Style Analysis to inspect the
overlapping resistors.
</p>
<SchematicViewer
circuitJson={circuitJson}
containerStyle={{ height: "80vh" }}
/>
</div>
)
13 changes: 13 additions & 0 deletions lib/components/SchematicViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { StyleAnalysisDialog } from "./StyleAnalysisDialog"
import { su } from "@tscircuit/soup-util"
import type { CircuitJson, SchematicSheet } from "circuit-json"
import {
Expand Down Expand Up @@ -152,6 +153,8 @@ export const SchematicViewer = ({
)

const [showGridInternal, setShowGridInternal] = useState(false)
const [analysisCircuitJson, setAnalysisCircuitJson] =
useState<CircuitJson | null>(null)
const [showWarnings, setShowWarnings] = useState(false)
const showGrid = debugGrid || showGridInternal
const [isInteractionEnabled, setIsInteractionEnabled] = useState<boolean>(
Expand Down Expand Up @@ -624,13 +627,23 @@ export const SchematicViewer = ({
</div>
</div>
)}
{analysisCircuitJson && (
<StyleAnalysisDialog
circuitJson={analysisCircuitJson}
onClose={() => setAnalysisCircuitJson(null)}
/>
)}
{menuVisible && (
<ViewMenu
circuitJson={circuitJson}
circuitJsonKey={circuitJsonKey}
menuRef={menuRef}
menuPos={menuPos}
onOpenChange={setMenuVisible}
onRunStyleAnalysis={() => {
setMenuVisible(false)
setAnalysisCircuitJson(structuredClone(circuitJson))
}}
showPorts={showSchematicPortsInternal}
onTogglePorts={(value) => {
setShowSchematicPortsInternal(value)
Expand Down
154 changes: 154 additions & 0 deletions lib/components/StyleAnalysisDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import * as Dialog from "@radix-ui/react-dialog"
import {
styleAnalyzerLoader,
type StyleAnalysisArtifact,
} from "../utils/load-style-analyzer"
import type { CircuitJson } from "circuit-json"
import { useEffect, useState } from "react"
import { zIndexMap } from "../utils/z-index-map"

type AnalysisState =
| { status: "loading" }
| { status: "error"; message: string }
| { status: "complete"; artifacts: StyleAnalysisArtifact[] }

export const StyleAnalysisDialog = ({
circuitJson,
onClose,
}: {
circuitJson: CircuitJson
onClose: () => void
}) => {
const [state, setState] = useState<AnalysisState>({ status: "loading" })

useEffect(() => {
let cancelled = false
// Let the dialog paint before loading and running the analyzer.
const timer = window.setTimeout(async () => {
try {
const { createSchematicPlacementIssueArtifacts } =
await styleAnalyzerLoader.load()
if (cancelled) return
const artifacts = createSchematicPlacementIssueArtifacts(circuitJson)
if (!cancelled) setState({ status: "complete", artifacts })
} catch (error) {
if (!cancelled) {
setState({
status: "error",
message: error instanceof Error ? error.message : String(error),
})
}
}
}, 0)
return () => {
cancelled = true
window.clearTimeout(timer)
}
}, [circuitJson])

return (
<Dialog.Root open onOpenChange={(open) => !open && onClose()}>
<Dialog.Portal>
<Dialog.Overlay
style={{
position: "fixed",
inset: 0,
background: "#0008",
zIndex: zIndexMap.styleAnalysis,
}}
/>
<Dialog.Content
style={{
position: "fixed",
top: "5vh",
left: "50%",
transform: "translateX(-50%)",
width: "min(1000px, 94vw)",
maxHeight: "90vh",
overflowY: "auto",
boxSizing: "border-box",
padding: 24,
borderRadius: 12,
background: "#fafafa",
color: "#171717",
fontFamily: "system-ui, sans-serif",
boxShadow: "0 20px 60px #0005",
zIndex: zIndexMap.styleAnalysis,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 16,
}}
>
<Dialog.Title style={{ margin: 0, fontSize: 22 }}>
Style Analysis
</Dialog.Title>
<Dialog.Close style={{ cursor: "pointer", padding: "6px 12px" }}>
Close
</Dialog.Close>
</div>
<Dialog.Description style={{ color: "#525252" }}>
Placement and style issues across all schematic sheets. Each image
highlights one issue with its description below.
</Dialog.Description>
{state.status === "loading" && (
<p role="status">Running style analysis…</p>
)}
{state.status === "error" && (
<p role="alert">Style analysis failed: {state.message}</p>
)}
{state.status === "complete" && (
<>
<p role="status">
{state.artifacts.length === 0
? "No style issues found."
: `${state.artifacts.length} style ${state.artifacts.length === 1 ? "issue" : "issues"} found.`}
</p>
{state.artifacts.map((artifact) => (
<section
key={artifact.issueIndex}
style={{
marginTop: 24,
border: "1px solid #ddd",
borderRadius: 8,
overflow: "hidden",
background: "white",
}}
>
<h3 style={{ margin: 16, fontSize: 16 }}>
{artifact.issueIndex + 1}.{" "}
{artifact.issue.lineItemType.replace(
/([a-z])([A-Z])/g,
"$1 $2",
)}
{artifact.schematicSheetId && (
<small
style={{
display: "block",
marginTop: 4,
color: "#525252",
}}
>
Sheet: {artifact.schematicSheetId}
</small>
)}
</h3>
<img
src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(artifact.content)}`}
alt={artifact.descriptionXml}
loading="lazy"
style={{ display: "block", width: "100%", height: "auto" }}
/>
</section>
))}
</>
)}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}
11 changes: 11 additions & 0 deletions lib/components/ViewMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface ViewMenuProps {
menuRef: React.RefObject<HTMLDivElement | null>
menuPos: { x: number; y: number }
onOpenChange: (open: boolean) => void
onRunStyleAnalysis: () => void
showGroups: boolean
onToggleGroups: (show: boolean) => void
showGrid: boolean
Expand Down Expand Up @@ -99,6 +100,7 @@ export const ViewMenu = ({
menuRef,
menuPos,
onOpenChange,
onRunStyleAnalysis,
showGroups,
onToggleGroups,
showGrid,
Expand Down Expand Up @@ -236,6 +238,15 @@ export const ViewMenu = ({
<span>Show Warnings</span>
</DropdownMenu.CheckboxItem>

<DropdownMenu.Separator style={separatorStyles} />
<DropdownMenu.Item
className="sv-vm-item"
style={itemStyles}
onSelect={onRunStyleAnalysis}
>
<span style={iconSlotStyles} />
<span>Run Style Analysis</span>
</DropdownMenu.Item>
<DropdownMenu.Separator style={separatorStyles} />

<div
Expand Down
33 changes: 33 additions & 0 deletions lib/utils/load-style-analyzer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { CircuitJson } from "circuit-json"

export interface StyleAnalysisArtifact {
issueIndex: number
issue: { lineItemType: string }
schematicSheetId?: string
descriptionXml: string
content: string
}

export interface StyleAnalyzer {
createSchematicPlacementIssueArtifacts: (
circuitJson: CircuitJson,
) => StyleAnalysisArtifact[]
}

export const STYLE_ANALYZER_URL =
"https://jscdn.tscircuit.com/@tscircuit/circuit-json-schematic-placement-analysis/latest/dist/browser.js"

export const styleAnalyzerLoader = {
async load(): Promise<StyleAnalyzer> {
// Leave this URL import to the browser, including in downstream Vite/Webpack apps.
const analyzer = await import(
/* @vite-ignore */ /* webpackIgnore: true */ STYLE_ANALYZER_URL
)
if (typeof analyzer.createSchematicPlacementIssueArtifacts !== "function") {
throw new Error(
"The style analyzer module does not export its analysis function.",
)
}
return analyzer
},
}
1 change: 1 addition & 0 deletions lib/utils/z-index-map.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const zIndexMap = {
contextMenu: 110,
styleAnalysis: 120,
viewMenu: 55,
viewMenuIcon: 48,
clickToInteractOverlay: 100,
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@
"semver": "^7.7.2",
"tscircuit": "^0.0.2460",
"tsup": "^8.3.5",
"vite": "^6.0.3"
"vite": "^6.0.3",
"@tscircuit/circuit-json-schematic-placement-analysis": "https://codeload.github.com/tscircuit/circuit-json-schematic-placement-analysis/tar.gz/refs/heads/main"
},
"peerDependencies": {
"typescript": "^5.0.0",
"tscircuit": "*"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.23",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@tscircuit/circuit-json-util": "^0.0.108",
"circuit-json": "^0.0.479",
Expand Down
Loading
Loading