Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,57 @@ Grouped bullets use `bullet3_d3.5mm` (three male/female contact pairs) or
`bullet3_d3.5mm_afemale_bmale`. The object form adds `pinCount: 3`; counts 1–16
are supported and default to 1. Each contact has its own insulated wire,
spaced at nominal diameter + 2 mm, with distinct display colors. The contact
count and diameter must match at both ends.
Contact counts must match at both ends.

All modifiers are named and can appear in any order: `d` specifies the nominal
diameter, `a` specifies end A's gender, and `b` specifies end B's gender.
For example, `bullet3_bmale_d3.5mm_afemale` is equivalent to
`bullet3_d3.5mm_afemale_bmale`. Each end's gender is optional and defaults
independently. Positional modifiers, unknown names, and duplicate parameters
are rejected.

### Independent-ended adapter cables

The generic wrapper names two independently specified connectors:

```text
adaptercable_a(bullet3_d3.5mm_gfemale)_b(bullet3_d4mm_gfemale)
adaptercable_a(bullet3_d3.5mm_gfemale)_b(jst_ph_pins3)
adaptercable_a(jst_sh_pins4)_b(jst_ph_pins4)
```

The first example mates with a motor's three 3.5 mm male outputs and a board's
4 mm male plugs. Use `gmale` at B for female board sockets. `g` names a single
connector's gender; `d` names its diameter. Both modifiers are required for
bullet connector strings. A and B may appear in either order; duplicates,
missing ends, positional parameters, nested cables, and trailing tokens fail.
There are no `da`/`db` parameters or `diameterA`/`diameterB` object properties.

Each end is parsed by `parseConnectorString`. Supported connector strings are
`bullet[CONTACTS]_dNmm_gmale|gfemale`, `jst_sh[_pinsN]`, `jst_ph[_pinsN]`,
`usb_c`, `nema_5_15p`, and `iec_c13`. `us_mains` is a two-ended cable preset,
not a connector name. Explicit contact counts must match. The string describes
physical mating interfaces, not electrical pin mapping or ratings.

```ts
getCableDefinition({
standard: "adaptercable",
connectorA: parseConnectorString("bullet3_d3.5mm_gfemale"),
connectorB: parseConnectorString("bullet3_d4mm_gfemale"),
})
```

`getAdapterCableDefinition({ connectorA, connectorB, crossSection? })` exposes
the same composition API. An optional cross section configures wire sizes and
colors. Defaults use separate wires for two explicitly counted multi-contact
ends, or a round jacket otherwise. Each end retains its native contact pitch;
for different pitches, the renderer keeps a compact middle bundle and fans out
within the last 20 mm at either end. Short routes use shorter transitions,
preserving a compact middle third. The supplied route and connector poses stay intact.
Existing cable preset strings and their snapshots remain unchanged.

`stringifyConnector` and `stringifyCableDefinition` emit canonical stock strings;
custom dimensions and cross sections stay in the object API. The optional
`useShorthand` serializer flag emits existing cable presets when applicable.

![Generic adapters with independent bullet and JST connectors](tests/__snapshots__/bullet-adapter-cables.snap.png)
6 changes: 4 additions & 2 deletions lib/bullet-connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ export function getBulletConnector({
diameter,
gender,
pinCount = 1,
pitch = diameter + 2,
}: {
diameter: BulletDiameter
gender: BulletGender
pinCount?: number
pitch?: number
}) {
const contactDepth = diameter * 2
const bodyDiameter = diameter + (gender === "female" ? 1 : 0.6)
Expand All @@ -35,8 +37,8 @@ export function getBulletConnector({
diameter,
contactDepth,
pinCount,
pitch: diameter + 2,
bodyWidth: bodyDiameter + (pinCount - 1) * (diameter + 2),
pitch,
bodyWidth: bodyDiameter + (pinCount - 1) * pitch,
bodyHeight: bodyDiameter,
bodyDepth: contactDepth + diameter * 1.5,
}
Expand Down
64 changes: 61 additions & 3 deletions lib/cable-definition.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod"
import { bulletDiameterSchema } from "./bullet-connector"
import { cableStandardSchema } from "./cable-input"
import { cableStandardSchema } from "./cable-standard"

const dimension = z.number().finite().positive()
const color = z.string().regex(/^#[0-9a-fA-F]{6}$/)
Expand Down Expand Up @@ -96,6 +96,7 @@ export const cableDefinitionSchema = z
jst_ph: ["jst_ph_housing", "jst_ph_housing"],
us_mains: ["nema_5_15p", "iec_c13"],
bullet: [connectorA.kind, connectorB.kind],
adaptercable: [connectorA.kind, connectorB.kind],
}[cable.standard]
if (connectorA.kind !== expected[0] || connectorB.kind !== expected[1]) {
ctx.addIssue({
Expand All @@ -115,7 +116,7 @@ export const cableDefinitionSchema = z
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"Bullet connectors require matching nominal diameters and contact counts",
"Bullet connectors require supported nominal diameters and matching contact counts",
})
}
if (
Expand Down Expand Up @@ -153,12 +154,61 @@ export const cableDefinitionSchema = z
})
}
}
} else if ("diameter" in connectorA || "diameter" in connectorB) {
} else if (
cable.standard !== "adaptercable" &&
("diameter" in connectorA || "diameter" in connectorB)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Bullet connectors require the bullet cable standard",
})
}
if (cable.standard === "adaptercable") {
if (
"pinCount" in connectorA &&
"pinCount" in connectorB &&
connectorA.pinCount !== connectorB.pinCount
)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Adapter connector pin counts must match",
})
if (
"pinCount" in connectorA &&
"pinCount" in connectorB &&
connectorA.pinCount > 1 &&
crossSection.kind !== "wire_bundle"
)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Adapter cables require one wire per explicit contact",
})
for (const connector of [connectorA, connectorB]) {
if (
"diameter" in connector &&
(connector.bodyHeight <= connector.diameter ||
connector.pitch < connector.bodyHeight ||
Math.abs(
connector.bodyWidth -
(connector.bodyHeight +
(connector.pinCount - 1) * connector.pitch),
) > 1e-6 ||
connector.contactDepth >= connector.bodyDepth ||
(connector.pinCount === 1 &&
(crossSection.kind !== "round_jacket" ||
crossSection.diameter > connector.bodyHeight)) ||
(connector.pinCount > 1 &&
(crossSection.kind !== "wire_bundle" ||
crossSection.wires.some(
(wire) => wire.diameter > connector.bodyHeight,
))))
)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Bullet contact, body, and wire dimensions must fit",
})
}
}
if (crossSection.kind === "wire_bundle") {
for (const connector of [connectorA, connectorB]) {
if (
Expand All @@ -170,6 +220,14 @@ export const cableDefinitionSchema = z
message: "Bundle wire count must match each connector pin count",
})
}
if (
"pitch" in connector &&
crossSection.wires.some((wire) => wire.diameter > connector.pitch)
)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Insulated wires must fit each connector pitch",
})
}
if (
crossSection.wires.some(
Expand Down
24 changes: 16 additions & 8 deletions lib/cable-input.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { z } from "zod"
import { bulletDiameterSchema, bulletGenderSchema } from "./bullet-connector"
import {
cableConnectorSchema,
cableCrossSectionSchema,
} from "./cable-definition"
export { cableStandardSchema } from "./cable-standard"
export type { CableStandard } from "./cable-standard"

export const cableStandardSchema = z.enum([
"usb_c",
"jst_sh",
"jst_ph",
"us_mains",
"bullet",
])
export type CableStandard = z.infer<typeof cableStandardSchema>
export const adapterCableInputSchema = z
.object({
standard: z.literal("adaptercable"),
connectorA: cableConnectorSchema,
connectorB: cableConnectorSchema,
crossSection: cableCrossSectionSchema.optional(),
})
.strict()
export type AdapterCableInput = z.input<typeof adapterCableInputSchema>

const color = z.string().regex(/^#[0-9a-fA-F]{6}$/)
const jacket = {
Expand Down Expand Up @@ -51,6 +58,7 @@ export const cableInputSchema = z.discriminatedUnion("standard", [
jacketDiameter: jacket.jacketDiameter.default(6.2),
})
.strict(),
adapterCableInputSchema,
])

export type CableInput = z.input<typeof cableInputSchema>
Expand Down
11 changes: 11 additions & 0 deletions lib/cable-standard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { z } from "zod"

export const cableStandardSchema = z.enum([
"usb_c",
"jst_sh",
"jst_ph",
"us_mains",
"bullet",
"adaptercable",
])
export type CableStandard = z.infer<typeof cableStandardSchema>
56 changes: 56 additions & 0 deletions lib/get-adapter-cable-definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { cableDefinitionSchema, type CableDefinition } from "./cable-definition"
import { adapterCableInputSchema, type AdapterCableInput } from "./cable-input"

const wireColors = [
"#df4049",
"#263449",
"#e1b13c",
"#347dc9",
"#27a782",
"#af62bd",
]

/** Compose independent mating interfaces. Dimensions are connector-local mm;
* this describes the cable, never its world placement or route.
*/
export function getAdapterCableDefinition(
input: Omit<AdapterCableInput, "standard">,
): CableDefinition {
const cable = adapterCableInputSchema.parse({
standard: "adaptercable",
...input,
})
const { connectorA, connectorB } = cable
const bundled =
"pinCount" in connectorA &&
"pinCount" in connectorB &&
connectorA.pinCount > 1
const pitches = [connectorA, connectorB].flatMap((connector) =>
"pitch" in connector ? [connector.pitch] : [],
)
const wireDiameter = Math.min(2, ...pitches.map((pitch) => pitch * 0.6))
return cableDefinitionSchema.parse({
...cable,
crossSection:
cable.crossSection ??
(bundled
? {
kind: "wire_bundle",
wirePitch: Math.max(...pitches),
wires: Array.from({ length: connectorA.pinCount }, (_, index) => ({
diameter: wireDiameter,
color: wireColors[index % wireColors.length],
})),
}
: {
kind: "round_jacket",
diameter: Math.min(
4,
connectorA.bodyHeight,
connectorB.bodyHeight,
...pitches.map((pitch) => Math.min(2, pitch * 0.6)),
),
color: "#263449",
}),
})
}
7 changes: 6 additions & 1 deletion lib/get-cable-definition.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getBulletConnector } from "./bullet-connector"
import { getAdapterCableDefinition } from "./get-adapter-cable-definition"
import { type CableDefinition, cableDefinitionSchema } from "./cable-definition"
import { type CableInput, cableInputSchema } from "./cable-input"

Expand All @@ -13,18 +14,22 @@ const wireColors = [

export function getCableDefinition(cableInput: CableInput): CableDefinition {
const cable = cableInputSchema.parse(cableInput)
if (cable.standard === "adaptercable") return getAdapterCableDefinition(cable)
if (cable.standard === "bullet") {
const pitch = cable.diameter + 2
return cableDefinitionSchema.parse({
standard: "bullet",
connectorA: getBulletConnector({
diameter: cable.diameter,
gender: cable.genderA,
pinCount: cable.pinCount,
pitch,
}),
connectorB: getBulletConnector({
diameter: cable.diameter,
gender: cable.genderB,
pinCount: cable.pinCount,
pitch,
}),
crossSection:
cable.pinCount === 1
Expand All @@ -35,7 +40,7 @@ export function getCableDefinition(cableInput: CableInput): CableDefinition {
}
: {
kind: "wire_bundle",
wirePitch: cable.diameter + 2,
wirePitch: pitch,
wires: Array.from({ length: cable.pinCount }, (_, index) => ({
diameter: cable.wireDiameter,
color: wireColors[index % wireColors.length],
Expand Down
13 changes: 12 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
export { cableStandardSchema, cableInputSchema } from "./cable-input"
export {
cableStandardSchema,
cableInputSchema,
adapterCableInputSchema,
} from "./cable-input"
export type {
CableStandard,
CableInput,
NormalizedCableInput,
AdapterCableInput,
} from "./cable-input"
export {
cableConnectorSchema,
Expand All @@ -12,6 +17,12 @@ export {
export type { CableConnector, CableDefinition } from "./cable-definition"
export { getCableDefinition } from "./get-cable-definition"
export { parseCableString } from "./parse-cable-string"
export { parseConnectorString } from "./parse-connector-string"
export { getAdapterCableDefinition } from "./get-adapter-cable-definition"
export {
stringifyConnector,
stringifyCableDefinition,
} from "./stringify-cable-definition"

export {
bulletDiameterSchema,
Expand Down
Loading
Loading