diff --git a/lib/analyze-schematic-placement.ts b/lib/analyze-schematic-placement.ts
index a6df2c0..f6c33d4 100644
--- a/lib/analyze-schematic-placement.ts
+++ b/lib/analyze-schematic-placement.ts
@@ -7,6 +7,7 @@ import { SchematicBoxInnerLabelCollisionSolver } from "./solvers/SchematicBoxInn
import { SchematicBoxTooWideSolver } from "./solvers/SchematicBoxTooWideSolver/SchematicBoxTooWideSolver"
import { SchematicPinPaddingToEdgeSolver } from "./solvers/SchematicPinPaddingToEdgeSolver/SchematicPinPaddingToEdgeSolver"
import { DiodeResistorAlignmentSolver } from "./solvers/DiodeResistorAlignmentSolver/DiodeResistorAlignmentSolver"
+import { NetLabelOrientationSolver } from "./solvers/NetLabelOrientationSolver/NetLabelOrientationSolver"
import type {
SchematicBoxPlacementLineItem,
SchematicPlacementIssue,
@@ -55,6 +56,8 @@ export class SchematicPlacementAnalysis {
return SchematicPinPaddingToEdgeSolver.issueToString(issue)
case "DiodeResistorNotAligned":
return DiodeResistorAlignmentSolver.issueToString(issue)
+ case "NetLabelOrientationUnreadable":
+ return NetLabelOrientationSolver.issueToString(issue)
default:
return ""
}
diff --git a/lib/solvers/NetLabelOrientationSolver/NetLabelOrientationSolver.ts b/lib/solvers/NetLabelOrientationSolver/NetLabelOrientationSolver.ts
new file mode 100644
index 0000000..2f9cd67
--- /dev/null
+++ b/lib/solvers/NetLabelOrientationSolver/NetLabelOrientationSolver.ts
@@ -0,0 +1,116 @@
+import { BaseSolver } from "@tscircuit/solver-utils"
+import type { CircuitJson, SchematicNetLabel } from "circuit-json"
+import type {
+ SchematicPlacementIssue,
+ NetLabelOrientationUnreadable,
+} from "../../types"
+import type { SolverContext } from "../SolverContext"
+import { addAttr } from "../../utils/format"
+
+interface NetLabel {
+ schematicNetLabelId: string
+ text: string
+ centerX: number
+ centerY: number
+ anchorX?: number
+ anchorY?: number
+ anchorSide: "top" | "bottom" | "left" | "right"
+ angleDeg: number
+}
+
+export class NetLabelOrientationSolver extends BaseSolver {
+ private readonly netLabels: NetLabel[]
+ private currentIndex = 0
+ private readonly out: SchematicPlacementIssue[]
+
+ constructor({
+ ctx,
+ issues: out,
+ }: {
+ ctx: SolverContext
+ issues: SchematicPlacementIssue[]
+ }) {
+ super()
+ this.out = out
+ this.netLabels = this.computeNetLabels(ctx.circuitJson)
+ this.solved = this.netLabels.length === 0
+ }
+
+ override _step(): void {
+ const label = this.netLabels[this.currentIndex++]
+ if (!label) {
+ this.solved = true
+ return
+ }
+ this.solved = this.currentIndex >= this.netLabels.length
+
+ if (this.isUpsideDown(label.angleDeg)) {
+ this.out.push({
+ lineItemType: "NetLabelOrientationUnreadable",
+ schematicNetLabelId: label.schematicNetLabelId,
+ text: label.text,
+ anchorSide: label.anchorSide,
+ currentAngleDeg: label.angleDeg,
+ normalizedAngleDeg: this.normalizeAngle(label.angleDeg),
+ })
+ }
+ }
+
+ static issueToString(issue: NetLabelOrientationUnreadable): string {
+ const attrs: string[] = []
+ addAttr(attrs, "text", issue.text)
+ addAttr(attrs, "anchorSide", issue.anchorSide)
+ addAttr(attrs, "currentAngleDeg", issue.currentAngleDeg)
+ addAttr(attrs, "normalizedAngleDeg", issue.normalizedAngleDeg)
+ return ``
+ }
+
+ private isSchematicNetLabel(
+ el: CircuitJson[number],
+ ): el is SchematicNetLabel {
+ return el.type === "schematic_net_label"
+ }
+
+ private computeAngleDeg(
+ center: { x: number; y: number },
+ anchor: { x: number; y: number },
+ ): number {
+ return (
+ Math.atan2(anchor.y - center.y, anchor.x - center.x) * (180 / Math.PI)
+ )
+ }
+
+ private isUpsideDown(angleDeg: number): boolean {
+ return angleDeg < -90 || angleDeg > 90
+ }
+
+ private normalizeAngle(angleDeg: number): number {
+ const shifted = (angleDeg + 90) % 180
+ const positive = shifted < 0 ? shifted + 180 : shifted
+ return positive - 90
+ }
+
+ private computeNetLabels(circuitJson: CircuitJson): NetLabel[] {
+ const labels: NetLabel[] = []
+
+ for (const el of circuitJson) {
+ if (!this.isSchematicNetLabel(el)) continue
+ if (!el.anchor_position) continue
+
+ const angleDeg = this.computeAngleDeg(el.center, el.anchor_position)
+
+ labels.push({
+ schematicNetLabelId: el.schematic_net_label_id,
+ text: el.text,
+ centerX: el.center.x,
+ centerY: el.center.y,
+ anchorX: el.anchor_position.x,
+ anchorY: el.anchor_position.y,
+ anchorSide: el.anchor_side,
+ angleDeg,
+ })
+ }
+
+ return labels
+ }
+}
diff --git a/lib/solvers/SchematicPlacementPipeline/SchematicPlacementPipeline.ts b/lib/solvers/SchematicPlacementPipeline/SchematicPlacementPipeline.ts
index 9eb8c06..04ebca9 100644
--- a/lib/solvers/SchematicPlacementPipeline/SchematicPlacementPipeline.ts
+++ b/lib/solvers/SchematicPlacementPipeline/SchematicPlacementPipeline.ts
@@ -14,6 +14,7 @@ import { SchematicBoxTooWideSolver } from "../SchematicBoxTooWideSolver/Schemati
import { SchematicPinPaddingToEdgeSolver } from "../SchematicPinPaddingToEdgeSolver/SchematicPinPaddingToEdgeSolver"
import { VerboseNetLabelSolver } from "../VerboseNetLabelSolver/VerboseNetLabelSolver"
import { DiodeResistorAlignmentSolver } from "../DiodeResistorAlignmentSolver/DiodeResistorAlignmentSolver"
+import { NetLabelOrientationSolver } from "../NetLabelOrientationSolver/NetLabelOrientationSolver"
type SolverParams = { ctx: SolverContext; issues: SchematicPlacementIssue[] }
@@ -43,6 +44,13 @@ export class SchematicPlacementPipeline extends BasePipelineSolver
{ ctx: p.ctx, issues: p.issues },
],
),
+ definePipelineStep(
+ "NetLabelOrientationSolver",
+ NetLabelOrientationSolver,
+ (p: SchematicPlacementPipeline): [SolverParams] => [
+ { ctx: p.ctx, issues: p.issues },
+ ],
+ ),
definePipelineStep(
"SchematicBoxTooWideSolver",
SchematicBoxTooWideSolver,
diff --git a/lib/types.ts b/lib/types.ts
index a3f8f89..30c206f 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -111,6 +111,15 @@ export interface DiodeResistorNotAligned {
message: string
}
+export interface NetLabelOrientationUnreadable {
+ lineItemType: "NetLabelOrientationUnreadable"
+ schematicNetLabelId: string
+ text: string
+ anchorSide: SchematicSide
+ currentAngleDeg: number
+ normalizedAngleDeg: number
+}
+
export type SchematicPlacementIssue =
| ComponentOverlap
| SchematicBoxHasALotOfSurroundingWhitespace
@@ -120,6 +129,7 @@ export type SchematicPlacementIssue =
| SchematicBoxInnerLabelCollision
| SchematicPinPaddingToEdgeTooLarge
| DiodeResistorNotAligned
+ | NetLabelOrientationUnreadable
export interface SchematicPlacementIssues {
lineItemType: "SchematicPlacementIssues"
diff --git a/tests/assets/net-label-orientation-circuits.ts b/tests/assets/net-label-orientation-circuits.ts
new file mode 100644
index 0000000..2feac1a
--- /dev/null
+++ b/tests/assets/net-label-orientation-circuits.ts
@@ -0,0 +1,37 @@
+import type { CircuitJson } from "circuit-json"
+
+export const unreadableNetLabelCircuitJson: CircuitJson = [
+ {
+ type: "source_net",
+ source_net_id: "net_GND",
+ name: "GND",
+ member_source_group_ids: [],
+ },
+ {
+ type: "schematic_net_label",
+ schematic_net_label_id: "schematic_net_label_GND",
+ source_net_id: "net_GND",
+ center: { x: 0, y: 0 },
+ anchor_position: { x: -1, y: 1 },
+ anchor_side: "left",
+ text: "GND",
+ },
+]
+
+export const readableNetLabelCircuitJson: CircuitJson = [
+ {
+ type: "source_net",
+ source_net_id: "net_V3_3",
+ name: "V3_3",
+ member_source_group_ids: [],
+ },
+ {
+ type: "schematic_net_label",
+ schematic_net_label_id: "schematic_net_label_V3_3",
+ source_net_id: "net_V3_3",
+ center: { x: 0, y: 0 },
+ anchor_position: { x: 0, y: -1 },
+ anchor_side: "bottom",
+ text: "V3_3",
+ },
+]
diff --git a/tests/cases/__snapshots__/diode-resistor-misaligned.snap.svg b/tests/cases/__snapshots__/diode-resistor-misaligned.snap.svg
index b256f4c..c8b20ae 100644
--- a/tests/cases/__snapshots__/diode-resistor-misaligned.snap.svg
+++ b/tests/cases/__snapshots__/diode-resistor-misaligned.snap.svg
@@ -1,4 +1,4 @@
-