@tscircuit/props 0.0.682 → 0.0.684

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ import type { AutocompleteString } from "lib/common/autocomplete"
2
+ import { z } from "zod"
3
+
4
+ export type BoardMountRotationAnchor = AutocompleteString<
5
+ "topedge" | "bottomedge" | "leftedge" | "rightedge"
6
+ >
7
+
8
+ export type BoardMountOrientation =
9
+ | "top_layer_toward_mount_face"
10
+ | "bottom_layer_toward_mount_face"
11
+
12
+ const identityPathPattern =
13
+ "[A-Za-z_][A-Za-z0-9_-]*(?:\\.[A-Za-z_][A-Za-z0-9_-]*)*"
14
+ const referencePattern =
15
+ "[A-Za-z_][A-Za-z0-9_-]*(?:\\.[A-Za-z_][A-Za-z0-9_-]*)+"
16
+ const reference = new RegExp(`^${referencePattern}$`)
17
+ const anchor = new RegExp(`^${identityPathPattern}$`)
18
+ const expression = new RegExp(
19
+ `^calc\\(\\s*(${referencePattern})\\s*(?:([+-])\\s*((?:\\d+(?:\\.\\d*)?|\\.\\d+))\\s*(degcw|degccw))?\\s*\\)$`,
20
+ )
21
+
22
+ export const boardMountRotation = z
23
+ .string()
24
+ .trim()
25
+ .superRefine((value, ctx) => {
26
+ if (reference.test(value)) return
27
+ const match = expression.exec(value)
28
+ if (!match) {
29
+ ctx.addIssue({
30
+ code: z.ZodIssueCode.custom,
31
+ message:
32
+ "Expected a qualified mounting reference or calc(reference +/- Ndegcw|Ndegccw)",
33
+ })
34
+ return
35
+ }
36
+ if (match[3] !== undefined && !Number.isFinite(Number(match[3]))) {
37
+ ctx.addIssue({
38
+ code: z.ZodIssueCode.custom,
39
+ message: "Mount rotation angle must be finite",
40
+ })
41
+ }
42
+ })
43
+
44
+ export const boardMountRotationAnchor = z
45
+ .custom<BoardMountRotationAnchor>((value) => typeof value === "string", {
46
+ message: "Expected a board edge or component identity path",
47
+ })
48
+ .transform((value) => value.trim())
49
+ .refine((value) => anchor.test(value), {
50
+ message: "Expected a board edge or component identity path",
51
+ })
52
+
53
+ export const boardMountOrientation = z.enum([
54
+ "top_layer_toward_mount_face",
55
+ "bottom_layer_toward_mount_face",
56
+ ])
@@ -4,6 +4,7 @@ import { assemblyMotorProps } from "./motor"
4
4
  import { assemblyScreenProps } from "./screen"
5
5
  import { assemblySubassemblyProps } from "./subassembly"
6
6
 
7
+ export * from "./board-mounting"
7
8
  export * from "./device"
8
9
  export * from "./motor"
9
10
  export * from "./screen"
@@ -23,6 +23,8 @@ export interface AssemblyMotorProps {
23
23
  * shaft. Defaults to "z+", the native shaft axis of the NEMA models.
24
24
  */
25
25
  shaftFacingDirection?: CadModelAxisDirection
26
+ /** e.g. "jst-ph-6". */
27
+ wireConnection?: "none" | "stubs" | "jst-ph-6"
26
28
  }
27
29
 
28
30
  export const assemblyMotorProps = z
@@ -34,8 +36,17 @@ export const assemblyMotorProps = z
34
36
  standard: z.enum(["nema8", "nema17", "nema23"]).optional(),
35
37
  model: z.string().trim().min(1).optional(),
36
38
  shaftFacingDirection: cadModelAxisDirection.default("z+"),
39
+ wireConnection: z.enum(["none", "stubs", "jst-ph-6"]).optional(),
37
40
  })
38
41
  .superRefine((motor, context) => {
42
+ if (motor.model !== undefined && motor.wireConnection !== undefined) {
43
+ context.addIssue({
44
+ code: z.ZodIssueCode.custom,
45
+ message:
46
+ "Set wireConnection in the custom model string, or use standard",
47
+ path: ["wireConnection"],
48
+ })
49
+ }
39
50
  if (motor.standard === undefined && motor.model === undefined) {
40
51
  context.addIssue({
41
52
  code: z.ZodIssueCode.custom,
@@ -0,0 +1,125 @@
1
+ import { resistance } from "circuit-json"
2
+ import { z } from "zod"
3
+ import { expectTypesMatch } from "lib/typecheck"
4
+
5
+ /** Scalar ohm target or a tolerance string, e.g. "50±25ohm".
6
+ * Input remains XML-compatible. Component parsing expands tolerance strings
7
+ * to the scalar target and its Min/Max sibling props, all in ohms. */
8
+ export type ImpedanceTarget = number | string
9
+ export const positiveImpedance = z
10
+ .union([
11
+ z.number(),
12
+ z
13
+ .string()
14
+ .refine(
15
+ (value) => !/(?:±|\+\/-)/.test(value),
16
+ "Use a scalar impedance for explicit bounds",
17
+ ),
18
+ ])
19
+ .pipe(resistance)
20
+ .pipe(z.number().positive().finite())
21
+ const quantity = /^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)\s*(.*?)$/i
22
+ const tolerantImpedance = z
23
+ .string()
24
+ .regex(/(?:±|\+\/-)/)
25
+ .transform((value, ctx) => {
26
+ const parts = value.trim().split(/\s*(?:±|\+\/-)\s*/)
27
+ const nominal = parts.length === 2 ? parts[0]!.match(quantity) : null
28
+ const tolerance = parts.length === 2 ? parts[1]!.match(quantity) : null
29
+ if (!nominal || !tolerance) {
30
+ ctx.addIssue({
31
+ code: z.ZodIssueCode.custom,
32
+ message: "Use a nominal impedance ± an absolute ohm tolerance",
33
+ })
34
+ return z.NEVER
35
+ }
36
+ const ohmUnit = /^(?:[yzafpnumkKMGTPEZYµμ])?(?:ohms?|Ω)$/i
37
+ if (
38
+ (nominal[2] && !ohmUnit.test(nominal[2])) ||
39
+ (tolerance[2] && !ohmUnit.test(tolerance[2]))
40
+ ) {
41
+ ctx.addIssue({
42
+ code: z.ZodIssueCode.custom,
43
+ message:
44
+ "Use an absolute ohm tolerance, not a percentage or another unit",
45
+ })
46
+ return z.NEVER
47
+ }
48
+ const nominalValue = resistance.safeParse(
49
+ `${nominal[1]}${nominal[2] || tolerance[2] || "ohm"}`,
50
+ )
51
+ const toleranceValue = resistance.safeParse(
52
+ `${tolerance[1]}${tolerance[2] || nominal[2] || "ohm"}`,
53
+ )
54
+ if (
55
+ !nominalValue.success ||
56
+ !toleranceValue.success ||
57
+ !Number.isFinite(nominalValue.data) ||
58
+ !Number.isFinite(toleranceValue.data) ||
59
+ nominalValue.data <= 0 ||
60
+ toleranceValue.data < 0
61
+ ) {
62
+ ctx.addIssue({
63
+ code: z.ZodIssueCode.custom,
64
+ message:
65
+ "Nominal impedance must be positive and tolerance nonnegative, in ohms",
66
+ })
67
+ return z.NEVER
68
+ }
69
+ return {
70
+ nominal: nominalValue.data,
71
+ min: nominalValue.data - toleranceValue.data,
72
+ max: nominalValue.data + toleranceValue.data,
73
+ }
74
+ })
75
+ .pipe(
76
+ z.object({
77
+ nominal: z.number().positive().finite(),
78
+ min: z.number().positive().finite(),
79
+ max: z.number().positive().finite(),
80
+ }),
81
+ )
82
+ const scalarImpedance = positiveImpedance
83
+ export const impedanceTarget = z.union([tolerantImpedance, scalarImpedance])
84
+ expectTypesMatch<ImpedanceTarget, z.input<typeof impedanceTarget>>(true)
85
+
86
+ /** Explicit bounds may accompany a scalar target. When both range notations
87
+ * are supplied, they must agree; no conflicting bounds are silently replaced. */
88
+ export function validateImpedanceTarget(
89
+ target: z.output<typeof impedanceTarget> | undefined,
90
+ min: number | undefined,
91
+ max: number | undefined,
92
+ ctx: z.RefinementCtx,
93
+ propName: string,
94
+ ) {
95
+ if (min !== undefined && max !== undefined && min > max)
96
+ ctx.addIssue({
97
+ code: z.ZodIssueCode.custom,
98
+ path: [`${propName}Max`],
99
+ message: "Maximum impedance cannot be below minimum impedance",
100
+ })
101
+ if (typeof target === "object") {
102
+ for (const [suffix, explicit, derived] of [
103
+ ["Min", min, target.min],
104
+ ["Max", max, target.max],
105
+ ] as const)
106
+ if (
107
+ explicit !== undefined &&
108
+ Math.abs(explicit - derived) > 1e-9 * Math.max(1, Math.abs(derived))
109
+ )
110
+ ctx.addIssue({
111
+ code: z.ZodIssueCode.custom,
112
+ path: [`${propName}${suffix}`],
113
+ message:
114
+ "Explicit impedance bound conflicts with the target tolerance",
115
+ })
116
+ } else if (
117
+ target !== undefined &&
118
+ ((min !== undefined && target < min) || (max !== undefined && target > max))
119
+ )
120
+ ctx.addIssue({
121
+ code: z.ZodIssueCode.custom,
122
+ path: [propName],
123
+ message: "Target impedance must lie within its declared bounds",
124
+ })
125
+ }
@@ -0,0 +1,102 @@
1
+ import { distance } from "circuit-json"
2
+ import { z } from "zod"
3
+ import { expectTypesMatch } from "lib/typecheck"
4
+
5
+ /** A length measured from the selected signals' pad endpoints, in board-world
6
+ * XY mm (+X right, +Y up). of accepts trace/port/bus/pair selectors. Without
7
+ * of, use the current members and any lengthMatchTo members. */
8
+ export interface RelativeRouteLength {
9
+ reference: "longest_manhattan"
10
+ of?: string[]
11
+ /** Offset added to the reference distance. Raw numbers are mm; may be negative. */
12
+ offset?: number | string
13
+ }
14
+ export type RouteLength = number | string | RelativeRouteLength
15
+ export const nonnegativeRouteDistance = distance.pipe(
16
+ z.number().nonnegative().finite(),
17
+ )
18
+ export const routeLength = z.union([
19
+ nonnegativeRouteDistance,
20
+ z
21
+ .object({
22
+ reference: z.literal("longest_manhattan"),
23
+ of: z.array(z.string().min(1)).min(1).optional(),
24
+ offset: distance.pipe(z.number().finite()).optional(),
25
+ })
26
+ .strict(),
27
+ ])
28
+ expectTypesMatch<RouteLength, z.input<typeof routeLength>>(true)
29
+
30
+ /** Validate comparable bounds without resolving selectors or geometry here.
31
+ * Relative values with different references remain independent constraints. */
32
+ export function validateRouteLengths(
33
+ props: {
34
+ minLength?: z.output<typeof routeLength>
35
+ maxLength?: z.output<typeof routeLength>
36
+ targetLength?: z.output<typeof routeLength>
37
+ lengthTolerance?: number
38
+ lengthMatchTo?: string | string[]
39
+ maxLengthSkew?: number
40
+ },
41
+ ctx: z.RefinementCtx,
42
+ ) {
43
+ if (
44
+ Boolean(props.targetLength !== undefined) !==
45
+ Boolean(props.lengthTolerance !== undefined)
46
+ )
47
+ ctx.addIssue({
48
+ code: z.ZodIssueCode.custom,
49
+ path: ["lengthTolerance"],
50
+ message: "targetLength and lengthTolerance must be supplied together",
51
+ })
52
+ if (props.lengthMatchTo !== undefined && props.maxLengthSkew === undefined)
53
+ ctx.addIssue({
54
+ code: z.ZodIssueCode.custom,
55
+ path: ["maxLengthSkew"],
56
+ message: "lengthMatchTo requires an explicit maxLengthSkew",
57
+ })
58
+ const comparable = (value: z.output<typeof routeLength>) =>
59
+ typeof value === "number"
60
+ ? { basis: "absolute", amount: value }
61
+ : {
62
+ basis: JSON.stringify([
63
+ value.reference,
64
+ value.of ? [...value.of].sort() : null,
65
+ ]),
66
+ amount: value.offset ?? 0,
67
+ }
68
+ const min =
69
+ props.minLength === undefined ? undefined : comparable(props.minLength)
70
+ const max =
71
+ props.maxLength === undefined ? undefined : comparable(props.maxLength)
72
+ const target =
73
+ props.targetLength === undefined
74
+ ? undefined
75
+ : comparable(props.targetLength)
76
+ if (min && max && min.basis === max.basis && min.amount > max.amount)
77
+ ctx.addIssue({
78
+ code: z.ZodIssueCode.custom,
79
+ path: ["maxLength"],
80
+ message: "maxLength cannot be below minLength for the same reference",
81
+ })
82
+ if (
83
+ (target &&
84
+ min &&
85
+ target.basis === min.basis &&
86
+ target.amount + (props.lengthTolerance ?? 0) < min.amount) ||
87
+ (target &&
88
+ max &&
89
+ target.basis === max.basis &&
90
+ target.amount - (props.lengthTolerance ?? 0) > max.amount)
91
+ )
92
+ ctx.addIssue({
93
+ code: z.ZodIssueCode.custom,
94
+ path: ["targetLength"],
95
+ message:
96
+ "targetLength tolerance window does not intersect the declared length bounds",
97
+ })
98
+ }
99
+ export const lengthMatchTo = z.union([
100
+ z.string().min(1),
101
+ z.array(z.string().min(1)).min(1),
102
+ ])
@@ -0,0 +1,18 @@
1
+ import { distance } from "circuit-json"
2
+ import { z } from "zod"
3
+ import { expectTypesMatch } from "lib/typecheck"
4
+
5
+ /** Centreline distance in mm, or a multiple of the larger local trace width
6
+ * written as e.g. "3w". Parsed multiples remain {widthMultiplier: 3} so the
7
+ * checker can evaluate varying widths without choosing a width in props. */
8
+ export type TraceSpacing = number | string
9
+ const widthSpacing = z
10
+ .string()
11
+ .regex(/^\s*(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\s*w\s*$/i)
12
+ .transform((value) => ({
13
+ widthMultiplier: Number(value.trim().replace(/w$/i, "")),
14
+ }))
15
+ .pipe(z.object({ widthMultiplier: z.number().positive().finite() }))
16
+ const absoluteSpacing = distance.pipe(z.number().positive().finite())
17
+ export const traceSpacing = z.union([widthSpacing, absoluteSpacing])
18
+ expectTypesMatch<TraceSpacing, z.input<typeof traceSpacing>>(true)
@@ -5,6 +5,13 @@ import { type Point, point } from "lib/common/point"
5
5
  import { expectTypesMatch } from "lib/typecheck"
6
6
  import { z } from "zod"
7
7
  import { subcircuitGroupProps, type SubcircuitGroupProps } from "./group"
8
+ import {
9
+ boardMountOrientation,
10
+ boardMountRotation,
11
+ boardMountRotationAnchor,
12
+ type BoardMountOrientation,
13
+ type BoardMountRotationAnchor,
14
+ } from "../assembly/board-mounting"
8
15
 
9
16
  const boardColorPresets = [
10
17
  "not_specified",
@@ -78,17 +85,16 @@ export const boardOutlinePoint = z
78
85
  export interface BoardProps
79
86
  extends Omit<SubcircuitGroupProps, "subcircuit" | "connections" | "outline"> {
80
87
  title?: string
81
- /**
82
- * Assembly mounting target, such as "NEMA17.backface". Trimmed; omitted
83
- * leaves the board unattached. The target face is resolved by the consumer.
84
- */
88
+ /** e.g. "NEMA17.backface". */
85
89
  mountedTo?: string
86
- /**
87
- * Nonnegative finite clearance between the target mounting face and the
88
- * nearest PCB surface, in millimeters or a unit string. Parsed to mm.
89
- * Omitted leaves the gap unset; only meaningful when mountedTo is supplied.
90
- */
90
+ /** Clearance from the mounting face, e.g. "6mm". */
91
91
  mountGap?: Distance
92
+ /** e.g. "J_USB" or "rightedge". */
93
+ mountRotationAnchor?: BoardMountRotationAnchor
94
+ /** e.g. "calc(NEMA17.wireside-90degcw)". */
95
+ mountRotation?: string
96
+ /** e.g. "top_layer_toward_mount_face". */
97
+ mountOrientation?: BoardMountOrientation
92
98
  /** Fabricator preset, preserved as supplied. Omitted leaves the preset unset. */
93
99
  fabricatorPreset?:
94
100
  | "jlcpcb_economy"
@@ -169,15 +175,20 @@ export const boardProps = subcircuitGroupProps
169
175
  .trim()
170
176
  .min(1)
171
177
  .optional()
172
- .describe(
173
- 'Assembly mounting target, such as "NEMA17.backface". Trimmed; omitted leaves the board unattached. The target face is resolved by the consumer.',
174
- ),
178
+ .describe('e.g. "NEMA17.backface".'),
175
179
  mountGap: distance
176
180
  .pipe(z.number().nonnegative().finite())
177
181
  .optional()
178
- .describe(
179
- "Nonnegative finite clearance between the target mounting face and the nearest PCB surface, in millimeters or a unit string, parsed to mm. Omitted leaves the gap unset; only meaningful when mountedTo is supplied.",
180
- ),
182
+ .describe('Clearance from the mounting face, e.g. "6mm".'),
183
+ mountRotationAnchor: boardMountRotationAnchor
184
+ .optional()
185
+ .describe('e.g. "J_USB" or "rightedge".'),
186
+ mountRotation: boardMountRotation
187
+ .optional()
188
+ .describe('e.g. "calc(NEMA17.wireside-90degcw)".'),
189
+ mountOrientation: boardMountOrientation
190
+ .optional()
191
+ .describe('e.g. "top_layer_toward_mount_face".'),
181
192
  fabricatorPreset: z
182
193
  .enum([
183
194
  "jlcpcb_economy",
@@ -1,10 +1,19 @@
1
- import { expectTypesMatch } from "lib/typecheck"
2
1
  import {
3
- distance,
4
- layer_ref,
5
- resistance,
6
- type LayerRefInput,
7
- } from "circuit-json"
2
+ routeLength,
3
+ lengthMatchTo,
4
+ nonnegativeRouteDistance,
5
+ validateRouteLengths,
6
+ type RouteLength,
7
+ } from "../common/routeLength"
8
+ import { traceSpacing, type TraceSpacing } from "../common/traceSpacing"
9
+ import {
10
+ impedanceTarget,
11
+ positiveImpedance,
12
+ validateImpedanceTarget,
13
+ type ImpedanceTarget,
14
+ } from "../common/impedanceTarget"
15
+ import { expectTypesMatch } from "lib/typecheck"
16
+ import { distance, layer_ref, type LayerRefInput } from "circuit-json"
8
17
  import { z } from "zod"
9
18
 
10
19
  export type BusName = string
@@ -15,14 +24,34 @@ export type BusName = string
15
24
  */
16
25
  export interface BusProps {
17
26
  name?: string
27
+ /** Match these trace/port/bus/pair selectors as well as this element's members.
28
+ * maxLengthSkew applies to the combined members; no tolerance is inferred. */
29
+ lengthMatchTo?: string | string[]
30
+ /** Minimum/maximum pad-to-pad planar length for each member. Raw numbers are mm. */
31
+ minLength?: RouteLength
32
+ maxLength?: RouteLength
33
+ /** Nominal member length, absolute or relative to selected endpoints.
34
+ * Requires an explicit lengthTolerance, including zero for exact matching. */
35
+ targetLength?: RouteLength
36
+ /** Allowed deviation above/below targetLength, in mm. */
37
+ lengthTolerance?: number | string
38
+ /** Minimum centreline spacing between bus members, excluding declared pair partners.
39
+ * Raw numbers are mm; "3w" means three times the larger local trace width. */
40
+ pcbTraceSpacing?: TraceSpacing
41
+ /** Minimum centreline spacing to traces outside this bus/pair (mm or e.g. "4w"). */
42
+ pcbSpacingToOtherSignals?: TraceSpacing
43
+
18
44
  /** One or more trace names or port selectors for the connections in the bus. */
19
45
  connections: string[]
20
46
  /** If set, every trace in this bus is assigned to this autorouting phase. */
21
47
  routingPhaseIndex?: number | null
22
48
  /** Maximum routed-length difference between bus members. Raw numbers are millimeters. */
23
49
  maxLengthSkew?: number | string
24
- /** Intended single-ended characteristic impedance. Raw numbers are ohms. */
25
- targetImpedance?: number | string
50
+ /** Intended single-ended impedance, e.g. "50±25ohm". Raw numbers are ohms. */
51
+ targetImpedance?: ImpedanceTarget
52
+ /** Inclusive minimum/maximum acceptable impedance, in ohms. May be used without a nominal target. */
53
+ targetImpedanceMin?: number | string
54
+ targetImpedanceMax?: number | string
26
55
  /** Explicit PCB trace width for every bus member. Raw numbers are millimeters. */
27
56
  pcbTraceWidth?: number | string
28
57
  /** PCB layers on which the bus may be routed. */
@@ -33,17 +62,54 @@ export interface BusProps {
33
62
  preferredLayers?: LayerRefInput[]
34
63
  }
35
64
 
36
- export const busProps = z.object({
37
- name: z.string().optional(),
38
- connections: z.array(z.string()).min(1),
39
- routingPhaseIndex: z.number().nullable().optional(),
40
- maxLengthSkew: distance.pipe(z.number().min(0).finite()).optional(),
41
- targetImpedance: resistance.pipe(z.number().positive().finite()).optional(),
42
- pcbTraceWidth: distance.pipe(z.number().positive().finite()).optional(),
43
- pcbAllowedLayers: z.array(layer_ref).min(1).optional(),
44
- preferredLayer: layer_ref.optional(),
45
- preferredLayers: z.array(layer_ref).min(1).optional(),
46
- })
65
+ export const busProps = z
66
+ .object({
67
+ name: z.string().optional(),
68
+ lengthMatchTo: lengthMatchTo.optional(),
69
+ minLength: routeLength.optional(),
70
+ maxLength: routeLength.optional(),
71
+ targetLength: routeLength.optional(),
72
+ lengthTolerance: nonnegativeRouteDistance.optional(),
73
+ pcbTraceSpacing: traceSpacing.optional(),
74
+ pcbSpacingToOtherSignals: traceSpacing.optional(),
75
+
76
+ connections: z.array(z.string()).min(1),
77
+ routingPhaseIndex: z.number().nullable().optional(),
78
+ maxLengthSkew: distance.pipe(z.number().min(0).finite()).optional(),
79
+ targetImpedance: impedanceTarget.optional(),
80
+ targetImpedanceMin: positiveImpedance.optional(),
81
+ targetImpedanceMax: positiveImpedance.optional(),
82
+ pcbTraceWidth: distance.pipe(z.number().positive().finite()).optional(),
83
+ pcbAllowedLayers: z.array(layer_ref).min(1).optional(),
84
+ preferredLayer: layer_ref.optional(),
85
+ preferredLayers: z.array(layer_ref).min(1).optional(),
86
+ })
87
+ .superRefine((props, ctx) => {
88
+ validateRouteLengths(props, ctx)
89
+ validateImpedanceTarget(
90
+ props.targetImpedance,
91
+ props.targetImpedanceMin,
92
+ props.targetImpedanceMax,
93
+ ctx,
94
+ "targetImpedance",
95
+ )
96
+ })
97
+ .transform(
98
+ ({
99
+ targetImpedance,
100
+ ...props
101
+ }): typeof props & { targetImpedance?: number } => {
102
+ if (targetImpedance === undefined) return props
103
+ if (typeof targetImpedance === "number")
104
+ return { ...props, targetImpedance }
105
+ return {
106
+ ...props,
107
+ targetImpedance: targetImpedance.nominal,
108
+ targetImpedanceMin: targetImpedance.min,
109
+ targetImpedanceMax: targetImpedance.max,
110
+ }
111
+ },
112
+ )
47
113
 
48
114
  type InferredBusProps = z.input<typeof busProps>
49
115
  expectTypesMatch<BusProps, InferredBusProps>(true)
@@ -1,5 +1,19 @@
1
+ import {
2
+ routeLength,
3
+ lengthMatchTo,
4
+ nonnegativeRouteDistance,
5
+ validateRouteLengths,
6
+ type RouteLength,
7
+ } from "../common/routeLength"
8
+ import { traceSpacing, type TraceSpacing } from "../common/traceSpacing"
9
+ import {
10
+ impedanceTarget,
11
+ positiveImpedance,
12
+ validateImpedanceTarget,
13
+ type ImpedanceTarget,
14
+ } from "../common/impedanceTarget"
1
15
  import { expectTypesMatch } from "lib/typecheck"
2
- import { distance, resistance } from "circuit-json"
16
+ import { distance } from "circuit-json"
3
17
  import { z } from "zod"
4
18
 
5
19
  /**
@@ -8,31 +22,82 @@ import { z } from "zod"
8
22
  */
9
23
  export interface DifferentialPairProps {
10
24
  name?: string
25
+ /** Match these trace/port/bus/pair selectors as well as this element's members.
26
+ * maxLengthSkew applies to the combined members; no tolerance is inferred. */
27
+ lengthMatchTo?: string | string[]
28
+ /** Minimum/maximum pad-to-pad planar length for each member. Raw numbers are mm. */
29
+ minLength?: RouteLength
30
+ maxLength?: RouteLength
31
+ /** Nominal member length, absolute or relative to selected endpoints.
32
+ * Requires an explicit lengthTolerance, including zero for exact matching. */
33
+ targetLength?: RouteLength
34
+ /** Allowed deviation above/below targetLength, in mm. */
35
+ lengthTolerance?: number | string
36
+ /** Minimum centreline spacing to traces outside this bus/pair (mm or e.g. "4w"). */
37
+ pcbSpacingToOtherSignals?: TraceSpacing
38
+
11
39
  /** Name of the trace or pin carrying the positive signal. */
12
40
  positiveConnection: string
13
41
  /** Name of the trace or pin carrying the negative signal. */
14
42
  negativeConnection: string
15
43
  /** Maximum permitted routed-length skew. Raw numbers are millimeters. */
16
44
  maxLengthSkew?: number | string
17
- /** Intended differential characteristic impedance. Raw numbers are ohms. */
18
- targetDifferentialImpedance?: number | string
45
+ /** Intended differential impedance, e.g. "100±10ohm". Raw numbers are ohms. */
46
+ targetDifferentialImpedance?: ImpedanceTarget
47
+ /** Inclusive minimum/maximum acceptable impedance, in ohms. May be used without a nominal target. */
48
+ targetDifferentialImpedanceMin?: number | string
49
+ targetDifferentialImpedanceMax?: number | string
19
50
  /** Edge-to-edge PCB copper gap between the pair. Raw numbers are millimeters. */
20
51
  pcbTraceGap?: number | string
21
52
  /** Maximum length over which the pair may be routed without coupling. Raw numbers are millimeters. */
22
53
  maxUncoupledLength?: number | string
23
54
  }
24
55
 
25
- export const differentialPairProps = z.object({
26
- name: z.string().optional(),
27
- positiveConnection: z.string(),
28
- negativeConnection: z.string(),
29
- maxLengthSkew: distance.pipe(z.number().min(0).finite()).optional(),
30
- targetDifferentialImpedance: resistance
31
- .pipe(z.number().positive().finite())
32
- .optional(),
33
- pcbTraceGap: distance.pipe(z.number().positive().finite()).optional(),
34
- maxUncoupledLength: distance.pipe(z.number().min(0).finite()).optional(),
35
- })
56
+ export const differentialPairProps = z
57
+ .object({
58
+ name: z.string().optional(),
59
+ lengthMatchTo: lengthMatchTo.optional(),
60
+ minLength: routeLength.optional(),
61
+ maxLength: routeLength.optional(),
62
+ targetLength: routeLength.optional(),
63
+ lengthTolerance: nonnegativeRouteDistance.optional(),
64
+ pcbSpacingToOtherSignals: traceSpacing.optional(),
65
+
66
+ positiveConnection: z.string(),
67
+ negativeConnection: z.string(),
68
+ maxLengthSkew: distance.pipe(z.number().min(0).finite()).optional(),
69
+ targetDifferentialImpedance: impedanceTarget.optional(),
70
+ targetDifferentialImpedanceMin: positiveImpedance.optional(),
71
+ targetDifferentialImpedanceMax: positiveImpedance.optional(),
72
+ pcbTraceGap: distance.pipe(z.number().positive().finite()).optional(),
73
+ maxUncoupledLength: distance.pipe(z.number().min(0).finite()).optional(),
74
+ })
75
+ .superRefine((props, ctx) => {
76
+ validateRouteLengths(props, ctx)
77
+ validateImpedanceTarget(
78
+ props.targetDifferentialImpedance,
79
+ props.targetDifferentialImpedanceMin,
80
+ props.targetDifferentialImpedanceMax,
81
+ ctx,
82
+ "targetDifferentialImpedance",
83
+ )
84
+ })
85
+ .transform(
86
+ ({
87
+ targetDifferentialImpedance,
88
+ ...props
89
+ }): typeof props & { targetDifferentialImpedance?: number } => {
90
+ if (targetDifferentialImpedance === undefined) return props
91
+ if (typeof targetDifferentialImpedance === "number")
92
+ return { ...props, targetDifferentialImpedance }
93
+ return {
94
+ ...props,
95
+ targetDifferentialImpedance: targetDifferentialImpedance.nominal,
96
+ targetDifferentialImpedanceMin: targetDifferentialImpedance.min,
97
+ targetDifferentialImpedanceMax: targetDifferentialImpedance.max,
98
+ }
99
+ },
100
+ )
36
101
 
37
102
  type InferredDifferentialPairProps = z.input<typeof differentialPairProps>
38
103
  expectTypesMatch<DifferentialPairProps, InferredDifferentialPairProps>(true)
package/lib/index.ts CHANGED
@@ -151,3 +151,14 @@ export * from "./common/fanoutProps"
151
151
  export * from "./common/implicitBreakoutPointSolver"
152
152
 
153
153
  export * from "./common/fanoutTracePath"
154
+
155
+ export {
156
+ routeLength,
157
+ type RouteLength,
158
+ type RelativeRouteLength,
159
+ } from "./common/routeLength"
160
+ export {
161
+ traceSpacing,
162
+ type TraceSpacing,
163
+ } from "./common/traceSpacing"
164
+ export { impedanceTarget, type ImpedanceTarget } from "./common/impedanceTarget"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/props",
3
- "version": "0.0.682",
3
+ "version": "0.0.684",
4
4
  "description": "Props for tscircuit builtin component types",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",