diff --git a/bun.lockb b/bun.lockb
index 9388ddf..1438137 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/lib/sch/arial-text-metrics.ts b/lib/sch/arial-text-metrics.ts
new file mode 100644
index 0000000..c4264dd
--- /dev/null
+++ b/lib/sch/arial-text-metrics.ts
@@ -0,0 +1,763 @@
+// https://claude.ai/share/5e327d90-627f-48d5-9a51-ba6117479007
+export const arialTextMetrics = {
+ "0": {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ "1": {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -3,
+ right: 9,
+ },
+ "2": {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ "3": {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ "4": {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 12,
+ },
+ "5": {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ "6": {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ "7": {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ "8": {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ "9": {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ " ": {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 0,
+ },
+ "!": {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 5,
+ },
+ '"': {
+ width: 9,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 7,
+ },
+ "#": {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 13,
+ },
+ $: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ "%": {
+ width: 21,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 20,
+ },
+ "&": {
+ width: 16,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 15,
+ },
+ "'": {
+ width: 5,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 3,
+ },
+ "(": {
+ width: 8,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 7,
+ },
+ ")": {
+ width: 8,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 7,
+ },
+ "*": {
+ width: 9,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 8,
+ },
+ "+": {
+ width: 14,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 13,
+ },
+ ",": {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 5,
+ },
+ "-": {
+ width: 8,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 7,
+ },
+ ".": {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 5,
+ },
+ "/": {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 7,
+ },
+ ":": {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 5,
+ },
+ ";": {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 5,
+ },
+ "<": {
+ width: 14,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 13,
+ },
+ "=": {
+ width: 14,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 13,
+ },
+ ">": {
+ width: 14,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 13,
+ },
+ "?": {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ "@": {
+ width: 24,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 23,
+ },
+ A: {
+ width: 16,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 16,
+ },
+ B: {
+ width: 16,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 15,
+ },
+ C: {
+ width: 17,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 16,
+ },
+ D: {
+ width: 17,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 16,
+ },
+ E: {
+ width: 16,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 15,
+ },
+ F: {
+ width: 15,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 14,
+ },
+ G: {
+ width: 19,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 17,
+ },
+ H: {
+ width: 17,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 15,
+ },
+ I: {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 5,
+ },
+ J: {
+ width: 12,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 10,
+ },
+ K: {
+ width: 16,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 16,
+ },
+ L: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 12,
+ },
+ M: {
+ width: 20,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 18,
+ },
+ N: {
+ width: 17,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 15,
+ },
+ O: {
+ width: 19,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 18,
+ },
+ P: {
+ width: 16,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 15,
+ },
+ Q: {
+ width: 19,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 18,
+ },
+ R: {
+ width: 17,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 17,
+ },
+ S: {
+ width: 16,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 15,
+ },
+ T: {
+ width: 15,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 14,
+ },
+ U: {
+ width: 17,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 15,
+ },
+ V: {
+ width: 16,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 16,
+ },
+ W: {
+ width: 23,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 22,
+ },
+ X: {
+ width: 16,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 16,
+ },
+ Y: {
+ width: 16,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 16,
+ },
+ Z: {
+ width: 15,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 14,
+ },
+ "[": {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 6,
+ },
+ "\\": {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 7,
+ },
+ "]": {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 5,
+ },
+ "^": {
+ width: 11,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 11,
+ },
+ _: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 14,
+ },
+ "`": {
+ width: 8,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 5,
+ },
+ a: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ b: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 12,
+ },
+ c: {
+ width: 12,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ d: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ e: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ f: {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 8,
+ },
+ g: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ h: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 12,
+ },
+ i: {
+ width: 5,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 4,
+ },
+ j: {
+ width: 5,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 1,
+ right: 4,
+ },
+ k: {
+ width: 12,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 12,
+ },
+ l: {
+ width: 5,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 4,
+ },
+ m: {
+ width: 20,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 18,
+ },
+ n: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 12,
+ },
+ o: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ p: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 12,
+ },
+ q: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 12,
+ },
+ r: {
+ width: 8,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 8,
+ },
+ s: {
+ width: 12,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 11,
+ },
+ t: {
+ width: 7,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 6,
+ },
+ u: {
+ width: 13,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 12,
+ },
+ v: {
+ width: 12,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 12,
+ },
+ w: {
+ width: 17,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 17,
+ },
+ x: {
+ width: 12,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 12,
+ },
+ y: {
+ width: 12,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 12,
+ },
+ z: {
+ width: 12,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: 0,
+ right: 11,
+ },
+ "{": {
+ width: 8,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 7,
+ },
+ "|": {
+ width: 6,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -2,
+ right: 4,
+ },
+ "}": {
+ width: 8,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 7,
+ },
+ "~": {
+ width: 14,
+ height: 27,
+ ascent: 22,
+ descent: 5,
+ left: -1,
+ right: 13,
+ },
+}
diff --git a/lib/sch/convert-circuit-json-to-schematic-svg.ts b/lib/sch/convert-circuit-json-to-schematic-svg.ts
index 7e6c399..1535cb6 100644
--- a/lib/sch/convert-circuit-json-to-schematic-svg.ts
+++ b/lib/sch/convert-circuit-json-to-schematic-svg.ts
@@ -99,16 +99,6 @@ export function convertCircuitJsonToSchematicSvg(
)
}
- // Add labeled points if provided
- if (options?.labeledPoints) {
- svgChildren.push(
- drawSchematicLabeledPoints({
- points: options.labeledPoints,
- transform,
- }),
- )
- }
-
const schDebugObjectSvgs: SvgObject[] = []
const schComponentSvgs: SvgObject[] = []
const schTraceSvgs: SvgObject[] = []
@@ -142,6 +132,16 @@ export function convertCircuitJsonToSchematicSvg(
...schNetLabel,
)
+ // Add labeled points if provided
+ if (options?.labeledPoints) {
+ svgChildren.push(
+ drawSchematicLabeledPoints({
+ points: options.labeledPoints,
+ transform,
+ }),
+ )
+ }
+
const svgObject: SvgObject = {
name: "svg",
type: "element",
diff --git a/lib/sch/estimate-text-width.ts b/lib/sch/estimate-text-width.ts
new file mode 100644
index 0000000..5782269
--- /dev/null
+++ b/lib/sch/estimate-text-width.ts
@@ -0,0 +1,19 @@
+import { arialTextMetrics } from "./arial-text-metrics"
+
+export const estimateTextWidth = (text: string): number => {
+ if (!text) return 0
+
+ let totalWidth = 0
+ for (const char of text) {
+ const metrics = arialTextMetrics[char as keyof typeof arialTextMetrics]
+ if (metrics) {
+ totalWidth += metrics.width
+ } else {
+ // Default width for unknown characters
+ totalWidth += arialTextMetrics["?"].width
+ }
+ }
+
+ // Return width normalized to font size 1
+ return totalWidth / 27 // Normalize by font height from metrics
+}
diff --git a/lib/sch/svg-object-fns/create-svg-objects-for-sch-net-label.ts b/lib/sch/svg-object-fns/create-svg-objects-for-sch-net-label.ts
index 06713bc..efd535e 100644
--- a/lib/sch/svg-object-fns/create-svg-objects-for-sch-net-label.ts
+++ b/lib/sch/svg-object-fns/create-svg-objects-for-sch-net-label.ts
@@ -1,79 +1,141 @@
import type { AnyCircuitElement, SchematicNetLabel } from "circuit-json"
import type { SvgObject } from "lib/svg-object"
import { colorMap } from "lib/utils/colors"
-import { getSchFontSize } from "lib/utils/get-sch-font-size"
+import {
+ getSchMmFontSize,
+ getSchScreenFontSize,
+} from "lib/utils/get-sch-font-size"
import { getSchStrokeSize } from "lib/utils/get-sch-stroke-size"
-import { applyToPoint, type Matrix } from "transformation-matrix"
+import { getUnitVectorFromEdgeToOutside } from "lib/utils/get-unit-vector-from-edge-to-outside"
+import { getUnitVectorFromOutsideToEdge } from "lib/utils/get-unit-vector-from-outside-to-edge"
+import {
+ applyToPoint,
+ compose,
+ rotate,
+ scale,
+ translate,
+ type Matrix,
+} from "transformation-matrix"
+import { estimateTextWidth } from "../estimate-text-width"
+
+/**
+ * Arrow point width as a fraction of font size (Font Size Ratio)
+ */
+const ARROW_POINT_WIDTH_FSR = 0.3
+
+/**
+ * End padding as a fraction of font size (Font Size Ratio)
+ */
+const END_PADDING_FSR = 0.3
+const END_PADDING_EXTRA_PER_CHARACTER_FSR = 0.06
export const createSvgObjectsForSchNetLabel = (
schNetLabel: SchematicNetLabel,
- transform: Matrix,
+ realToScreenTransform: Matrix,
): SvgObject[] => {
+ if (!schNetLabel.text) return []
const svgObjects: SvgObject[] = []
+ const fontSizePx = getSchScreenFontSize(realToScreenTransform, "net_label")
+ const fontSizeMm = getSchMmFontSize("net_label")
+ const textWidthFSR = estimateTextWidth(schNetLabel.text || "")
+
// Transform the center position to screen coordinates
- const screenPos = applyToPoint(transform, schNetLabel.center)
+ const screenCenter = applyToPoint(realToScreenTransform, schNetLabel.center)
- // Get font size for text
- const fontSize = getSchFontSize(transform, "net_label")
+ const realTextGrowthVec = getUnitVectorFromOutsideToEdge(
+ schNetLabel.anchor_side,
+ )
- // Calculate label dimensions based on text
- const textWidth = (schNetLabel.text?.length || 0) * fontSize * 0.7
- const padding = fontSize * 0.5
- // Increase height to better accommodate text
- const screenLabelHeight = fontSize * 1.2
- const screenArrowPoint = fontSize * 0.3
- const screenLabelWidth = textWidth + padding * 2 + screenArrowPoint
+ const screenTextGrowthVec = { ...realTextGrowthVec }
+ screenTextGrowthVec.y *= -1 // Invert y direction because anchor_side is pre-transform
- // Get rotation angle based on anchor_side
- let rotation = 0
- let baseX = screenPos.x
- let baseY = screenPos.y
-
- switch (schNetLabel.anchor_side) {
- case "left":
- rotation = 0
- break
- case "right":
- rotation = 180
- baseX -= screenLabelWidth
- break
- case "top":
- rotation = -90
- baseX -= screenLabelHeight / 2
- baseY += screenLabelWidth / 2
- break
- case "bottom":
- rotation = 90
- baseX -= screenLabelHeight / 2
- baseY -= screenLabelWidth / 2
- break
- default:
- rotation = 0
+ const fullWidthFsr =
+ textWidthFSR +
+ ARROW_POINT_WIDTH_FSR * 2 +
+ END_PADDING_EXTRA_PER_CHARACTER_FSR * schNetLabel.text.length +
+ END_PADDING_FSR
+ const screenAnchorPosition = schNetLabel.anchor_position
+ ? applyToPoint(realToScreenTransform, schNetLabel.anchor_position)
+ : {
+ x:
+ screenCenter.x -
+ (screenTextGrowthVec.x * fullWidthFsr * fontSizePx) / 2,
+ y:
+ screenCenter.y -
+ (screenTextGrowthVec.y * fullWidthFsr * fontSizePx) / 2,
+ }
+ const realAnchorPosition = schNetLabel.anchor_position ?? {
+ x:
+ schNetLabel.center.x -
+ (realTextGrowthVec.x * fullWidthFsr * fontSizeMm) / 2,
+ y:
+ schNetLabel.center.y -
+ (realTextGrowthVec.y * fullWidthFsr * fontSizeMm) / 2,
}
- // Create transform string for rotation
- const transformString = `rotate(${rotation} ${screenPos.x} ${screenPos.y})`
+ // Get rotation angle based on anchor_side
+ const pathRotation = {
+ left: 0,
+ top: -90,
+ bottom: 90,
+ right: 180,
+ }[schNetLabel.anchor_side]
- // Calculate the points for the path
- const points = [
- { x: baseX, y: baseY }, // Left edge
- { x: baseX + screenLabelWidth - screenArrowPoint, y: baseY }, // Top edge
- { x: baseX + screenLabelWidth, y: baseY + screenLabelHeight / 2 }, // Arrow point
+ // Calculate the points for the outline
+ const screenOutlinePoints: Array<{ x: number; y: number }> = [
+ // Arrow point in font-relative coordinates
+ {
+ x: 0,
+ y: 0,
+ },
+ // Top left corner in font-relative coordinates
+ {
+ x: ARROW_POINT_WIDTH_FSR,
+ y: 0.6,
+ },
+ // Top right corner in font-relative coordinates
{
- x: baseX + screenLabelWidth - screenArrowPoint,
- y: baseY + screenLabelHeight,
- }, // Bottom after arrow
- { x: baseX, y: baseY + screenLabelHeight }, // Bottom left corner
- ]
+ x:
+ ARROW_POINT_WIDTH_FSR * 2 +
+ END_PADDING_FSR +
+ END_PADDING_EXTRA_PER_CHARACTER_FSR * schNetLabel.text.length +
+ textWidthFSR,
+ y: 0.6,
+ },
+ // Bottom right corner in font-relative coordinates
+ {
+ x:
+ ARROW_POINT_WIDTH_FSR * 2 +
+ END_PADDING_FSR +
+ END_PADDING_EXTRA_PER_CHARACTER_FSR * schNetLabel.text.length +
+ textWidthFSR,
+ y: -0.6,
+ },
+ // Bottom left corner in font-relative coordinates
+ {
+ x: ARROW_POINT_WIDTH_FSR,
+ y: -0.6,
+ },
+ ].map((fontRelativePoint) =>
+ applyToPoint(
+ compose(
+ realToScreenTransform,
+ translate(realAnchorPosition.x, realAnchorPosition.y),
+ scale(fontSizeMm),
+ rotate((pathRotation / 180) * Math.PI),
+ ),
+ fontRelativePoint,
+ ),
+ )
// Create the label path
const pathD = `
- M ${points[0]?.x},${points[0]?.y}
- L ${points[1]?.x},${points[1]?.y}
- L ${points[2]?.x},${points[2]?.y}
- L ${points[3]?.x},${points[3]?.y}
- L ${points[4]?.x},${points[4]?.y}
+ M ${screenOutlinePoints[0]!.x},${screenOutlinePoints[0]!.y}
+ L ${screenOutlinePoints[1]!.x},${screenOutlinePoints[1]!.y}
+ L ${screenOutlinePoints[2]!.x},${screenOutlinePoints[2]!.y}
+ L ${screenOutlinePoints[3]!.x},${screenOutlinePoints[3]!.y}
+ L ${screenOutlinePoints[4]!.x},${screenOutlinePoints[4]!.y}
Z
`
@@ -86,17 +148,30 @@ export const createSvgObjectsForSchNetLabel = (
d: pathD,
fill: "white",
stroke: colorMap.schematic.label_global,
- "stroke-width": `${getSchStrokeSize(transform)}px`,
- transform: transformString,
+ "stroke-width": `${getSchStrokeSize(realToScreenTransform)}px`,
},
value: "",
children: [],
})
- // Calculate text position (centered in label, accounting for arrow)
- const screenTextX = baseX + (screenLabelWidth - screenArrowPoint) / 2
- // Adjust text Y position for better vertical centering
- const screenTextY = baseY + screenLabelHeight / 2 + fontSize * 0.05
+ const screenTextPos = {
+ x: screenAnchorPosition.x + screenTextGrowthVec.x * fontSizePx * 0.5,
+ y: screenAnchorPosition.y + screenTextGrowthVec.y * fontSizePx * 0.5,
+ }
+
+ const textAnchor = {
+ left: "start",
+ top: "start",
+ bottom: "start",
+ right: "end",
+ }[schNetLabel.anchor_side]
+
+ const textTransformString = {
+ left: "",
+ right: "",
+ top: `rotate(90 ${screenTextPos.x} ${screenTextPos.y})`,
+ bottom: `rotate(-90 ${screenTextPos.x} ${screenTextPos.y})`,
+ }[schNetLabel.anchor_side]
// Add the label text
svgObjects.push({
@@ -104,14 +179,15 @@ export const createSvgObjectsForSchNetLabel = (
type: "element",
attributes: {
class: "net-label-text",
- x: screenTextX.toString(),
- y: screenTextY.toString(),
+ x: screenTextPos.x.toString(),
+ y: screenTextPos.y.toString(),
fill: colorMap.schematic.label_global,
- "text-anchor": "middle",
- "dominant-baseline": "central", // Changed to central for better vertical alignment
+ "text-anchor": textAnchor,
+ "dominant-baseline": "central",
"font-family": "sans-serif",
- "font-size": `${fontSize}px`,
- transform: transformString,
+ "font-variant-numeric": "tabular-nums",
+ "font-size": `${fontSizePx}px`,
+ transform: textTransformString,
},
children: [
{
diff --git a/lib/sch/svg-object-fns/create-svg-objects-for-sch-port-pin-label.ts b/lib/sch/svg-object-fns/create-svg-objects-for-sch-port-pin-label.ts
index a66dc4f..1734e09 100644
--- a/lib/sch/svg-object-fns/create-svg-objects-for-sch-port-pin-label.ts
+++ b/lib/sch/svg-object-fns/create-svg-objects-for-sch-port-pin-label.ts
@@ -5,7 +5,7 @@ import type {
} from "circuit-json"
import type { SvgObject } from "lib/svg-object"
import { colorMap } from "lib/utils/colors"
-import { getSchFontSize } from "lib/utils/get-sch-font-size"
+import { getSchScreenFontSize } from "lib/utils/get-sch-font-size"
import { getUnitVectorFromOutsideToEdge } from "lib/utils/get-unit-vector-from-outside-to-edge"
import { applyToPoint, type Matrix } from "transformation-matrix"
@@ -61,7 +61,7 @@ export const createSvgObjectsForSchPortPinLabel = (params: {
? "start"
: "end",
"dominant-baseline": "middle",
- "font-size": `${getSchFontSize(transform, "pin_number")}px`,
+ "font-size": `${getSchScreenFontSize(transform, "pin_number")}px`,
transform:
schPort.side_of_component === "top" ||
schPort.side_of_component === "bottom"
diff --git a/lib/sch/svg-object-fns/create-svg-objects-for-sch-port-pin-number-text.ts b/lib/sch/svg-object-fns/create-svg-objects-for-sch-port-pin-number-text.ts
index 1edfbfb..a2f67b0 100644
--- a/lib/sch/svg-object-fns/create-svg-objects-for-sch-port-pin-number-text.ts
+++ b/lib/sch/svg-object-fns/create-svg-objects-for-sch-port-pin-number-text.ts
@@ -5,7 +5,7 @@ import type {
} from "circuit-json"
import type { SvgObject } from "lib/svg-object"
import { colorMap } from "lib/utils/colors"
-import { getSchFontSize } from "lib/utils/get-sch-font-size"
+import { getSchScreenFontSize } from "lib/utils/get-sch-font-size"
import { getUnitVectorFromOutsideToEdge } from "lib/utils/get-unit-vector-from-outside-to-edge"
import { applyToPoint, type Matrix } from "transformation-matrix"
@@ -58,7 +58,7 @@ export const createSvgObjectsForSchPortPinNumberText = (params: {
fill: colorMap.schematic.pin_number,
"text-anchor": "middle",
"dominant-baseline": "auto",
- "font-size": `${getSchFontSize(transform, "pin_number")}px`,
+ "font-size": `${getSchScreenFontSize(transform, "pin_number")}px`,
transform:
schPort.side_of_component === "top" ||
schPort.side_of_component === "bottom"
diff --git a/lib/sch/svg-object-fns/create-svg-objects-from-sch-component-with-box.ts b/lib/sch/svg-object-fns/create-svg-objects-from-sch-component-with-box.ts
index 3473434..343b4fe 100644
--- a/lib/sch/svg-object-fns/create-svg-objects-from-sch-component-with-box.ts
+++ b/lib/sch/svg-object-fns/create-svg-objects-from-sch-component-with-box.ts
@@ -13,7 +13,7 @@ import { applyToPoint, type Matrix } from "transformation-matrix"
import { createSvgObjectsFromSchematicComponentWithSymbol } from "./create-svg-objects-from-sch-component-with-symbol"
import { createSvgObjectsFromSchPortOnBox } from "./create-svg-objects-from-sch-port-on-box"
import { getSchStrokeSize } from "lib/utils/get-sch-stroke-size"
-import { getSchFontSize } from "lib/utils/get-sch-font-size"
+import { getSchScreenFontSize } from "lib/utils/get-sch-font-size"
export const createSvgObjectsFromSchematicComponentWithBox = ({
component: schComponent,
@@ -62,7 +62,7 @@ export const createSvgObjectsFromSchematicComponentWithBox = ({
x: schComponent.center.x + schComponent.size.width / 2,
y: schComponent.center.y + schComponent.size.height / 2 + 0.5, // Above the component top edge
})
- const fontSizePx = getSchFontSize(transform, "manufacturer_number")
+ const fontSizePx = getSchScreenFontSize(transform, "manufacturer_number")
// Add manufacturer number and component name text
if (srcComponent?.manufacturer_part_number) {
diff --git a/lib/sch/svg-object-fns/create-svg-objects-from-sch-component-with-symbol.ts b/lib/sch/svg-object-fns/create-svg-objects-from-sch-component-with-symbol.ts
index c9375ae..539b6b3 100644
--- a/lib/sch/svg-object-fns/create-svg-objects-from-sch-component-with-symbol.ts
+++ b/lib/sch/svg-object-fns/create-svg-objects-from-sch-component-with-symbol.ts
@@ -17,7 +17,7 @@ import {
import { getSchStrokeSize } from "lib/utils/get-sch-stroke-size"
import { matchSchPortsToSymbolPorts } from "lib/utils/match-sch-ports-with-symbol-ports"
import { pointPairsToMatrix } from "lib/utils/point-pairs-to-matrix"
-import { getSchFontSize } from "lib/utils/get-sch-font-size"
+import { getSchScreenFontSize } from "lib/utils/get-sch-font-size"
import type { TextPrimitive } from "schematic-symbols"
const ninePointAnchorToTextAnchor: Record<
@@ -138,7 +138,7 @@ export const createSvgObjectsFromSchematicComponentWithSymbol = ({
"dominant-baseline": ninePointAnchorToDominantBaseline[text.anchor],
"text-anchor": ninePointAnchorToTextAnchor[text.anchor],
"font-family": "sans-serif",
- "font-size": `${getSchFontSize(realToScreenTransform, "reference_designator")}px`,
+ "font-size": `${getSchScreenFontSize(realToScreenTransform, "reference_designator")}px`,
},
value: "",
children: [
diff --git a/lib/utils/get-sch-font-size.ts b/lib/utils/get-sch-font-size.ts
index cb230cc..cae31f3 100644
--- a/lib/utils/get-sch-font-size.ts
+++ b/lib/utils/get-sch-font-size.ts
@@ -1,13 +1,19 @@
import type { Matrix } from "transformation-matrix"
import { applyToPoint } from "transformation-matrix"
-export const getSchFontSize = (
+type SchTextType =
+ | "pin_number"
+ | "reference_designator"
+ | "manufacturer_number"
+ | "net_label"
+
+export const getSchMmFontSize = (textType: SchTextType) => {
+ return textType === "pin_number" ? 0.15 : 0.18
+}
+
+export const getSchScreenFontSize = (
transform: Matrix,
- textType:
- | "pin_number"
- | "reference_designator"
- | "manufacturer_number"
- | "net_label",
+ textType: SchTextType,
) => {
- return Math.abs(transform.a) * (textType === "pin_number" ? 0.15 : 0.18)
+ return Math.abs(transform.a) * getSchMmFontSize(textType)
}
diff --git a/lib/utils/get-unit-vector-from-edge-to-outside.ts b/lib/utils/get-unit-vector-from-edge-to-outside.ts
new file mode 100644
index 0000000..d05ac63
--- /dev/null
+++ b/lib/utils/get-unit-vector-from-edge-to-outside.ts
@@ -0,0 +1,15 @@
+export const getUnitVectorFromEdgeToOutside = (
+ side: "top" | "bottom" | "left" | "right",
+) => {
+ switch (side) {
+ case "top":
+ return { x: 0, y: 1 }
+ case "bottom":
+ return { x: 0, y: -1 }
+ case "left":
+ return { x: -1, y: 0 }
+ case "right":
+ return { x: 1, y: 0 }
+ }
+ throw new Error(`Invalid side: ${side}`)
+}
diff --git a/package.json b/package.json
index de3f28a..55ca236 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
"vite-tsconfig-paths": "^5.0.1"
},
"peerDependencies": {
- "circuit-json": "^0.0.98"
+ "circuit-json": "^0.0.99"
},
"dependencies": {
"@tscircuit/footprinter": "^0.0.57",
diff --git a/tests/fixtures/get-test-fixture.ts b/tests/fixtures/get-test-fixture.ts
index 3c0a32f..893540a 100644
--- a/tests/fixtures/get-test-fixture.ts
+++ b/tests/fixtures/get-test-fixture.ts
@@ -1,10 +1,9 @@
import { Circuit } from "@tscircuit/core"
export const getTestFixture = () => {
- const project = new Circuit()
+ const circuit = new Circuit()
return {
- project,
- circuit: project,
+ circuit,
}
}
diff --git a/tests/sch/__snapshots__/net-label.snap.svg b/tests/sch/__snapshots__/net-label.snap.svg
index a47b2e3..0f8d51f 100644
--- a/tests/sch/__snapshots__/net-label.snap.svg
+++ b/tests/sch/__snapshots__/net-label.snap.svg
@@ -9,11 +9,81 @@
.pin-number { fill: rgb(169, 0, 0); }
.port-label { fill: rgb(0, 100, 100); }
.component-name { fill: rgb(0, 100, 100); }
- GND
\ No newline at end of file
+ -1,-1-1,0-1,1-1,2-1,3-1,4-1,50,-10,00,10,20,30,40,51,-11,01,11,21,31,41,52,-12,02,12,22,32,42,53,-13,03,13,23,33,43,54,-14,04,14,24,34,44,55,-15,05,15,25,35,45,5VCCGNDSDASCLshort labelWWWWWWWWWWsuperlong label. This is a long one!CENTER1CENTER2CENTER3CENTER4anchor_side=topanchor_side=bottomanchor_side=leftanchor_side=rightcenter1center2center3center4
\ No newline at end of file
diff --git a/tests/sch/net-label.test.tsx b/tests/sch/net-label.test.tsx
index 5469c6e..4bf1052 100644
--- a/tests/sch/net-label.test.tsx
+++ b/tests/sch/net-label.test.tsx
@@ -1,18 +1,117 @@
import { expect, test } from "bun:test"
+import type { SchematicNetLabel } from "circuit-json"
import { convertCircuitJsonToSchematicSvg } from "lib/index"
-import { getTestFixture } from "tests/fixtures/get-test-fixture"
test("schematic net label", () => {
- const { project } = getTestFixture()
-
- project.add(
-
-
- ,
- )
+ // NOTE: center isn't computed properly where anchor_position is defined but
+ // this doesn't affect the implementation, which prefers to use anchor_position.
+ // There are some net labels missing anchor_position intentionally to test
+ // computing the anchor position
+ const circuitJson: SchematicNetLabel[] = [
+ {
+ type: "schematic_net_label",
+ source_net_id: "net1",
+ center: { x: 0, y: 3 },
+ anchor_position: { x: 0, y: 3 },
+ anchor_side: "top",
+ text: "VCC",
+ },
+ {
+ type: "schematic_net_label",
+ source_net_id: "net2",
+ center: { x: 1, y: 3 },
+ anchor_position: { x: 1, y: 3 },
+ anchor_side: "bottom",
+ text: "GND",
+ },
+ {
+ type: "schematic_net_label",
+ source_net_id: "net3",
+ center: { x: 0, y: 4 },
+ anchor_position: { x: 0, y: 4 },
+ anchor_side: "left",
+ text: "SDA",
+ },
+ {
+ type: "schematic_net_label",
+ source_net_id: "net4",
+ center: { x: 2, y: 4 },
+ anchor_position: { x: 2, y: 4 },
+ anchor_side: "right",
+ text: "SCL",
+ },
+ {
+ type: "schematic_net_label",
+ source_net_id: "net5",
+ center: { x: 0, y: 1 },
+ anchor_position: { x: 0, y: 1 },
+ anchor_side: "left",
+ text: "short label",
+ },
+ {
+ type: "schematic_net_label",
+ source_net_id: "net6",
+ center: { x: 0, y: 0 },
+ anchor_position: { x: 0, y: 0 },
+ anchor_side: "left",
+ text: "WWWWWWWWWW", // W is a wide character
+ },
+ {
+ type: "schematic_net_label",
+ source_net_id: "net5",
+ center: { x: 2.5, y: 0 },
+ anchor_position: { x: 2.5, y: 0 },
+ anchor_side: "bottom",
+ text: "superlong label. This is a long one!",
+ },
+ // MISSING ANCHOR POSITION
+ {
+ type: "schematic_net_label",
+ source_net_id: "net5",
+ center: { x: 4, y: 1 },
+ anchor_side: "left",
+ text: "CENTER1",
+ },
+ {
+ type: "schematic_net_label",
+ source_net_id: "net5",
+ center: { x: 4, y: 2 },
+ anchor_side: "right",
+ text: "CENTER2",
+ },
+ {
+ type: "schematic_net_label",
+ source_net_id: "net5",
+ center: { x: 4, y: 3 },
+ anchor_side: "top",
+ text: "CENTER3",
+ },
+ {
+ type: "schematic_net_label",
+ source_net_id: "net5",
+ center: { x: 4, y: 4 },
+ anchor_side: "bottom",
+ text: "CENTER4",
+ },
+ ]
expect(
// @ts-ignore
- convertCircuitJsonToSchematicSvg(project.getCircuitJson()),
+ convertCircuitJsonToSchematicSvg(circuitJson, {
+ grid: {
+ cellSize: 1,
+ labelCells: true,
+ },
+ labeledPoints: [
+ { x: 0, y: 3, label: "anchor_side=top" },
+ { x: 1, y: 3, label: "anchor_side=bottom" },
+ { x: 0, y: 4, label: "anchor_side=left" },
+ { x: 2, y: 4, label: "anchor_side=right" },
+ { x: 4, y: 1, label: "center1" },
+ { x: 4, y: 2, label: "center2" },
+ { x: 4, y: 3, label: "center3" },
+ { x: 4, y: 4, label: "center4" },
+ ],
+ }),
).toMatchSvgSnapshot(import.meta.path)
})
diff --git a/tests/sch/resistor.test.tsx b/tests/sch/resistor.test.tsx
index 56db49b..38da687 100644
--- a/tests/sch/resistor.test.tsx
+++ b/tests/sch/resistor.test.tsx
@@ -3,9 +3,9 @@ import { convertCircuitJsonToSchematicSvg } from "lib/index"
import { getTestFixture } from "tests/fixtures/get-test-fixture"
test("schematic resistor", () => {
- const { project } = getTestFixture()
+ const { circuit } = getTestFixture()
- project.add(
+ circuit.add(
{
expect(
// @ts-ignore
convertCircuitJsonToSchematicSvg(
- project
+ circuit
.getCircuitJson()
// TEMPORARY HACK: until @tscircuit/core supports symbol_display_value
.map((elm) => {
diff --git a/tests/sch/trace-overlap.test.tsx b/tests/sch/trace-overlap.test.tsx
index 5cc4f6d..a1c5e11 100644
--- a/tests/sch/trace-overlap.test.tsx
+++ b/tests/sch/trace-overlap.test.tsx
@@ -3,9 +3,9 @@ import { convertCircuitJsonToSchematicSvg } from "lib"
import { getTestFixture } from "tests/fixtures/get-test-fixture"
test("schematic trace overlap", () => {
- const { project } = getTestFixture()
+ const { circuit } = getTestFixture()
- project.add(
+ circuit.add(
{
)
expect(
- convertCircuitJsonToSchematicSvg(project.getCircuitJson()),
+ convertCircuitJsonToSchematicSvg(circuit.getCircuitJson()),
).toMatchSvgSnapshot(import.meta.path)
})