@tscircuit/props 0.0.683 → 0.0.685

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.
@@ -1,5 +1,6 @@
1
1
  import { assemblyCadAssemblyProps } from "./cadassembly"
2
2
  import { assemblyDeviceProps } from "./device"
3
+ import { assemblyPrintedPartProps } from "./printedpart"
3
4
  import { assemblyMotorProps } from "./motor"
4
5
  import { assemblyScreenProps } from "./screen"
5
6
  import { assemblySubassemblyProps } from "./subassembly"
@@ -7,6 +8,7 @@ import { assemblySubassemblyProps } from "./subassembly"
7
8
  export * from "./board-mounting"
8
9
  export * from "./device"
9
10
  export * from "./motor"
11
+ export * from "./printedpart"
10
12
  export * from "./screen"
11
13
  export * from "./subassembly"
12
14
  export * from "./cadassembly"
@@ -14,6 +16,7 @@ export * from "./cadassembly"
14
16
  export const assemblyProps = {
15
17
  device: assemblyDeviceProps,
16
18
  motor: assemblyMotorProps,
19
+ printedpart: assemblyPrintedPartProps,
17
20
  screen: assemblyScreenProps,
18
21
  subassembly: assemblySubassemblyProps,
19
22
  cadassembly: assemblyCadAssemblyProps,
@@ -0,0 +1,86 @@
1
+ import { type CadModelProp, cadModelProp } from "lib/common/cadModel"
2
+ import { url } from "lib/common/url"
3
+ import { distance, type Distance } from "lib/common/distance"
4
+ import { expectTypesMatch } from "lib/typecheck"
5
+ import { isValidElement, type ReactElement } from "react"
6
+ import { z } from "zod"
7
+
8
+ /** Provide exactly one of jscad, model, modelUrl, or cadModel. */
9
+ export interface AssemblyPrintedPartProps {
10
+ /** Stable identity used by assembly mounting selectors. */
11
+ name: string
12
+ displayName?: string
13
+ /** Pure jscad-fiber JSX in right-handed local XYZ, dimensions in millimeters.
14
+ * Named reference rectangles define attachment faces; they add no material.
15
+ * Hooks, async components, and raw kernel geometry are not supported.
16
+ */
17
+ jscad?: ReactElement
18
+ /** Modelprinter/footprinter string or model URL. */
19
+ model?: string
20
+ modelUrl?: string
21
+ cadModel?: CadModelProp
22
+ /** Assembly mounting target. */
23
+ mountedTo?: string
24
+ /** Name of this part's reference rectangle to mate with mountedTo.
25
+ * Both props must be supplied together; outward normals oppose and local
26
+ * in-plane X directions align. Without them the part uses its local origin.
27
+ */
28
+ mountFace?: string
29
+ /** Nonnegative surface clearance in mm or a unit string; defaults to zero.
30
+ * Requires mountedTo. Positive values separate the mating faces.
31
+ */
32
+ mountGap?: Distance
33
+ }
34
+
35
+ export const assemblyPrintedPartProps = z
36
+ .object({
37
+ name: z.string().trim().min(1),
38
+ displayName: z.string().optional(),
39
+ jscad: z
40
+ .custom<ReactElement>(isValidElement, "Expected jscad-fiber JSX")
41
+ .optional(),
42
+ model: z.string().trim().min(1).optional(),
43
+ modelUrl: url
44
+ .refine((value) => value.trim().length > 0, "modelUrl cannot be empty")
45
+ .optional(),
46
+ cadModel: cadModelProp.optional(),
47
+ mountedTo: z
48
+ .string()
49
+ .trim()
50
+ .regex(/^.+\.[^.]+$/, "Expected part.face")
51
+ .optional(),
52
+ mountFace: z.string().trim().min(1).optional(),
53
+ mountGap: distance.pipe(z.number().nonnegative().finite()).optional(),
54
+ })
55
+ .superRefine((part, ctx) => {
56
+ if (
57
+ [part.jscad, part.model, part.modelUrl, part.cadModel].filter(
58
+ (value) => value !== undefined,
59
+ ).length !== 1
60
+ ) {
61
+ ctx.addIssue({
62
+ code: z.ZodIssueCode.custom,
63
+ path: ["jscad"],
64
+ message: "Provide exactly one of jscad, model, modelUrl, or cadModel",
65
+ })
66
+ }
67
+ if ((part.mountedTo === undefined) !== (part.mountFace === undefined)) {
68
+ ctx.addIssue({
69
+ code: z.ZodIssueCode.custom,
70
+ path: ["mountFace"],
71
+ message: "Provide mountedTo and mountFace together",
72
+ })
73
+ }
74
+ if (part.mountGap !== undefined && part.mountedTo === undefined) {
75
+ ctx.addIssue({
76
+ code: z.ZodIssueCode.custom,
77
+ path: ["mountGap"],
78
+ message: "mountGap requires mountedTo",
79
+ })
80
+ }
81
+ })
82
+
83
+ export type AssemblyPrintedPartPropsInput = z.input<
84
+ typeof assemblyPrintedPartProps
85
+ >
86
+ expectTypesMatch<AssemblyPrintedPartProps, AssemblyPrintedPartPropsInput>(true)
@@ -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)
@@ -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.683",
3
+ "version": "0.0.685",
4
4
  "description": "Props for tscircuit builtin component types",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",