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 @@ - +