From 7bbdefc5f476559b00b96730161816628dee5022 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Tue, 6 Oct 2026 10:33:10 -0700 Subject: [PATCH 1/9] Add bullet connector and assembly cable props --- README.md | 5 +++ docs/assembly-cable.md | 34 ++++++++++++++++---- generated/COMPONENT_TYPES.md | 29 +++++++++++++---- generated/PROPS_OVERVIEW.md | 7 +++- lib/assembly/cable.ts | 6 ++-- lib/components/connector.ts | 59 +++++++++++++++++++++++++++++++--- tests/bullet-connector.test.ts | 42 ++++++++++++++++++++++++ tests/connector.test.ts | 8 ++++- 8 files changed, 168 insertions(+), 22 deletions(-) create mode 100644 tests/bullet-connector.test.ts diff --git a/README.md b/README.md index 08b99794..79a0560b 100644 --- a/README.md +++ b/README.md @@ -737,6 +737,11 @@ export interface ConnectorProps extends ChipPropsSU { * Number of electrical circuits in the connector */ pinCount?: number; + + /** Nominal bullet contact diameter in mm or a length string; required for bullet. */ + bulletDiameter?: number | string; + /** Gender of the endpoint connector; the attached cable uses the opposite gender. */ + bulletGender?: "male" | "female"; } ``` diff --git a/docs/assembly-cable.md b/docs/assembly-cable.md index f3a6a49f..0b551e7a 100644 --- a/docs/assembly-cable.md +++ b/docs/assembly-cable.md @@ -18,12 +18,12 @@ validator is exported as `assemblyCableProps` and `assemblyProps.cable`. /> ``` -| Prop | Accepted input | Parsed output | -| --- | --- | --- | -| `name` | Required nonempty string | Trimmed cable identity | -| `from` | Required nonempty string | Trimmed start endpoint reference | -| `to` | Required nonempty string | Trimmed end endpoint reference | -| `standard` | Optional `"usb_c"` | Unchanged; omitted when not supplied | +| Prop | Accepted input | Parsed output | +| ---------- | -------------------------------- | ------------------------------------ | +| `name` | Required nonempty string | Trimmed cable identity | +| `from` | Required nonempty string | Trimmed start endpoint reference | +| `to` | Required nonempty string | Trimmed end endpoint reference | +| `standard` | Optional `"usb_c"` or `"bullet"` | Unchanged; omitted when not supplied | Endpoints can be connector selectors or named assembly references. The initial `usb_c` preset represents a USB-C-to-USB-C cable. Omitting `standard` leaves @@ -33,3 +33,25 @@ are inferred; there are no corresponding props or aliases. This is an additive props API and requires no migration. The schema validates the inputs; endpoint resolution, routing, and rendering require support in core and the CAD renderer. + +Bullet connectors use `standard="bullet"`, a required `bulletDiameter` (2, 3, +3.5, 4, 5, 5.5, 6, or 8 mm), and a required `bulletGender="male" | "female"`. +Length strings such as `"3.5mm"` normalize to millimeters. A bullet has one +contact; `pinCount` can be omitted or set to 1. Supply a footprint for each PCB +connector, as for other custom connectors. + +```tsx + + + +``` + +Each cable end has the opposite gender of its selected connector. The example +emits `bullet_3.5mm_female_male`. Same-gender PCB endpoints produce a cable +with two ends of the opposite gender. Endpoint diameters must match; mismatches +are errors. Omitting the cable's `standard` infers `bullet` from the endpoints. +There are no aliases, and existing USB-C/JST usage requires no migration. +The bullet models represent generic solder contacts rather than a specific +manufacturer's production part. diff --git a/generated/COMPONENT_TYPES.md b/generated/COMPONENT_TYPES.md index ecedbf63..84e5350b 100644 --- a/generated/COMPONENT_TYPES.md +++ b/generated/COMPONENT_TYPES.md @@ -2161,14 +2161,29 @@ export interface ConnectorProps extends ChipPropsSU { standard?: ConnectorStandard pinCount?: number + + bulletDiameter?: number | string + bulletGender?: "male" | "female" } -/** - * Number of electrical circuits in the connector - */ -export const connectorProps = chipProps.extend({ - standard: connectorStandard.optional(), - pinCount: z.number().int().positive().optional(), -}) +.extend({ + standard: connectorStandard.optional(), + pinCount: z.number().int().positive().optional(), + bulletDiameter: distance + .pipe( + 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), + ]), + ) + .optional(), + bulletGender: z.enum(["male", "female"]).optional(), + }) ``` ### constrainedlayout diff --git a/generated/PROPS_OVERVIEW.md b/generated/PROPS_OVERVIEW.md index a710ef43..035c82aa 100644 --- a/generated/PROPS_OVERVIEW.md +++ b/generated/PROPS_OVERVIEW.md @@ -162,7 +162,7 @@ export interface AssemblyCableProps { from: string /** End connector selector or named assembly reference. */ to: string - /** Optional USB-C-to-USB-C preset. Omit to infer the cable from its endpoints. */ + /** Optional cable standard; bullet size and end genders are inferred from endpoints. Omit to infer the cable from its endpoints. */ standard?: AssemblyCableStandard } @@ -1084,6 +1084,11 @@ export interface ConnectorProps extends ChipPropsSU { * Number of electrical circuits in the connector */ pinCount?: number + + /** Nominal bullet contact diameter in mm or a length string; required for bullet. */ + bulletDiameter?: number | string + /** Gender of the endpoint connector; the attached cable uses the opposite gender. */ + bulletGender?: "male" | "female" } diff --git a/lib/assembly/cable.ts b/lib/assembly/cable.ts index 8b4e226c..005ac90a 100644 --- a/lib/assembly/cable.ts +++ b/lib/assembly/cable.ts @@ -1,7 +1,7 @@ import { expectTypesMatch } from "lib/typecheck" import { z } from "zod" -export type AssemblyCableStandard = "usb_c" +export type AssemblyCableStandard = "usb_c" | "bullet" export interface AssemblyCableProps { /** Stable assembly identity for the cable. */ @@ -10,7 +10,7 @@ export interface AssemblyCableProps { from: string /** End connector selector or named assembly reference. */ to: string - /** Optional USB-C-to-USB-C preset. Omit to infer the cable from its endpoints. */ + /** Optional cable standard; bullet size and end genders are inferred from endpoints. Omit to infer the cable from its endpoints. */ standard?: AssemblyCableStandard } @@ -18,7 +18,7 @@ export const assemblyCableProps = z.object({ name: z.string().trim().min(1), from: z.string().trim().min(1), to: z.string().trim().min(1), - standard: z.literal("usb_c").optional(), + standard: z.enum(["usb_c", "bullet"]).optional(), }) export type AssemblyCablePropsInput = z.input diff --git a/lib/components/connector.ts b/lib/components/connector.ts index ca62ad6d..851ba274 100644 --- a/lib/components/connector.ts +++ b/lib/components/connector.ts @@ -1,9 +1,11 @@ import { chipProps, type ChipPropsSU } from "./chip" import { expectTypesMatch } from "lib/typecheck" import { z } from "zod" +import { distance } from "lib/common/distance" export const connectorStandard = z.enum([ "usb_c", + "bullet", "m2", "jst_sh", "jst_gh", @@ -25,12 +27,61 @@ export interface ConnectorProps extends ChipPropsSU { * Number of electrical circuits in the connector */ pinCount?: number + + /** Nominal bullet contact diameter in mm or a length string; required for bullet. */ + bulletDiameter?: number | string + /** Gender of the endpoint connector; the attached cable uses the opposite gender. */ + bulletGender?: "male" | "female" } -export const connectorProps = chipProps.extend({ - standard: connectorStandard.optional(), - pinCount: z.number().int().positive().optional(), -}) +export const connectorProps = chipProps + .extend({ + standard: connectorStandard.optional(), + pinCount: z.number().int().positive().optional(), + bulletDiameter: distance + .pipe( + 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), + ]), + ) + .optional(), + bulletGender: z.enum(["male", "female"]).optional(), + }) + .superRefine((connector, ctx) => { + if (connector.standard === "bullet") { + for (const field of ["bulletDiameter", "bulletGender"] as const) { + if (connector[field] === undefined) + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [field], + message: `${field} is required for bullet connectors`, + }) + } + if (connector.pinCount !== undefined && connector.pinCount !== 1) + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["pinCount"], + message: "A bullet connector has one electrical contact", + }) + } else if ( + connector.bulletDiameter !== undefined || + connector.bulletGender !== undefined + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "bulletDiameter and bulletGender require standard=bullet", + }) + } + }) + +export type ParsedConnectorProps = z.output type InferredConnectorProps = z.input expectTypesMatch(true) diff --git a/tests/bullet-connector.test.ts b/tests/bullet-connector.test.ts new file mode 100644 index 00000000..36fe21ae --- /dev/null +++ b/tests/bullet-connector.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from "bun:test" +import { assemblyCableProps, connectorProps } from "lib" + +test("bullet connector props require a supported diameter and endpoint gender", () => { + expect( + connectorProps.parse({ + name: "J1", + standard: "bullet", + bulletDiameter: "3.5mm", + bulletGender: "female", + pinCount: 1, + }), + ).toMatchObject({ bulletDiameter: 3.5, bulletGender: "female" }) + for (const fields of [ + {}, + { bulletDiameter: 4 }, + { bulletGender: "male" }, + { bulletDiameter: 7, bulletGender: "male" }, + { bulletDiameter: 4, bulletGender: "socket" }, + { bulletDiameter: 4, bulletGender: "male", pinCount: 2 }, + ]) { + expect( + connectorProps.safeParse({ name: "J1", standard: "bullet", ...fields }) + .success, + ).toBe(false) + } + expect( + connectorProps.safeParse({ + name: "J1", + standard: "usb_c", + bulletDiameter: 4, + }).success, + ).toBe(false) + expect( + assemblyCableProps.parse({ + name: "POWER", + from: ".J1", + to: ".J2", + standard: "bullet", + }).standard, + ).toBe("bullet") +}) diff --git a/tests/connector.test.ts b/tests/connector.test.ts index fcae1f9c..5318816b 100644 --- a/tests/connector.test.ts +++ b/tests/connector.test.ts @@ -7,7 +7,13 @@ import { for (const standard of connectorStandard.options) { test(`should parse connector with ${standard} standard`, () => { - const raw: ConnectorProps = { name: "conn", standard } + const raw: ConnectorProps = { + name: "conn", + standard, + ...(standard === "bullet" + ? { bulletDiameter: 4, bulletGender: "male" as const } + : {}), + } const parsed = connectorProps.parse(raw) expect(parsed.standard).toBe(standard) }) From f16526970d73c8b5e7001729c8589fd37414b58d Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Tue, 6 Oct 2026 12:31:41 -0700 Subject: [PATCH 2/9] Allow grouped bullet connectors with one to sixteen contacts --- README.md | 2 +- docs/assembly-cable.md | 7 ++++--- generated/PROPS_OVERVIEW.md | 2 +- lib/components/connector.ts | 6 +++--- tests/bullet-connector.test.ts | 27 ++++++++++++++++++++++++++- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 79a0560b..e1ef6cb8 100644 --- a/README.md +++ b/README.md @@ -734,7 +734,7 @@ export interface ConnectorProps extends ChipPropsSU { standard?: ConnectorStandard; /** - * Number of electrical circuits in the connector + * Number of electrical circuits in the connector; bullet groups support 1–16 (default 1) */ pinCount?: number; diff --git a/docs/assembly-cable.md b/docs/assembly-cable.md index 0b551e7a..226e2d20 100644 --- a/docs/assembly-cable.md +++ b/docs/assembly-cable.md @@ -36,8 +36,9 @@ and the CAD renderer. Bullet connectors use `standard="bullet"`, a required `bulletDiameter` (2, 3, 3.5, 4, 5, 5.5, 6, or 8 mm), and a required `bulletGender="male" | "female"`. -Length strings such as `"3.5mm"` normalize to millimeters. A bullet has one -contact; `pinCount` can be omitted or set to 1. Supply a footprint for each PCB +Length strings such as `"3.5mm"` normalize to millimeters. A bullet group has 1–16 +contacts; `pinCount` defaults to 1. Set `pinCount={3}` on both endpoint connectors +for three bullet contacts and three insulated wires, emitting `bullet3_3.5mm_female_male`. Supply a footprint for each PCB connector, as for other custom connectors. ```tsx @@ -50,7 +51,7 @@ connector, as for other custom connectors. Each cable end has the opposite gender of its selected connector. The example emits `bullet_3.5mm_female_male`. Same-gender PCB endpoints produce a cable -with two ends of the opposite gender. Endpoint diameters must match; mismatches +with two ends of the opposite gender. Endpoint diameters and contact counts must match; mismatches are errors. Omitting the cable's `standard` infers `bullet` from the endpoints. There are no aliases, and existing USB-C/JST usage requires no migration. The bullet models represent generic solder contacts rather than a specific diff --git a/generated/PROPS_OVERVIEW.md b/generated/PROPS_OVERVIEW.md index 035c82aa..69ea44df 100644 --- a/generated/PROPS_OVERVIEW.md +++ b/generated/PROPS_OVERVIEW.md @@ -1081,7 +1081,7 @@ export interface ConnectorProps extends ChipPropsSU { standard?: ConnectorStandard /** - * Number of electrical circuits in the connector + * Number of electrical circuits in the connector; bullet groups support 1–16 (default 1) */ pinCount?: number diff --git a/lib/components/connector.ts b/lib/components/connector.ts index 851ba274..19eb185d 100644 --- a/lib/components/connector.ts +++ b/lib/components/connector.ts @@ -24,7 +24,7 @@ export interface ConnectorProps extends ChipPropsSU { standard?: ConnectorStandard /** - * Number of electrical circuits in the connector + * Number of electrical circuits in the connector; bullet groups support 1–16 (default 1) */ pinCount?: number @@ -64,11 +64,11 @@ export const connectorProps = chipProps message: `${field} is required for bullet connectors`, }) } - if (connector.pinCount !== undefined && connector.pinCount !== 1) + if (connector.pinCount !== undefined && connector.pinCount > 16) ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["pinCount"], - message: "A bullet connector has one electrical contact", + message: "Bullet groups support 1 to 16 electrical contacts", }) } else if ( connector.bulletDiameter !== undefined || diff --git a/tests/bullet-connector.test.ts b/tests/bullet-connector.test.ts index 36fe21ae..81279413 100644 --- a/tests/bullet-connector.test.ts +++ b/tests/bullet-connector.test.ts @@ -17,7 +17,7 @@ test("bullet connector props require a supported diameter and endpoint gender", { bulletGender: "male" }, { bulletDiameter: 7, bulletGender: "male" }, { bulletDiameter: 4, bulletGender: "socket" }, - { bulletDiameter: 4, bulletGender: "male", pinCount: 2 }, + { bulletDiameter: 4, bulletGender: "male", pinCount: 17 }, ]) { expect( connectorProps.safeParse({ name: "J1", standard: "bullet", ...fields }) @@ -40,3 +40,28 @@ test("bullet connector props require a supported diameter and endpoint gender", }).standard, ).toBe("bullet") }) + +test("bullet groups accept a contact count and reject out-of-range counts", () => { + for (const pinCount of [1, 2, 3, 6, 16]) { + expect( + connectorProps.parse({ + name: "J1", + standard: "bullet", + bulletDiameter: 3.5, + bulletGender: "male", + pinCount, + }).pinCount, + ).toBe(pinCount) + } + for (const pinCount of [0, -1, 1.5, 17]) { + expect( + connectorProps.safeParse({ + name: "J1", + standard: "bullet", + bulletDiameter: 3.5, + bulletGender: "male", + pinCount, + }).success, + ).toBe(false) + } +}) From b508cfab644d7b6fbed9751222e3a62d880a9e7e Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Tue, 6 Oct 2026 13:35:26 -0700 Subject: [PATCH 3/9] Document named bullet cable diameter and endpoint gender modifiers --- docs/assembly-cable.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/assembly-cable.md b/docs/assembly-cable.md index 226e2d20..3f74f26e 100644 --- a/docs/assembly-cable.md +++ b/docs/assembly-cable.md @@ -38,7 +38,7 @@ Bullet connectors use `standard="bullet"`, a required `bulletDiameter` (2, 3, 3.5, 4, 5, 5.5, 6, or 8 mm), and a required `bulletGender="male" | "female"`. Length strings such as `"3.5mm"` normalize to millimeters. A bullet group has 1–16 contacts; `pinCount` defaults to 1. Set `pinCount={3}` on both endpoint connectors -for three bullet contacts and three insulated wires, emitting `bullet3_3.5mm_female_male`. Supply a footprint for each PCB +for three bullet contacts and three insulated wires, emitting `bullet3_d3.5mm_afemale_bmale`. Supply a footprint for each PCB connector, as for other custom connectors. ```tsx @@ -50,7 +50,7 @@ connector, as for other custom connectors. ``` Each cable end has the opposite gender of its selected connector. The example -emits `bullet_3.5mm_female_male`. Same-gender PCB endpoints produce a cable +emits `bullet_d3.5mm_afemale_bmale`. Same-gender PCB endpoints produce a cable with two ends of the opposite gender. Endpoint diameters and contact counts must match; mismatches are errors. Omitting the cable's `standard` infers `bullet` from the endpoints. There are no aliases, and existing USB-C/JST usage requires no migration. From e2cd4ecfbe21b4c2d308c426bbe7cfee4836faa7 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Tue, 6 Oct 2026 14:39:32 -0700 Subject: [PATCH 4/9] Expose named bullet cable connectors on imported CAD assemblies --- docs/assembly-cable.md | 43 ++++++++++++++++++++++-- generated/COMPONENT_TYPES.md | 15 +-------- generated/PROPS_OVERVIEW.md | 21 ++++++++++++ lib/assembly/cable-connector.ts | 45 ++++++++++++++++++++++++++ lib/assembly/index.ts | 1 + lib/assembly/subassembly.ts | 19 +++++++++++ lib/components/connector.ts | 28 ++++++++-------- tests/assembly-cable-connector.test.ts | 34 +++++++++++++++++++ 8 files changed, 176 insertions(+), 30 deletions(-) create mode 100644 lib/assembly/cable-connector.ts create mode 100644 tests/assembly-cable-connector.test.ts diff --git a/docs/assembly-cable.md b/docs/assembly-cable.md index 3f74f26e..ca819ebe 100644 --- a/docs/assembly-cable.md +++ b/docs/assembly-cable.md @@ -51,8 +51,47 @@ connector, as for other custom connectors. Each cable end has the opposite gender of its selected connector. The example emits `bullet_d3.5mm_afemale_bmale`. Same-gender PCB endpoints produce a cable -with two ends of the opposite gender. Endpoint diameters and contact counts must match; mismatches -are errors. Omitting the cable's `standard` infers `bullet` from the endpoints. +with two ends of the opposite gender. Contact counts must match; different +diameters produce an adapter cable with named `da` and `db` parameters. +Omitting the cable's `standard` infers `bullet` from the endpoints. There are no aliases, and existing USB-C/JST usage requires no migration. The bullet models represent generic solder contacts rather than a specific manufacturer's production part. + +## Imported BLDC CAD and adapter leads + +An imported motor can expose a named physical connector through +`assembly.subassembly` (also `assembly.cadassembly`). `assembly.motor` remains +the NEMA preset API. This attaches the cable to the imported CAD assembly; +it does not require a placeholder PCB connector or a cable-to-cable joint. + +```tsx + + + +``` + +The resulting cable is `bullet3_da3.5mm_db4mm_afemale_bfemale`: three +3.5 mm female contacts on the motor side and three 4 mm female contacts on +the board side. A board with female sockets instead produces `bmale`. + +`cableConnectors` is an optional record of named interfaces. Each interface +requires `standard="bullet"`, `bulletDiameter`, `bulletGender`, `position`, and +`facingDirection`; `pinCount` defaults to one and supports 1–16. Positions +accept numbers (mm) or unit strings and normalize to physical mm relative to +the CAD placement anchor, in its right-handed local frame. They follow the +CAD model's rotation and position offset. These physical connector offsets +are not scaled by mesh file units or object-fit scaling. Directions are one +of `x+`, `x-`, `y+`, `y-`, `z+`, or `z-`, and receive rotation only. No mating +position or gender is guessed from the mesh. Use the measured connector +center for the real motor asset. Invalid sizes, counts, directions, unknown +connector names, or incompatible standards fail explicitly. No aliases or +migration changes are introduced for existing assemblies. diff --git a/generated/COMPONENT_TYPES.md b/generated/COMPONENT_TYPES.md index 84e5350b..267a9d79 100644 --- a/generated/COMPONENT_TYPES.md +++ b/generated/COMPONENT_TYPES.md @@ -2168,20 +2168,7 @@ export interface ConnectorProps extends ChipPropsSU { .extend({ standard: connectorStandard.optional(), pinCount: z.number().int().positive().optional(), - bulletDiameter: distance - .pipe( - 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), - ]), - ) - .optional(), + bulletDiameter: bulletDiameter.optional(), bulletGender: z.enum(["male", "female"]).optional(), }) ``` diff --git a/generated/PROPS_OVERVIEW.md b/generated/PROPS_OVERVIEW.md index 69ea44df..2547d786 100644 --- a/generated/PROPS_OVERVIEW.md +++ b/generated/PROPS_OVERVIEW.md @@ -155,6 +155,23 @@ export interface AntennaProps extends CommonComponentProps { } +export interface AssemblyCableConnectorProps { + standard: "bullet" + bulletDiameter: Distance + /** Gender on the CAD assembly; the cable uses the opposite gender. */ + bulletGender: "male" | "female" + /** Number of independent circuits; defaults to one, 1–16. */ + pinCount?: number + /** Mating center relative to the CAD placement anchor, in physical mm, + * right-handed model-local +X/+Y/+Z, before its rotation and positionOffset. + * Do not use mesh file units; this is already a physical offset in mm. + */ + position: { x: Distance; y: Distance; z: Distance } + /** Outward mating axis in that same model-local frame. Required. */ + facingDirection: CadModelAxisDirection +} + + export interface AssemblyCableProps { /** Stable assembly identity for the cable. */ name: string @@ -277,6 +294,10 @@ export interface AssemblySubassemblyProps { cadModel?: CadModelProp /** Nested assembly elements or CAD geometry; preserved without parsing. */ children?: ReactNode + /** Named physical cable interfaces on the imported CAD model. Connect using + * `from="MOTOR.phases"`; coordinates follow the CAD model's offset/rotation. + */ + cableConnectors?: Record } diff --git a/lib/assembly/cable-connector.ts b/lib/assembly/cable-connector.ts new file mode 100644 index 00000000..6fa1c91b --- /dev/null +++ b/lib/assembly/cable-connector.ts @@ -0,0 +1,45 @@ +import { z } from "zod" +import { bulletDiameter } from "../components/connector" +import { point3 } from "../common/point3" +import { + type CadModelAxisDirection, + cadModelAxisDirection, +} from "../common/cadModel" +import { type Distance } from "../common/distance" +import { expectTypesMatch } from "lib/typecheck" + +/** A physical cable interface on imported CAD, independent of PCB footprints. */ +export interface AssemblyCableConnectorProps { + standard: "bullet" + bulletDiameter: Distance + /** Gender on the CAD assembly; the cable uses the opposite gender. */ + bulletGender: "male" | "female" + /** Number of independent circuits; defaults to one, 1–16. */ + pinCount?: number + /** Mating center relative to the CAD placement anchor, in physical mm, + * right-handed model-local +X/+Y/+Z, before its rotation and positionOffset. + * Do not use mesh file units; this is already a physical offset in mm. + */ + position: { x: Distance; y: Distance; z: Distance } + /** Outward mating axis in that same model-local frame. Required. */ + facingDirection: CadModelAxisDirection +} + +export const assemblyCableConnectorProps = z + .object({ + standard: z.literal("bullet"), + bulletDiameter, + bulletGender: z.enum(["male", "female"]), + pinCount: z.number().int().min(1).max(16).optional(), + position: point3, + facingDirection: cadModelAxisDirection, + }) + .strict() + +export type ParsedAssemblyCableConnectorProps = z.output< + typeof assemblyCableConnectorProps +> +expectTypesMatch< + AssemblyCableConnectorProps, + z.input +>(true) diff --git a/lib/assembly/index.ts b/lib/assembly/index.ts index f312c841..834eafb9 100644 --- a/lib/assembly/index.ts +++ b/lib/assembly/index.ts @@ -8,6 +8,7 @@ import { assemblySubassemblyProps } from "./subassembly" export * from "./board-mounting" export * from "./cable" +export * from "./cable-connector" export * from "./device" export * from "./motor" export * from "./printedpart" diff --git a/lib/assembly/subassembly.ts b/lib/assembly/subassembly.ts index 258cd4eb..41a1c470 100644 --- a/lib/assembly/subassembly.ts +++ b/lib/assembly/subassembly.ts @@ -3,6 +3,10 @@ import { type CadModelProp, cadModelProp } from "lib/common/cadModel" import { expectTypesMatch } from "lib/typecheck" import type { ReactNode } from "react" import { z } from "zod" +import { + assemblyCableConnectorProps, + type AssemblyCableConnectorProps, +} from "./cable-connector" export interface AssemblySubassemblyProps { /** Stable identity used by selectors from other assembly elements. */ @@ -17,6 +21,10 @@ export interface AssemblySubassemblyProps { cadModel?: CadModelProp /** Nested assembly elements or CAD geometry; preserved without parsing. */ children?: ReactNode + /** Named physical cable interfaces on the imported CAD model. Connect using + * `from="MOTOR.phases"`; coordinates follow the CAD model's offset/rotation. + */ + cableConnectors?: Record } export const assemblySubassemblyProps = z @@ -33,6 +41,17 @@ export const assemblySubassemblyProps = z .optional(), cadModel: cadModelProp.optional(), children: z.custom().optional(), + cableConnectors: z + .record( + z + .string() + .regex( + /^[A-Za-z][A-Za-z0-9_]*$/, + "Connector names must be simple identifiers", + ), + assemblyCableConnectorProps, + ) + .optional(), }) .refine( (assembly) => diff --git a/lib/components/connector.ts b/lib/components/connector.ts index 19eb185d..e6f36534 100644 --- a/lib/components/connector.ts +++ b/lib/components/connector.ts @@ -34,24 +34,24 @@ export interface ConnectorProps extends ChipPropsSU { bulletGender?: "male" | "female" } +export const bulletDiameter = distance.pipe( + 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 const connectorProps = chipProps .extend({ standard: connectorStandard.optional(), pinCount: z.number().int().positive().optional(), - bulletDiameter: distance - .pipe( - 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), - ]), - ) - .optional(), + bulletDiameter: bulletDiameter.optional(), bulletGender: z.enum(["male", "female"]).optional(), }) .superRefine((connector, ctx) => { diff --git a/tests/assembly-cable-connector.test.ts b/tests/assembly-cable-connector.test.ts new file mode 100644 index 00000000..d2316ad9 --- /dev/null +++ b/tests/assembly-cable-connector.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from "bun:test" +import { assemblySubassemblyProps } from "../lib/assembly/subassembly" + +test("CAD cable connectors validate mating interfaces and normalize physical lengths", () => { + const phases = { + standard: "bullet", + bulletDiameter: "3.5mm", + bulletGender: "male", + pinCount: 3, + position: { x: "10mm", y: 2, z: "5mm" }, + facingDirection: "x+", + } + expect( + assemblySubassemblyProps.parse({ + name: "MOTOR", + modelUrl: "https://example.com/motor.glb", + cableConnectors: { phases }, + }).cableConnectors?.phases, + ).toEqual({ ...phases, bulletDiameter: 3.5, position: { x: 10, y: 2, z: 5 } }) + for (const invalid of [ + { ...phases, pinCount: 0 }, + { ...phases, pinCount: 17 }, + { ...phases, bulletDiameter: 7 }, + { ...phases, bulletGender: "socket" }, + { ...phases, facingDirection: "front" }, + { ...phases, position: { x: NaN, y: 0, z: 0 } }, + ]) + expect( + assemblySubassemblyProps.safeParse({ + name: "MOTOR", + cableConnectors: { phases: invalid }, + }).success, + ).toBe(false) +}) From bc671a14a1c28972fe9e850d825e4c77cbb56435 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Tue, 6 Oct 2026 14:41:10 -0700 Subject: [PATCH 5/9] Keep expected CAD connector literal types precise in schema tests --- tests/assembly-cable-connector.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/assembly-cable-connector.test.ts b/tests/assembly-cable-connector.test.ts index d2316ad9..2da25b2e 100644 --- a/tests/assembly-cable-connector.test.ts +++ b/tests/assembly-cable-connector.test.ts @@ -9,7 +9,7 @@ test("CAD cable connectors validate mating interfaces and normalize physical len pinCount: 3, position: { x: "10mm", y: 2, z: "5mm" }, facingDirection: "x+", - } + } as const expect( assemblySubassemblyProps.parse({ name: "MOTOR", From 813a9f61c8e3c55fd68ca6f8ebabee6a3548e0e2 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Tue, 6 Oct 2026 16:28:24 -0700 Subject: [PATCH 6/9] Document generic adapter cables with independent connector ends --- docs/assembly-cable.md | 25 +++++++++++++++++++++---- generated/PROPS_OVERVIEW.md | 4 +++- lib/assembly/cable.ts | 8 +++++--- tests/assembly-cable.test.ts | 10 ++++++++++ 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/assembly-cable.md b/docs/assembly-cable.md index ca819ebe..fd013b6f 100644 --- a/docs/assembly-cable.md +++ b/docs/assembly-cable.md @@ -23,7 +23,7 @@ validator is exported as `assemblyCableProps` and `assemblyProps.cable`. | `name` | Required nonempty string | Trimmed cable identity | | `from` | Required nonempty string | Trimmed start endpoint reference | | `to` | Required nonempty string | Trimmed end endpoint reference | -| `standard` | Optional `"usb_c"` or `"bullet"` | Unchanged; omitted when not supplied | +| `standard` | Optional `"usb_c"`, `"bullet"`, or `"adaptercable"` | Unchanged; omitted when not supplied | Endpoints can be connector selectors or named assembly references. The initial `usb_c` preset represents a USB-C-to-USB-C cable. Omitting `standard` leaves @@ -52,7 +52,7 @@ connector, as for other custom connectors. Each cable end has the opposite gender of its selected connector. The example emits `bullet_d3.5mm_afemale_bmale`. Same-gender PCB endpoints produce a cable with two ends of the opposite gender. Contact counts must match; different -diameters produce an adapter cable with named `da` and `db` parameters. +diameters or connector families produce a generic `adaptercable_a(...)_b(...)`. Omitting the cable's `standard` infers `bullet` from the endpoints. There are no aliases, and existing USB-C/JST usage requires no migration. The bullet models represent generic solder contacts rather than a specific @@ -79,9 +79,9 @@ it does not require a placeholder PCB connector or a cable-to-cable joint. from="MOTOR.phases" to=".J_PHASES" /> ``` -The resulting cable is `bullet3_da3.5mm_db4mm_afemale_bfemale`: three +The resulting cable is `adaptercable_a(bullet3_d3.5mm_gfemale)_b(bullet3_d4mm_gfemale)`: three 3.5 mm female contacts on the motor side and three 4 mm female contacts on -the board side. A board with female sockets instead produces `bmale`. +the board side. A board with female sockets instead uses `gmale` in the B connector string. `cableConnectors` is an optional record of named interfaces. Each interface requires `standard="bullet"`, `bulletDiameter`, `bulletGender`, `position`, and @@ -95,3 +95,20 @@ position or gender is guessed from the mesh. Use the measured connector center for the real motor asset. Invalid sizes, counts, directions, unknown connector names, or incompatible standards fail explicitly. No aliases or migration changes are introduced for existing assemblies. + + +## Generic adapter composition + +Each adapter end is an independent connector specification. For example, +`adaptercable_a(bullet3_d3.5mm_gfemale)_b(jst_ph_pins3)` and +`adaptercable_a(jst_sh_pins4)_b(jst_ph_pins4)` use the same composition mechanism +as unequal-size bullet leads. Both ends retain their native contact pitch, and +wires fan between them. Explicit contact counts must match; the physical cable +specification does not define electrical pin mapping. + +Omitting `standard` infers an adapter when needed and retains existing presets +when the ends match. Set `standard="adaptercable"` to request the generic wrapper +explicitly, including for matching ends. `standard="bullet"` and `"usb_c"` +constrain both endpoint families; different bullet sizes are still allowed. +All adapter parameters are named; the older proposed `da`/`db` diameter +modifiers are unsupported. Existing cable strings and TSX need no migration. diff --git a/generated/PROPS_OVERVIEW.md b/generated/PROPS_OVERVIEW.md index 2547d786..8b280179 100644 --- a/generated/PROPS_OVERVIEW.md +++ b/generated/PROPS_OVERVIEW.md @@ -179,7 +179,9 @@ export interface AssemblyCableProps { from: string /** End connector selector or named assembly reference. */ to: string - /** Optional cable standard; bullet size and end genders are inferred from endpoints. Omit to infer the cable from its endpoints. */ + /** Optional cable standard. adaptercable composes independent ends; omit to + * infer an adapter or an existing same-end preset from the endpoints. + */ standard?: AssemblyCableStandard } diff --git a/lib/assembly/cable.ts b/lib/assembly/cable.ts index 005ac90a..9e3e4506 100644 --- a/lib/assembly/cable.ts +++ b/lib/assembly/cable.ts @@ -1,7 +1,7 @@ import { expectTypesMatch } from "lib/typecheck" import { z } from "zod" -export type AssemblyCableStandard = "usb_c" | "bullet" +export type AssemblyCableStandard = "usb_c" | "bullet" | "adaptercable" export interface AssemblyCableProps { /** Stable assembly identity for the cable. */ @@ -10,7 +10,9 @@ export interface AssemblyCableProps { from: string /** End connector selector or named assembly reference. */ to: string - /** Optional cable standard; bullet size and end genders are inferred from endpoints. Omit to infer the cable from its endpoints. */ + /** Optional cable standard. adaptercable composes independent ends; omit to + * infer an adapter or an existing same-end preset from the endpoints. + */ standard?: AssemblyCableStandard } @@ -18,7 +20,7 @@ export const assemblyCableProps = z.object({ name: z.string().trim().min(1), from: z.string().trim().min(1), to: z.string().trim().min(1), - standard: z.enum(["usb_c", "bullet"]).optional(), + standard: z.enum(["usb_c", "bullet", "adaptercable"]).optional(), }) export type AssemblyCablePropsInput = z.input diff --git a/tests/assembly-cable.test.ts b/tests/assembly-cable.test.ts index 0911072d..b3629c18 100644 --- a/tests/assembly-cable.test.ts +++ b/tests/assembly-cable.test.ts @@ -34,6 +34,16 @@ test("assembly.cable accepts the USB-C preset and trims endpoint references", () }) }) +test("assembly.cable accepts independent-ended adapter composition", () => { + const input: AssemblyCableProps = { + name: "PHASE_LEADS", + from: "MOTOR.phases", + to: ".J_PHASES", + standard: "adaptercable", + } + expect(assemblyCableProps.parse(input)).toEqual(input) +}) + test("assembly.cable requires an identity and two nonempty endpoint strings", () => { const valid = { name: "MOTOR_CABLE", From 5f09a99be237a6c050c802d0ddf9e5ef7236066a Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Tue, 6 Oct 2026 21:09:21 -0700 Subject: [PATCH 7/9] Accept physical connector model strings independently of standards --- README.md | 3 +++ generated/COMPONENT_TYPES.md | 3 +++ generated/PROPS_OVERVIEW.md | 3 +++ lib/components/connector.ts | 4 ++++ tests/connector-modelprinter-string.test.ts | 14 ++++++++++++++ 5 files changed, 27 insertions(+) create mode 100644 tests/connector-modelprinter-string.test.ts diff --git a/README.md b/README.md index d484a1b3..728eb3ab 100644 --- a/README.md +++ b/README.md @@ -733,6 +733,9 @@ export interface ConnectorProps extends ChipPropsSU { */ standard?: ConnectorStandard; + /** Physical connector model specification, e.g. bullet3_d3.5mm_gmale. */ + modelprinterString?: string; + /** * Number of electrical circuits in the connector; bullet groups support 1–16 (default 1) */ diff --git a/generated/COMPONENT_TYPES.md b/generated/COMPONENT_TYPES.md index 2484cbbb..a085b48c 100644 --- a/generated/COMPONENT_TYPES.md +++ b/generated/COMPONENT_TYPES.md @@ -2177,6 +2177,8 @@ export const chipProps = commonComponentProps.extend({ export interface ConnectorProps extends ChipPropsSU { standard?: ConnectorStandard + modelprinterString?: string + pinCount?: number bulletDiameter?: number | string @@ -2184,6 +2186,7 @@ export interface ConnectorProps extends ChipPropsSU { } .extend({ standard: connectorStandard.optional(), + modelprinterString: z.string().optional(), pinCount: z.number().int().positive().optional(), bulletDiameter: bulletDiameter.optional(), bulletGender: z.enum(["male", "female"]).optional(), diff --git a/generated/PROPS_OVERVIEW.md b/generated/PROPS_OVERVIEW.md index d1119b5b..22ba7b9e 100644 --- a/generated/PROPS_OVERVIEW.md +++ b/generated/PROPS_OVERVIEW.md @@ -1105,6 +1105,9 @@ export interface ConnectorProps extends ChipPropsSU { */ standard?: ConnectorStandard + /** Physical connector model specification, e.g. bullet3_d3.5mm_gmale. */ + modelprinterString?: string + /** * Number of electrical circuits in the connector; bullet groups support 1–16 (default 1) */ diff --git a/lib/components/connector.ts b/lib/components/connector.ts index e6f36534..ca774734 100644 --- a/lib/components/connector.ts +++ b/lib/components/connector.ts @@ -23,6 +23,9 @@ export interface ConnectorProps extends ChipPropsSU { */ standard?: ConnectorStandard + /** Physical connector model specification, e.g. bullet3_d3.5mm_gmale. */ + modelprinterString?: string + /** * Number of electrical circuits in the connector; bullet groups support 1–16 (default 1) */ @@ -50,6 +53,7 @@ export const bulletDiameter = distance.pipe( export const connectorProps = chipProps .extend({ standard: connectorStandard.optional(), + modelprinterString: z.string().optional(), pinCount: z.number().int().positive().optional(), bulletDiameter: bulletDiameter.optional(), bulletGender: z.enum(["male", "female"]).optional(), diff --git a/tests/connector-modelprinter-string.test.ts b/tests/connector-modelprinter-string.test.ts new file mode 100644 index 00000000..34f5a919 --- /dev/null +++ b/tests/connector-modelprinter-string.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from "bun:test" +import { connectorProps } from "lib" + +test("connector model specifications do not require a standardized interface", () => { + expect( + connectorProps.parse({ + name: "J1", + modelprinterString: "bullet3_d3.5mm_gmale", + }), + ).toMatchObject({ modelprinterString: "bullet3_d3.5mm_gmale" }) + expect( + connectorProps.safeParse({ name: "J1", modelprinterString: 3.5 }).success, + ).toBe(false) +}) From e3d0f5e5b4ca6721eb07b3571ba69f333ddef308 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Tue, 6 Oct 2026 21:20:33 -0700 Subject: [PATCH 8/9] Use model for physical connector specifications --- README.md | 2 +- generated/COMPONENT_TYPES.md | 4 ++-- generated/PROPS_OVERVIEW.md | 2 +- lib/components/connector.ts | 4 ++-- tests/connector-modelprinter-string.test.ts | 10 +++++----- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 728eb3ab..5c91670c 100644 --- a/README.md +++ b/README.md @@ -734,7 +734,7 @@ export interface ConnectorProps extends ChipPropsSU { standard?: ConnectorStandard; /** Physical connector model specification, e.g. bullet3_d3.5mm_gmale. */ - modelprinterString?: string; + model?: string; /** * Number of electrical circuits in the connector; bullet groups support 1–16 (default 1) diff --git a/generated/COMPONENT_TYPES.md b/generated/COMPONENT_TYPES.md index a085b48c..df7d0693 100644 --- a/generated/COMPONENT_TYPES.md +++ b/generated/COMPONENT_TYPES.md @@ -2177,7 +2177,7 @@ export const chipProps = commonComponentProps.extend({ export interface ConnectorProps extends ChipPropsSU { standard?: ConnectorStandard - modelprinterString?: string + model?: string pinCount?: number @@ -2186,7 +2186,7 @@ export interface ConnectorProps extends ChipPropsSU { } .extend({ standard: connectorStandard.optional(), - modelprinterString: z.string().optional(), + model: z.string().trim().min(1).optional(), pinCount: z.number().int().positive().optional(), bulletDiameter: bulletDiameter.optional(), bulletGender: z.enum(["male", "female"]).optional(), diff --git a/generated/PROPS_OVERVIEW.md b/generated/PROPS_OVERVIEW.md index 22ba7b9e..12300d57 100644 --- a/generated/PROPS_OVERVIEW.md +++ b/generated/PROPS_OVERVIEW.md @@ -1106,7 +1106,7 @@ export interface ConnectorProps extends ChipPropsSU { standard?: ConnectorStandard /** Physical connector model specification, e.g. bullet3_d3.5mm_gmale. */ - modelprinterString?: string + model?: string /** * Number of electrical circuits in the connector; bullet groups support 1–16 (default 1) diff --git a/lib/components/connector.ts b/lib/components/connector.ts index ca774734..b247612c 100644 --- a/lib/components/connector.ts +++ b/lib/components/connector.ts @@ -24,7 +24,7 @@ export interface ConnectorProps extends ChipPropsSU { standard?: ConnectorStandard /** Physical connector model specification, e.g. bullet3_d3.5mm_gmale. */ - modelprinterString?: string + model?: string /** * Number of electrical circuits in the connector; bullet groups support 1–16 (default 1) @@ -53,7 +53,7 @@ export const bulletDiameter = distance.pipe( export const connectorProps = chipProps .extend({ standard: connectorStandard.optional(), - modelprinterString: z.string().optional(), + model: z.string().trim().min(1).optional(), pinCount: z.number().int().positive().optional(), bulletDiameter: bulletDiameter.optional(), bulletGender: z.enum(["male", "female"]).optional(), diff --git a/tests/connector-modelprinter-string.test.ts b/tests/connector-modelprinter-string.test.ts index 34f5a919..afd2e08e 100644 --- a/tests/connector-modelprinter-string.test.ts +++ b/tests/connector-modelprinter-string.test.ts @@ -5,10 +5,10 @@ test("connector model specifications do not require a standardized interface", ( expect( connectorProps.parse({ name: "J1", - modelprinterString: "bullet3_d3.5mm_gmale", + model: "bullet3_d3.5mm_gmale", }), - ).toMatchObject({ modelprinterString: "bullet3_d3.5mm_gmale" }) - expect( - connectorProps.safeParse({ name: "J1", modelprinterString: 3.5 }).success, - ).toBe(false) + ).toMatchObject({ model: "bullet3_d3.5mm_gmale" }) + expect(connectorProps.safeParse({ name: "J1", model: 3.5 }).success).toBe( + false, + ) }) From 30c249fe42b3c7a933e18a7c8fa51cf422e6d72f Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Tue, 6 Oct 2026 21:33:17 -0700 Subject: [PATCH 9/9] Keep connector model data in model strings --- README.md | 9 +-- docs/assembly-cable.md | 91 +++++++------------------- generated/COMPONENT_TYPES.md | 18 +++-- generated/PROPS_OVERVIEW.md | 17 ++--- lib/assembly/cable-connector.ts | 14 +--- lib/assembly/cable.ts | 4 +- lib/components/connector.ts | 63 ++---------------- tests/assembly-cable-connector.test.ts | 13 ++-- tests/bullet-connector.test.ts | 66 +++---------------- tests/connector.test.ts | 3 - 10 files changed, 61 insertions(+), 237 deletions(-) diff --git a/README.md b/README.md index 5c91670c..98097601 100644 --- a/README.md +++ b/README.md @@ -733,18 +733,13 @@ export interface ConnectorProps extends ChipPropsSU { */ standard?: ConnectorStandard; - /** Physical connector model specification, e.g. bullet3_d3.5mm_gmale. */ + /** Modelprinter specification for the physical connector model. */ model?: string; /** - * Number of electrical circuits in the connector; bullet groups support 1–16 (default 1) + * Number of electrical circuits in the connector */ pinCount?: number; - - /** Nominal bullet contact diameter in mm or a length string; required for bullet. */ - bulletDiameter?: number | string; - /** Gender of the endpoint connector; the attached cable uses the opposite gender. */ - bulletGender?: "male" | "female"; } ``` diff --git a/docs/assembly-cable.md b/docs/assembly-cable.md index fd013b6f..3ad16ec1 100644 --- a/docs/assembly-cable.md +++ b/docs/assembly-cable.md @@ -23,7 +23,7 @@ validator is exported as `assemblyCableProps` and `assemblyProps.cable`. | `name` | Required nonempty string | Trimmed cable identity | | `from` | Required nonempty string | Trimmed start endpoint reference | | `to` | Required nonempty string | Trimmed end endpoint reference | -| `standard` | Optional `"usb_c"`, `"bullet"`, or `"adaptercable"` | Unchanged; omitted when not supplied | +| `standard` | Optional `"usb_c"` or `"adaptercable"` | Unchanged; omitted when not supplied | Endpoints can be connector selectors or named assembly references. The initial `usb_c` preset represents a USB-C-to-USB-C cable. Omitting `standard` leaves @@ -34,81 +34,34 @@ This is an additive props API and requires no migration. The schema validates the inputs; endpoint resolution, routing, and rendering require support in core and the CAD renderer. -Bullet connectors use `standard="bullet"`, a required `bulletDiameter` (2, 3, -3.5, 4, 5, 5.5, 6, or 8 mm), and a required `bulletGender="male" | "female"`. -Length strings such as `"3.5mm"` normalize to millimeters. A bullet group has 1–16 -contacts; `pinCount` defaults to 1. Set `pinCount={3}` on both endpoint connectors -for three bullet contacts and three insulated wires, emitting `bullet3_d3.5mm_afemale_bmale`. Supply a footprint for each PCB -connector, as for other custom connectors. +## Physical connector models + +Use a connector's `model` to describe its physical mating interface. `standard` +remains independent and can supply other connector behavior. Core preserves +model specifications as `modelprinter_string` in Circuit JSON and infers cable +ends from them. Known contact counts must match. ```tsx - - - + + + ``` -Each cable end has the opposite gender of its selected connector. The example -emits `bullet_d3.5mm_afemale_bmale`. Same-gender PCB endpoints produce a cable -with two ends of the opposite gender. Contact counts must match; different -diameters or connector families produce a generic `adaptercable_a(...)_b(...)`. -Omitting the cable's `standard` infers `bullet` from the endpoints. -There are no aliases, and existing USB-C/JST usage requires no migration. -The bullet models represent generic solder contacts rather than a specific -manufacturer's production part. - -## Imported BLDC CAD and adapter leads - -An imported motor can expose a named physical connector through -`assembly.subassembly` (also `assembly.cadassembly`). `assembly.motor` remains -the NEMA preset API. This attaches the cable to the imported CAD assembly; -it does not require a placeholder PCB connector or a cable-to-cable joint. +Imported CAD can expose named interfaces with a `model`, measured `position` +and outward `facingDirection`. Positions use model-local physical millimeters, +before the CAD's rotation and position offset. ```tsx - - - + ``` -The resulting cable is `adaptercable_a(bullet3_d3.5mm_gfemale)_b(bullet3_d4mm_gfemale)`: three -3.5 mm female contacts on the motor side and three 4 mm female contacts on -the board side. A board with female sockets instead uses `gmale` in the B connector string. - -`cableConnectors` is an optional record of named interfaces. Each interface -requires `standard="bullet"`, `bulletDiameter`, `bulletGender`, `position`, and -`facingDirection`; `pinCount` defaults to one and supports 1–16. Positions -accept numbers (mm) or unit strings and normalize to physical mm relative to -the CAD placement anchor, in its right-handed local frame. They follow the -CAD model's rotation and position offset. These physical connector offsets -are not scaled by mesh file units or object-fit scaling. Directions are one -of `x+`, `x-`, `y+`, `y-`, `z+`, or `z-`, and receive rotation only. No mating -position or gender is guessed from the mesh. Use the measured connector -center for the real motor asset. Invalid sizes, counts, directions, unknown -connector names, or incompatible standards fail explicitly. No aliases or -migration changes are introduced for existing assemblies. - - -## Generic adapter composition - -Each adapter end is an independent connector specification. For example, -`adaptercable_a(bullet3_d3.5mm_gfemale)_b(jst_ph_pins3)` and -`adaptercable_a(jst_sh_pins4)_b(jst_ph_pins4)` use the same composition mechanism -as unequal-size bullet leads. Both ends retain their native contact pitch, and -wires fan between them. Explicit contact counts must match; the physical cable -specification does not define electrical pin mapping. - -Omitting `standard` infers an adapter when needed and retains existing presets -when the ends match. Set `standard="adaptercable"` to request the generic wrapper -explicitly, including for matching ends. `standard="bullet"` and `"usb_c"` -constrain both endpoint families; different bullet sizes are still allowed. -All adapter parameters are named; the older proposed `da`/`db` diameter -modifiers are unsupported. Existing cable strings and TSX need no migration. +Different ends compose with `adaptercable_a(CONNECTOR)_b(CONNECTOR)`, including +JST SH-to-PH and other supported interfaces. Each end keeps its native contact +pitch. Set `standard="adaptercable"` to request that wrapper explicitly; otherwise +core retains compatible shorthand for matching ends. Wire bundles stay compact +through the middle, with fanout near each connector. diff --git a/generated/COMPONENT_TYPES.md b/generated/COMPONENT_TYPES.md index df7d0693..73e768cc 100644 --- a/generated/COMPONENT_TYPES.md +++ b/generated/COMPONENT_TYPES.md @@ -2180,17 +2180,15 @@ export interface ConnectorProps extends ChipPropsSU { model?: string pinCount?: number - - bulletDiameter?: number | string - bulletGender?: "male" | "female" } -.extend({ - standard: connectorStandard.optional(), - model: z.string().trim().min(1).optional(), - pinCount: z.number().int().positive().optional(), - bulletDiameter: bulletDiameter.optional(), - bulletGender: z.enum(["male", "female"]).optional(), - }) +/** + * Number of electrical circuits in the connector + */ +export const connectorProps = chipProps.extend({ + standard: connectorStandard.optional(), + model: z.string().trim().min(1).optional(), + pinCount: z.number().int().positive().optional(), +}) ``` ### constrainedlayout diff --git a/generated/PROPS_OVERVIEW.md b/generated/PROPS_OVERVIEW.md index 12300d57..0668b1a6 100644 --- a/generated/PROPS_OVERVIEW.md +++ b/generated/PROPS_OVERVIEW.md @@ -156,12 +156,8 @@ export interface AntennaProps extends CommonComponentProps { export interface AssemblyCableConnectorProps { - standard: "bullet" - bulletDiameter: Distance - /** Gender on the CAD assembly; the cable uses the opposite gender. */ - bulletGender: "male" | "female" - /** Number of independent circuits; defaults to one, 1–16. */ - pinCount?: number + /** Modelprinter specification for the physical mating interface. */ + model: string /** Mating center relative to the CAD placement anchor, in physical mm, * right-handed model-local +X/+Y/+Z, before its rotation and positionOffset. * Do not use mesh file units; this is already a physical offset in mm. @@ -1105,18 +1101,13 @@ export interface ConnectorProps extends ChipPropsSU { */ standard?: ConnectorStandard - /** Physical connector model specification, e.g. bullet3_d3.5mm_gmale. */ + /** Modelprinter specification for the physical connector model. */ model?: string /** - * Number of electrical circuits in the connector; bullet groups support 1–16 (default 1) + * Number of electrical circuits in the connector */ pinCount?: number - - /** Nominal bullet contact diameter in mm or a length string; required for bullet. */ - bulletDiameter?: number | string - /** Gender of the endpoint connector; the attached cable uses the opposite gender. */ - bulletGender?: "male" | "female" } diff --git a/lib/assembly/cable-connector.ts b/lib/assembly/cable-connector.ts index 6fa1c91b..ef6882bb 100644 --- a/lib/assembly/cable-connector.ts +++ b/lib/assembly/cable-connector.ts @@ -1,5 +1,4 @@ import { z } from "zod" -import { bulletDiameter } from "../components/connector" import { point3 } from "../common/point3" import { type CadModelAxisDirection, @@ -10,12 +9,8 @@ import { expectTypesMatch } from "lib/typecheck" /** A physical cable interface on imported CAD, independent of PCB footprints. */ export interface AssemblyCableConnectorProps { - standard: "bullet" - bulletDiameter: Distance - /** Gender on the CAD assembly; the cable uses the opposite gender. */ - bulletGender: "male" | "female" - /** Number of independent circuits; defaults to one, 1–16. */ - pinCount?: number + /** Modelprinter specification for the physical mating interface. */ + model: string /** Mating center relative to the CAD placement anchor, in physical mm, * right-handed model-local +X/+Y/+Z, before its rotation and positionOffset. * Do not use mesh file units; this is already a physical offset in mm. @@ -27,10 +22,7 @@ export interface AssemblyCableConnectorProps { export const assemblyCableConnectorProps = z .object({ - standard: z.literal("bullet"), - bulletDiameter, - bulletGender: z.enum(["male", "female"]), - pinCount: z.number().int().min(1).max(16).optional(), + model: z.string().trim().min(1), position: point3, facingDirection: cadModelAxisDirection, }) diff --git a/lib/assembly/cable.ts b/lib/assembly/cable.ts index 9e3e4506..f3574456 100644 --- a/lib/assembly/cable.ts +++ b/lib/assembly/cable.ts @@ -1,7 +1,7 @@ import { expectTypesMatch } from "lib/typecheck" import { z } from "zod" -export type AssemblyCableStandard = "usb_c" | "bullet" | "adaptercable" +export type AssemblyCableStandard = "usb_c" | "adaptercable" export interface AssemblyCableProps { /** Stable assembly identity for the cable. */ @@ -20,7 +20,7 @@ export const assemblyCableProps = z.object({ name: z.string().trim().min(1), from: z.string().trim().min(1), to: z.string().trim().min(1), - standard: z.enum(["usb_c", "bullet", "adaptercable"]).optional(), + standard: z.enum(["usb_c", "adaptercable"]).optional(), }) export type AssemblyCablePropsInput = z.input diff --git a/lib/components/connector.ts b/lib/components/connector.ts index b247612c..3fb76dcf 100644 --- a/lib/components/connector.ts +++ b/lib/components/connector.ts @@ -1,11 +1,9 @@ import { chipProps, type ChipPropsSU } from "./chip" import { expectTypesMatch } from "lib/typecheck" import { z } from "zod" -import { distance } from "lib/common/distance" export const connectorStandard = z.enum([ "usb_c", - "bullet", "m2", "jst_sh", "jst_gh", @@ -23,67 +21,20 @@ export interface ConnectorProps extends ChipPropsSU { */ standard?: ConnectorStandard - /** Physical connector model specification, e.g. bullet3_d3.5mm_gmale. */ + /** Modelprinter specification for the physical connector model. */ model?: string /** - * Number of electrical circuits in the connector; bullet groups support 1–16 (default 1) + * Number of electrical circuits in the connector */ pinCount?: number - - /** Nominal bullet contact diameter in mm or a length string; required for bullet. */ - bulletDiameter?: number | string - /** Gender of the endpoint connector; the attached cable uses the opposite gender. */ - bulletGender?: "male" | "female" } -export const bulletDiameter = distance.pipe( - 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 const connectorProps = chipProps - .extend({ - standard: connectorStandard.optional(), - model: z.string().trim().min(1).optional(), - pinCount: z.number().int().positive().optional(), - bulletDiameter: bulletDiameter.optional(), - bulletGender: z.enum(["male", "female"]).optional(), - }) - .superRefine((connector, ctx) => { - if (connector.standard === "bullet") { - for (const field of ["bulletDiameter", "bulletGender"] as const) { - if (connector[field] === undefined) - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: [field], - message: `${field} is required for bullet connectors`, - }) - } - if (connector.pinCount !== undefined && connector.pinCount > 16) - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["pinCount"], - message: "Bullet groups support 1 to 16 electrical contacts", - }) - } else if ( - connector.bulletDiameter !== undefined || - connector.bulletGender !== undefined - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "bulletDiameter and bulletGender require standard=bullet", - }) - } - }) +export const connectorProps = chipProps.extend({ + standard: connectorStandard.optional(), + model: z.string().trim().min(1).optional(), + pinCount: z.number().int().positive().optional(), +}) export type ParsedConnectorProps = z.output diff --git a/tests/assembly-cable-connector.test.ts b/tests/assembly-cable-connector.test.ts index 2da25b2e..dd4a402c 100644 --- a/tests/assembly-cable-connector.test.ts +++ b/tests/assembly-cable-connector.test.ts @@ -3,10 +3,7 @@ import { assemblySubassemblyProps } from "../lib/assembly/subassembly" test("CAD cable connectors validate mating interfaces and normalize physical lengths", () => { const phases = { - standard: "bullet", - bulletDiameter: "3.5mm", - bulletGender: "male", - pinCount: 3, + model: "bullet3_d3.5mm_gmale", position: { x: "10mm", y: 2, z: "5mm" }, facingDirection: "x+", } as const @@ -16,12 +13,10 @@ test("CAD cable connectors validate mating interfaces and normalize physical len modelUrl: "https://example.com/motor.glb", cableConnectors: { phases }, }).cableConnectors?.phases, - ).toEqual({ ...phases, bulletDiameter: 3.5, position: { x: 10, y: 2, z: 5 } }) + ).toEqual({ ...phases, position: { x: 10, y: 2, z: 5 } }) for (const invalid of [ - { ...phases, pinCount: 0 }, - { ...phases, pinCount: 17 }, - { ...phases, bulletDiameter: 7 }, - { ...phases, bulletGender: "socket" }, + { ...phases, model: "" }, + { ...phases, model: 3 }, { ...phases, facingDirection: "front" }, { ...phases, position: { x: NaN, y: 0, z: 0 } }, ]) diff --git a/tests/bullet-connector.test.ts b/tests/bullet-connector.test.ts index 81279413..29be2ee0 100644 --- a/tests/bullet-connector.test.ts +++ b/tests/bullet-connector.test.ts @@ -1,67 +1,19 @@ import { expect, test } from "bun:test" -import { assemblyCableProps, connectorProps } from "lib" +import { connectorProps, assemblyCableProps } from "lib" -test("bullet connector props require a supported diameter and endpoint gender", () => { +test("physical model families are independent of connector standards", () => { expect( - connectorProps.parse({ - name: "J1", - standard: "bullet", - bulletDiameter: "3.5mm", - bulletGender: "female", - pinCount: 1, - }), - ).toMatchObject({ bulletDiameter: 3.5, bulletGender: "female" }) - for (const fields of [ - {}, - { bulletDiameter: 4 }, - { bulletGender: "male" }, - { bulletDiameter: 7, bulletGender: "male" }, - { bulletDiameter: 4, bulletGender: "socket" }, - { bulletDiameter: 4, bulletGender: "male", pinCount: 17 }, - ]) { - expect( - connectorProps.safeParse({ name: "J1", standard: "bullet", ...fields }) - .success, - ).toBe(false) - } + connectorProps.parse({ name: "J1", model: "bullet3_d3.5mm_gmale" }), + ).toMatchObject({ model: "bullet3_d3.5mm_gmale" }) expect( - connectorProps.safeParse({ - name: "J1", - standard: "usb_c", - bulletDiameter: 4, - }).success, + connectorProps.safeParse({ name: "J1", standard: "bullet" }).success, ).toBe(false) expect( - assemblyCableProps.parse({ - name: "POWER", + assemblyCableProps.safeParse({ + name: "C1", from: ".J1", to: ".J2", standard: "bullet", - }).standard, - ).toBe("bullet") -}) - -test("bullet groups accept a contact count and reject out-of-range counts", () => { - for (const pinCount of [1, 2, 3, 6, 16]) { - expect( - connectorProps.parse({ - name: "J1", - standard: "bullet", - bulletDiameter: 3.5, - bulletGender: "male", - pinCount, - }).pinCount, - ).toBe(pinCount) - } - for (const pinCount of [0, -1, 1.5, 17]) { - expect( - connectorProps.safeParse({ - name: "J1", - standard: "bullet", - bulletDiameter: 3.5, - bulletGender: "male", - pinCount, - }).success, - ).toBe(false) - } + }).success, + ).toBe(false) }) diff --git a/tests/connector.test.ts b/tests/connector.test.ts index 5318816b..f9581dfe 100644 --- a/tests/connector.test.ts +++ b/tests/connector.test.ts @@ -10,9 +10,6 @@ for (const standard of connectorStandard.options) { const raw: ConnectorProps = { name: "conn", standard, - ...(standard === "bullet" - ? { bulletDiameter: 4, bulletGender: "male" as const } - : {}), } const parsed = connectorProps.parse(raw) expect(parsed.standard).toBe(standard)