diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 0000000..171132e --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,16 @@ +name: PR package preview +on: + pull_request: +permissions: + contents: read +jobs: + preview: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.2 + - run: bun install + - run: bun run build + - run: bunx pkg-pr-new@0.0.37 publish --comment=off diff --git a/README.md b/README.md index ae096ca..f0ae73f 100644 --- a/README.md +++ b/README.md @@ -124,3 +124,45 @@ npm install @tscircuit/cableprinter The `bun-pver-release.yml` GitHub Actions workflow publishes releases through npm trusted publishing, with signed provenance and no npm token secret. + + +## Bullet connectors + +`bullet_d3.5mm` defaults to a male-to-female single-wire cable. Specify both end +genders with `bullet_d3.5mm_afemale_bmale`, `bullet_d4mm_amale_bmale`, or +`bullet_d4mm_afemale_bfemale`. Supported nominal mating diameters are 2, 3, 3.5, +4, 5, 5.5, 6, and 8 mm. Size is required; no connector diameter is inferred. + +```ts +getCableDefinition({ + standard: "bullet", + diameter: 3.5, + genderA: "male", + genderB: "female", + wireDiameter: 2, // insulation included, mm + color: "#df4049", +}) +``` + +End genders default independently to male at A and female at B; wire diameter +defaults to 2 mm and color to `#263449`. Dimensions describe representative +solder bullets with gold contacts, male spring slots, female socket recesses, +and solder cups. Nominal contact diameter differs from socket outer diameter. +They are visualization models, not manufacturer-specific fabrication drawings. +The separate bullet gallery pairs code with actual meshes for every supported +diameter and a three-contact cable. + +![Single and grouped bullet connectors](tests/__snapshots__/bullet-cables.snap.png) + +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. + +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. diff --git a/lib/bullet-connector.ts b/lib/bullet-connector.ts new file mode 100644 index 0000000..39d135c --- /dev/null +++ b/lib/bullet-connector.ts @@ -0,0 +1,43 @@ +import { z } from "zod" + +/** Nominal mating-contact diameters in mm, independent of the outer socket. */ +export const bulletDiameterSchema = z.union([ + z.literal(2), + z.literal(3), + z.literal(3.5), + z.literal(4), + z.literal(5), + z.literal(5.5), + z.literal(6), + z.literal(8), +]) +export type BulletDiameter = z.infer +export const bulletGenderSchema = z.enum(["male", "female"]) +export type BulletGender = z.infer + +/** Representative solder bullets, not manufacturer-specific production models. + * Right-handed connector-local mm: mating tip/mouth at z=0, wire exits +Z. + */ +export function getBulletConnector({ + diameter, + gender, + pinCount = 1, +}: { + diameter: BulletDiameter + gender: BulletGender + pinCount?: number +}) { + const contactDepth = diameter * 2 + const bodyDiameter = diameter + (gender === "female" ? 1 : 0.6) + return { + kind: + gender === "male" ? ("bullet_male" as const) : ("bullet_female" as const), + diameter, + contactDepth, + pinCount, + pitch: diameter + 2, + bodyWidth: bodyDiameter + (pinCount - 1) * (diameter + 2), + bodyHeight: bodyDiameter, + bodyDepth: contactDepth + diameter * 1.5, + } +} diff --git a/lib/cable-definition.ts b/lib/cable-definition.ts index 80341cf..3d5cc87 100644 --- a/lib/cable-definition.ts +++ b/lib/cable-definition.ts @@ -1,4 +1,5 @@ import { z } from "zod" +import { bulletDiameterSchema } from "./bullet-connector" import { cableStandardSchema } from "./cable-input" const dimension = z.number().finite().positive() @@ -11,6 +12,26 @@ const body = { /** Local mating face is at z=0, centered on x/y; the cable exits along +z. */ export const cableConnectorSchema = z.discriminatedUnion("kind", [ + z + .object({ + kind: z.literal("bullet_male"), + ...body, + diameter: bulletDiameterSchema, + pinCount: z.number().int().min(1).max(16).default(1), + pitch: dimension, + contactDepth: dimension, + }) + .strict(), + z + .object({ + kind: z.literal("bullet_female"), + ...body, + diameter: bulletDiameterSchema, + pinCount: z.number().int().min(1).max(16).default(1), + pitch: dimension, + contactDepth: dimension, + }) + .strict(), z .object({ kind: z.literal("usb_c_plug"), @@ -74,6 +95,7 @@ export const cableDefinitionSchema = z jst_sh: ["jst_sh_housing", "jst_sh_housing"], jst_ph: ["jst_ph_housing", "jst_ph_housing"], us_mains: ["nema_5_15p", "iec_c13"], + bullet: [connectorA.kind, connectorB.kind], }[cable.standard] if (connectorA.kind !== expected[0] || connectorB.kind !== expected[1]) { ctx.addIssue({ @@ -81,6 +103,62 @@ export const cableDefinitionSchema = z message: "Connector kinds must match the cable standard", }) } + if (cable.standard === "bullet") { + if ( + !("diameter" in connectorA) || + !("diameter" in connectorB) || + connectorA.diameter !== connectorB.diameter || + ("pinCount" in connectorA && + "pinCount" in connectorB && + connectorA.pinCount !== connectorB.pinCount) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Bullet connectors require matching nominal diameters and contact counts", + }) + } + if ( + "pinCount" in connectorA && + ((connectorA.pinCount === 1 && crossSection.kind !== "round_jacket") || + (connectorA.pinCount > 1 && crossSection.kind !== "wire_bundle")) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Bullet cables require one insulated wire per 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 || + (crossSection.kind === "wire_bundle" && + (crossSection.wirePitch !== connector.pitch || + crossSection.wires.some( + (wire) => wire.diameter > connector.bodyHeight, + ))) || + connector.contactDepth >= connector.bodyDepth || + (crossSection.kind === "round_jacket" && + crossSection.diameter > connector.bodyHeight)) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Bullet contact, body, and wire dimensions must fit", + }) + } + } + } else if ("diameter" in connectorA || "diameter" in connectorB) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Bullet connectors require the bullet cable standard", + }) + } if (crossSection.kind === "wire_bundle") { for (const connector of [connectorA, connectorB]) { if ( diff --git a/lib/cable-input.ts b/lib/cable-input.ts index d0e6c7b..65d3878 100644 --- a/lib/cable-input.ts +++ b/lib/cable-input.ts @@ -1,10 +1,12 @@ import { z } from "zod" +import { bulletDiameterSchema, bulletGenderSchema } from "./bullet-connector" export const cableStandardSchema = z.enum([ "usb_c", "jst_sh", "jst_ph", "us_mains", + "bullet", ]) export type CableStandard = z.infer @@ -16,6 +18,17 @@ const jacket = { /** Dimensions are millimeters; wireDiameter includes insulation. */ export const cableInputSchema = z.discriminatedUnion("standard", [ + z + .object({ + standard: z.literal("bullet"), + diameter: bulletDiameterSchema, + pinCount: z.number().int().min(1).max(16).default(1), + genderA: bulletGenderSchema.default("male"), + genderB: bulletGenderSchema.default("female"), + wireDiameter: z.number().finite().positive().default(2), + color: color.default("#263449"), + }) + .strict(), z.object({ standard: z.literal("usb_c"), ...jacket }).strict(), z .object({ diff --git a/lib/get-cable-definition.ts b/lib/get-cable-definition.ts index 3b0717f..f244daa 100644 --- a/lib/get-cable-definition.ts +++ b/lib/get-cable-definition.ts @@ -1,3 +1,4 @@ +import { getBulletConnector } from "./bullet-connector" import { type CableDefinition, cableDefinitionSchema } from "./cable-definition" import { type CableInput, cableInputSchema } from "./cable-input" @@ -12,6 +13,36 @@ const wireColors = [ export function getCableDefinition(cableInput: CableInput): CableDefinition { const cable = cableInputSchema.parse(cableInput) + if (cable.standard === "bullet") { + return cableDefinitionSchema.parse({ + standard: "bullet", + connectorA: getBulletConnector({ + diameter: cable.diameter, + gender: cable.genderA, + pinCount: cable.pinCount, + }), + connectorB: getBulletConnector({ + diameter: cable.diameter, + gender: cable.genderB, + pinCount: cable.pinCount, + }), + crossSection: + cable.pinCount === 1 + ? { + kind: "round_jacket", + diameter: cable.wireDiameter, + color: cable.color, + } + : { + kind: "wire_bundle", + wirePitch: cable.diameter + 2, + wires: Array.from({ length: cable.pinCount }, (_, index) => ({ + diameter: cable.wireDiameter, + color: wireColors[index % wireColors.length], + })), + }, + }) + } if (cable.standard === "usb_c") { const connector = { kind: "usb_c_plug" as const, diff --git a/lib/index.ts b/lib/index.ts index edcac1e..554e28b 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -12,3 +12,10 @@ export { export type { CableConnector, CableDefinition } from "./cable-definition" export { getCableDefinition } from "./get-cable-definition" export { parseCableString } from "./parse-cable-string" + +export { + bulletDiameterSchema, + bulletGenderSchema, + getBulletConnector, +} from "./bullet-connector" +export type { BulletDiameter, BulletGender } from "./bullet-connector" diff --git a/lib/parse-cable-string.ts b/lib/parse-cable-string.ts index 746b1e4..3547dbb 100644 --- a/lib/parse-cable-string.ts +++ b/lib/parse-cable-string.ts @@ -1,3 +1,4 @@ +import { bulletDiameterSchema } from "./bullet-connector" import { type CableDefinition } from "./cable-definition" import { getCableDefinition } from "./get-cable-definition" @@ -7,6 +8,35 @@ export function parseCableString(cableString: string): CableDefinition { if (cableName === "usb_c" || cableName === "us_mains") { return getCableDefinition({ standard: cableName }) } + const bulletMatch = /^bullet([1-9][0-9]*)?_(.+)$/.exec(cableName) + if (bulletMatch) { + let diameter: number | undefined + let genderA: "male" | "female" | undefined + let genderB: "male" | "female" | undefined + const seen = new Set() + for (const parameter of bulletMatch[2]!.split("_")) { + const match = + /^(d)([0-9]+(?:\.[0-9]+)?)mm$/.exec(parameter) ?? + /^(a|b)(male|female)$/.exec(parameter) + if (!match || seen.has(match[1]!)) + throw new Error(`Invalid or duplicate bullet parameter: "${parameter}"`) + seen.add(match[1]!) + if (match[1] === "d") diameter = Number(match[2]) + else if (match[1] === "a") genderA = match[2] as "male" | "female" + else genderB = match[2] as "male" | "female" + } + if (diameter === undefined) + throw new Error( + "Bullet cables require a named diameter parameter, e.g. d3.5mm", + ) + return getCableDefinition({ + standard: "bullet", + diameter: bulletDiameterSchema.parse(diameter), + pinCount: bulletMatch[1] ? Number(bulletMatch[1]) : 1, + genderA: genderA ?? "male", + genderB: genderB ?? "female", + }) + } const jstMatch = /^(jst_sh|jst_ph)(?:_pins([0-9]+))?$/.exec(cableName) if (jstMatch) { return getCableDefinition({ @@ -15,6 +45,6 @@ export function parseCableString(cableString: string): CableDefinition { }) } throw new Error( - `Unsupported cable string: "${cableString}". Expected usb_c, jst_sh[_pinsN], jst_ph[_pinsN], or us_mains.`, + `Unsupported cable string: "${cableString}". Expected usb_c, jst_sh[_pinsN], jst_ph[_pinsN], us_mains, or bullet[CONTACTS]_dNmm[_amale][_bfemale].`, ) } diff --git a/package.json b/package.json index 3db628e..471e8ef 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "gl-matrix": "^3.4.4", - "jscad-electronics": "^0.0.187", + "jscad-electronics": "https://pkg.pr.new/tscircuit/jscad-electronics@b21b030", "looks-same": "^10.0.1", "poppygl": "0.0.30", "react": "^19.1.1", diff --git a/pages/bullet-cable-examples.ts b/pages/bullet-cable-examples.ts new file mode 100644 index 0000000..18fbf2c --- /dev/null +++ b/pages/bullet-cable-examples.ts @@ -0,0 +1,41 @@ +import { parseCableString } from "../lib" + +export const bulletCableExamples = [ + { + cableString: "bullet3_d3.5mm", + title: "Bullet group / three 3.5 mm contacts", + subtitle: "Three separate male/female pairs / three insulated wires", + annotation: "5.5 mm contact pitch / three electrical circuits", + code: [ + 'parseCableString("bullet3_d3.5mm_amale_bfemale")', + "", + "getCableDefinition({", + ' standard: "bullet",', + " diameter: 3.5,", + " pinCount: 3,", + ' genderA: "male",', + ' genderB: "female",', + "})", + ], + }, + ...[2, 3, 3.5, 4, 5, 5.5, 6, 8].map((diameter) => ({ + cableString: `bullet_d${diameter}mm`, + title: `Bullet / ${diameter} mm`, + subtitle: "Gold solder bullet / male to female / single insulated wire", + annotation: + "Representative geometry; nominal mating diameter differs from socket outer diameter", + code: [ + `parseCableString("bullet_d${diameter}mm_amale_bfemale")`, + "", + "getCableDefinition({", + ' standard: "bullet",', + ` diameter: ${diameter},`, + ' genderA: "male",', + ' genderB: "female",', + "})", + ], + })), +].map((example) => ({ + ...example, + cable: parseCableString(example.cableString), +})) diff --git a/pages/bullet-cables.page.tsx b/pages/bullet-cables.page.tsx new file mode 100644 index 0000000..289ae38 --- /dev/null +++ b/pages/bullet-cables.page.tsx @@ -0,0 +1,9 @@ +import snapshotUrl from "../tests/__snapshots__/bullet-cables.snap.png?url" + +export default ( + Single and grouped bullet connector cable meshes rendered with poppygl +) diff --git a/pages/playground.page.tsx b/pages/playground.page.tsx index d0b3f6d..98b56c6 100644 --- a/pages/playground.page.tsx +++ b/pages/playground.page.tsx @@ -1,6 +1,9 @@ import { useMemo, useState } from "react" import { cableStandardSchema, + bulletDiameterSchema, + type BulletDiameter, + type BulletGender, type CableStandard, getCableDefinition, } from "../lib" @@ -10,10 +13,20 @@ import { MeshCanvas } from "./components/mesh-canvas" export default function Playground() { const [standard, setStandard] = useState("jst_sh") const [pinCount, setPinCount] = useState(4) + const [diameter, setDiameter] = useState(3.5) + const [genderA, setGenderA] = useState("male") + const [genderB, setGenderB] = useState("female") const isJst = standard === "jst_sh" || standard === "jst_ph" const definition = useMemo( - () => getCableDefinition(isJst ? { standard, pinCount } : { standard }), - [standard, pinCount, isJst], + () => + getCableDefinition( + standard === "bullet" + ? { standard, diameter, genderA, genderB, pinCount } + : isJst + ? { standard, pinCount } + : { standard }, + ), + [standard, pinCount, isJst, diameter, genderA, genderB], ) const meshes = useMemo(() => previewCableMeshes(definition), [definition]) const connectorMeshes = useMemo( @@ -30,28 +43,80 @@ export default function Playground() { Standard{" "} {" "} - {isJst && ( + {(isJst || standard === "bullet") && ( )} + {standard === "bullet" && ( + <> + {" "} + {" "} + + + )}
diff --git a/tests/__snapshots__/bullet-cables.snap.png b/tests/__snapshots__/bullet-cables.snap.png new file mode 100644 index 0000000..d1845b4 Binary files /dev/null and b/tests/__snapshots__/bullet-cables.snap.png differ diff --git a/tests/bullet-cable-snapshots.test.ts b/tests/bullet-cable-snapshots.test.ts new file mode 100644 index 0000000..5178804 --- /dev/null +++ b/tests/bullet-cable-snapshots.test.ts @@ -0,0 +1,10 @@ +import { test } from "bun:test" +import { renderBulletCatalog } from "./fixtures/render-cable-snapshots" +import { expectPngSnapshot } from "./fixtures/expect-png-snapshot" + +test("single and grouped bullet cables render male and female contacts", async () => { + await expectPngSnapshot({ + name: "bullet-cables", + png: await renderBulletCatalog(), + }) +}, 30000) diff --git a/tests/bullet-connectors.test.ts b/tests/bullet-connectors.test.ts new file mode 100644 index 0000000..fa65c50 --- /dev/null +++ b/tests/bullet-connectors.test.ts @@ -0,0 +1,113 @@ +import { expect, test } from "bun:test" +import { + cableDefinitionSchema, + cableInputSchema, + getCableDefinition, + parseCableString, +} from "../lib" + +test("bullet sizes and independent end genders survive DSL and object input", () => { + for (const diameter of [2, 3, 3.5, 4, 5, 5.5, 6, 8] as const) { + for (const genderA of ["male", "female"] as const) { + for (const genderB of ["male", "female"] as const) { + const definition = getCableDefinition({ + standard: "bullet", + diameter, + genderA, + genderB, + }) + expect( + parseCableString(`bullet_d${diameter}mm_a${genderA}_b${genderB}`), + ).toEqual(definition) + expect(definition.connectorA.kind).toBe(`bullet_${genderA}`) + expect(definition.connectorB.kind).toBe(`bullet_${genderB}`) + expect(definition.crossSection).toEqual({ + kind: "round_jacket", + diameter: 2, + color: "#263449", + }) + } + } + expect(parseCableString(`bullet_d${diameter}mm`)).toEqual( + getCableDefinition({ standard: "bullet", diameter }), + ) + } +}) + +test("named bullet parameters are order-independent and end genders default independently", () => { + const expected = getCableDefinition({ + standard: "bullet", + diameter: 3.5, + pinCount: 3, + genderA: "female", + genderB: "male", + }) + for (const text of [ + "bullet3_d3.5mm_afemale_bmale", + "bullet3_bmale_d3.5mm_afemale", + "bullet3_afemale_bmale_d3.5mm", + ]) + expect(parseCableString(text)).toEqual(expected) + expect(parseCableString("bullet_d3.5mm_afemale")).toEqual( + getCableDefinition({ + standard: "bullet", + diameter: 3.5, + genderA: "female", + }), + ) + expect(parseCableString("bullet_bmale_d3.5mm")).toEqual( + getCableDefinition({ standard: "bullet", diameter: 3.5, genderB: "male" }), + ) +}) + +test("positional, duplicate, unknown and invalid parameters fail", () => { + for (const text of [ + "bullet3_3.5mm", + "bullet_3.5mm_male_female", + "bullet3_d3.5mm_male_female", + "bullet3_afemale_bmale", + "bullet_d3.5mm_d4mm", + "bullet_d3.5mm_amale_afemale", + "bullet_d3.5mm_bmale_bfemale", + "bullet_d3.5mm_asocket", + "bullet_d3.5mm_extra", + "bullet", + "bullet_d0mm", + "bullet_d7mm", + "bullet_d3.5mm_male", + "bullet_d4mm_male_socket", + "bullet_d4mm_amale_bfemale_extra", + ]) { + expect(() => parseCableString(text)).toThrow() + } + expect(cableInputSchema.safeParse({ standard: "bullet" }).success).toBe(false) + for (const diameter of [0, -1, 7, NaN, Infinity]) { + expect( + cableInputSchema.safeParse({ standard: "bullet", diameter }).success, + ).toBe(false) + } + const definition = getCableDefinition({ standard: "bullet", diameter: 4 }) + const other = getCableDefinition({ standard: "bullet", diameter: 3.5 }) + const usb = getCableDefinition({ standard: "usb_c" }) + expect( + cableDefinitionSchema.safeParse({ + ...definition, + connectorB: other.connectorB, + }).success, + ).toBe(false) + expect( + cableDefinitionSchema.safeParse({ + ...definition, + connectorB: usb.connectorB, + }).success, + ).toBe(false) + expect(() => + getCableDefinition({ standard: "bullet", diameter: 2, wireDiameter: 5 }), + ).toThrow() + expect( + cableDefinitionSchema.safeParse({ + ...definition, + connectorA: { ...definition.connectorA, contactDepth: 100 }, + }).success, + ).toBe(false) +}) diff --git a/tests/fixtures/render-cable-snapshots.ts b/tests/fixtures/render-cable-snapshots.ts index 0754d78..15ce7a9 100644 --- a/tests/fixtures/render-cable-snapshots.ts +++ b/tests/fixtures/render-cable-snapshots.ts @@ -8,6 +8,7 @@ import { import { encodePNG } from "poppygl" import { getCableDefinition, parseCableString } from "../../lib" import { cableExamples } from "../../pages/cable-examples" +import { bulletCableExamples } from "../../pages/bullet-cable-examples" import { previewCableMeshes, previewConnectorMeshes, @@ -195,6 +196,103 @@ export async function renderCableCatalog(): Promise { return compose({ width, height: 1890, content }) } +export async function renderBulletCatalog(): Promise { + const width = 1720 + const rowHeight = 440 + const content = [ + label({ + text: "Bullet connectors / single and grouped cable meshes", + x: 28, + y: 44, + size: 28, + }), + ] + for (const [index, example] of bulletCableExamples.entries()) { + const y = 82 + index * rowHeight + content.push( + ``, + label({ text: example.title, x: 40, y: y + 35, size: 23 }), + ``, + ...[ + "const definition =", + ` parseCableString("${example.cableString}")`, + "", + "const meshes = createCableMeshes({", + " definition,", + " path: resolvedPath,", + "})", + ].map((text, line) => + label({ text, x: 48, y: y + 94 + line * 25, size: 17 }), + ), + label({ + text: "CABLE ASSEMBLY / ISOMETRIC", + x: 485, + y: y + 67, + size: 14, + }), + await meshImage({ + meshes: previewCableMeshes(example.cable), + x: 460, + y: y + 80, + width: 720, + height: 300, + }), + label({ + text: + example.cable.standard === "bullet" + ? "MALE / FEMALE CONTACTS" + : "CONNECTOR DETAIL", + x: 1220, + y: y + 67, + size: 14, + }), + await meshImage({ + meshes: previewConnectorMeshes({ definition: example.cable }), + x: 1200, + y: y + 88, + width: + example.cable.standard === "us_mains" || + example.cable.standard === "bullet" + ? 230 + : 460, + height: 265, + detail: true, + }), + label({ text: example.annotation, x: 40, y: y + 397, size: 17 }), + ) + if ( + example.cable.standard === "us_mains" || + example.cable.standard === "bullet" + ) + content.push( + await meshImage({ + meshes: previewConnectorMeshes({ + definition: example.cable, + end: "B", + }), + x: 1430, + y: y + 88, + width: 230, + height: 265, + detail: true, + }), + ) + } + content.push( + label({ + text: "Actual indexed meshes / supplied fixture paths / routing and sagging remain separate", + x: 28, + y: 82 + bulletCableExamples.length * rowHeight + 20, + size: 17, + }), + ) + return compose({ + width, + height: 82 + bulletCableExamples.length * rowHeight + 50, + content, + }) +} + export async function renderJstComparison(): Promise { const content = [ label({ diff --git a/tests/grouped-bullet-connectors.test.ts b/tests/grouped-bullet-connectors.test.ts new file mode 100644 index 0000000..c29587a --- /dev/null +++ b/tests/grouped-bullet-connectors.test.ts @@ -0,0 +1,79 @@ +import { expect, test } from "bun:test" +import { + cableDefinitionSchema, + getCableDefinition, + parseCableString, +} from "../lib" + +test("grouped bullet DSL describes one separate contact and insulated wire per circuit", () => { + for (const diameter of [2, 3, 3.5, 4, 5, 5.5, 6, 8] as const) { + for (const pinCount of [2, 3, 6, 16]) { + for (const genderA of ["male", "female"] as const) { + for (const genderB of ["male", "female"] as const) { + const definition = getCableDefinition({ + standard: "bullet", + diameter, + pinCount, + genderA, + genderB, + }) + expect( + parseCableString( + `bullet${pinCount}_d${diameter}mm_a${genderA}_b${genderB}`, + ), + ).toEqual(definition) + expect(definition.connectorA).toMatchObject({ + kind: `bullet_${genderA}`, + pinCount, + pitch: diameter + 2, + }) + expect(definition.crossSection.kind).toBe("wire_bundle") + if (definition.crossSection.kind !== "wire_bundle") + throw new Error("Expected insulated wire bundle") + expect(definition.crossSection.wires).toHaveLength(pinCount) + expect(definition.crossSection.wirePitch).toBe(diameter + 2) + expect( + definition.crossSection.wires.every((wire) => wire.diameter === 2), + ).toBe(true) + } + } + } + } + const triple = parseCableString("bullet3_d3.5mm") + expect(triple).toEqual( + getCableDefinition({ standard: "bullet", diameter: 3.5, pinCount: 3 }), + ) + expect(parseCableString("bullet1_d3.5mm")).toEqual( + parseCableString("bullet_d3.5mm"), + ) + for (const text of [ + "bullet0_d3.5mm", + "bullet17_d3.5mm", + "bullet03_d3.5mm", + "bullet3_d3.5mm_male", + "bullet3_d7mm", + ]) { + expect(() => parseCableString(text)).toThrow() + } + const single = parseCableString("bullet_d3.5mm") + expect( + cableDefinitionSchema.safeParse({ + ...triple, + connectorB: single.connectorB, + }).success, + ).toBe(false) + expect( + cableDefinitionSchema.safeParse({ + ...triple, + crossSection: single.crossSection, + }).success, + ).toBe(false) + if (triple.crossSection.kind !== "wire_bundle") + throw new Error("Expected wire bundle") + expect( + cableDefinitionSchema.safeParse({ + ...triple, + crossSection: { ...triple.crossSection, wirePitch: 2 }, + }).success, + ).toBe(false) +})