diff --git a/docs/nema-motors.md b/docs/nema-motors.md index ecc46b4..0a8c4ba 100644 --- a/docs/nema-motors.md +++ b/docs/nema-motors.md @@ -1,24 +1,27 @@ # NEMA stepper motors -`mp.string("nema8").json()`, `nema17`, and `nema23` resolve to `fn: "nema"` -and `nemaSize: 8 | 17 | 23`. Lengths are millimeters or unit-bearing strings. +`mp.string("nema8").json()`, `nema17`, `nema23`, and `nema34` resolve to `fn: "nema"` +and `nemaSize: 8 | 17 | 23 | 34`. Lengths are millimeters or unit-bearing strings. `nemaMotorModelPropsSchema` validates direct props with a required `nemaSize`. -| Default | NEMA 8 | NEMA 17 | NEMA 23 | -| --- | --- | --- | --- | -| Body width × length | 20.3 × 33 | 42.3 × 38 | 56.4 × 51 | -| Square hole pitch | 16 | 31 | 47.14 | -| Hole center coordinates | (±8, ±8) | (±15.5, ±15.5) | (±23.57, ±23.57) | -| Hole diameter / depth | 2 / 2 blind | 3 / 4.5 blind | 5 / through front flange | -| Pilot diameter × height | 15 × 1.5 | 22 × 2 | 38.1 × 1.6 | -| Shaft diameter × length from face | 4 × 15 | 5 × 24 | 6.35 × 20.6 | -| Shaft shape | round | D | D | -| D flat depth / length | 0.5 / 10 when enabled | 0.5 / 15 | 0.5 / 15 | +| Default | NEMA 8 | NEMA 17 | NEMA 23 | NEMA 34 | +| --- | --- | --- | --- | --- | +| Body width × length | 20.3 × 33 | 42.3 × 38 | 56.4 × 51 | 86 × 80 | +| Square hole pitch | 16 | 31 | 47.14 | 69.6 | +| Hole center coordinates | (±8, ±8) | (±15.5, ±15.5) | (±23.57, ±23.57) | (±34.8, ±34.8) | +| Hole diameter / depth | 2 / 2 blind | 3 / 4.5 blind | 5 / through front flange | 6.5 / through front flange | +| Pilot diameter × height | 15 × 1.5 | 22 × 2 | 38.1 × 1.6 | 73 × 1.6 | +| Shaft diameter × length from face | 4 × 15 | 5 × 24 | 6.35 × 20.6 | 14 × 32 | +| Shaft shape | round | D | D | round (keyway omitted) | +| D flat depth / length | 0.5 / 10 when enabled | 0.5 / 15 | 0.5 / 15 | 0.5 / 15 when enabled | Mounting, pilot and shaft dimensions follow representative Nanotec drawings: [SCA2018](https://www.nanotec.com/fileadmin/files/Baureihenuebersichten/Schrittmotoren/Product_Overview_SCA2018.pdf), [ST4118](https://www.nanotec.com/fileadmin/files/Baureihenuebersichten/Schrittmotoren/Product_Overview_ST4118.pdf), [ST5918](https://www.nanotec.com/fileadmin/files/Baureihenuebersichten/Schrittmotoren/Product_Overview_ST5918.pdf). +NEMA34 envelope, mounting, pilot and shaft dimensions follow the +[STEPPERONLINE 34HS31-6004S1 drawing](https://www.omc-stepperonline.com/download/34HS31-6004S1.pdf). +Its keyway is omitted and the default shaft is round; `_dshaft` enables an illustrative flat. Frame names do not guarantee body length or shaft details. D cuts, cap lengths and corner chamfers are illustrative defaults; no screw threads or wires are modeled. @@ -39,7 +42,7 @@ mp.string("nema23_shaftdiameter8mm_shaftlength25mm_flatdepth0.5mm_flatlength20mm Modifiers: `l` / `length` / `bodylength`, `bodywidth`, `shaftlength`, `shaftdiameter`, `round` / `dshaft`, `flatdepth`, `flatlength`, `flatangle`, `holespacing`, `holediameter`, `holedepth`, `throughholes` / `blindholes`, -`pilotdiameter`, `pilotlength`, `frontcap`, `rearcap`, `facechamfer`, `bodychamfer`. +`pilotdiameter`, `pilotlength`, `frontcap`, `rearcap`, `facechamfer`, `bodychamfer`, `rearchamfer`, `stepped` / `chamfered`. Duplicate, unknown and geometrically invalid parameters fail validation. This package defines parameters and parses model strings; it does not generate @@ -48,22 +51,34 @@ JSCAD solids and own the visual snapshots and geometry tests. ## Rear face -The rear face is Z=-bodyLength. Bare model strings show four installed socket-head -cap screws. `_backfaceholes` removes the screws and exposes four blind bores; +The rear face is Z=-bodyLength. NEMA8/17/23 model strings show four installed socket-head +cap screws; NEMA34 defaults to a plain rear casting. `_backfaceholes` removes the screws and exposes four blind bores; `_backfacescrews` explicitly selects installed screws. `_plainbackface` omits both. These flags are mutually exclusive. Direct props use `backFace: "holes" | "screws" | "plain"`. Rear fasteners are representative configurable details, not guaranteed by a NEMA -frame number. The square rear pitch defaults to the mounting pitch (16 / 31 / -47.14 mm), independently overridden by `backFaceHoleSpacing` / `_backholespacing`. -The enabled rear face uses the front-face outline so the NEMA23 rear fasteners -have support outside the chamfered core. Front flange holes are unchanged. - -| Rear default (mm) | NEMA8 | NEMA17 | NEMA23 | -| --- | --- | --- | --- | -| Screw size | M2 | M3 | M4 | -| Bore diameter / depth | 2 / 2 | 3 / 4.5 | 4 / 4.5 | -| Head diameter / height | 3.8 / 2 | 5.5 / 3 | 7 / 4 | +frame number. The square rear pitches default to 16 / 31 / 47.14 / 40 mm, +independently overridden by `backFaceHoleSpacing` / `_backholespacing`. +NEMA34 follows the rounded, stepped shoulders and plain rear casting visible in +STEPPERONLINE product photos. The rear follows the body profile instead of +repeating the mounting flange. Front assembly screws sit in edge recesses +separately from the mounting holes. The side wire bundle has a grommet, a 90 degree +default exit angle, 2 mm wires, and 25 mm visible length. Threads, laminations, +and the shaft keyway are omitted. + +These visual details are photo-based approximations, not manufacturing dimensions. +`cornerProfile` (`stepped` / `chamfered`) and `rearCornerChamfer` / `rearchamfer` +configure the profiles. In stepped mode, the chamfer value divided by sqrt(2) +sets the corner shoulder depth, with fillets up to 4 mm. Default body and rear +values are both 24 mm. Rear bores/screws remain opt-in with an illustrative +40 mm square pitch and can be overridden to match a specific motor. +NEMA8/17/23 retain their existing rear profiles. Front flange holes are unchanged. + +| Rear default (mm) | NEMA8 | NEMA17 | NEMA23 | NEMA34 | +| --- | --- | --- | --- | --- | +| Screw size | M2 | M3 | M4 | M5 | +| Bore diameter / depth | 2 / 2 | 3 / 4.5 | 4 / 4.5 | 5 / 6 | +| Head diameter / height | 3.8 / 2 | 5.5 / 3 | 7 / 4 | 8.5 / 5 | `backFaceScrewSize` / `_backscrewm3` selects the existing ISO 4762 bolt head/socket dimensions. `backFaceHoleDiameter` / `_backholediameter` and diff --git a/src/nema-motor-reference-points.ts b/src/nema-motor-reference-points.ts index c4b5018..67159ca 100644 --- a/src/nema-motor-reference-points.ts +++ b/src/nema-motor-reference-points.ts @@ -18,10 +18,40 @@ export function getNemaMotorReferencePoints(input: NemaMotorModelPropsInput) { const x = Math.cos(radians), y = Math.sin(radians) // Intersect the direction with the cap's chamfered square specification. - const radius = Math.min( + let radius = Math.min( p.bodyWidth / 2 / Math.max(Math.abs(x), Math.abs(y)), (p.bodyWidth - p.faceCornerChamfer) / (Math.abs(x) + Math.abs(y)), ) + if (p.nemaSize === 34 && p.cornerProfile === "stepped") { + const h = p.bodyWidth / 2, + c = p.bodyCornerChamfer / Math.SQRT2, + r = Math.min(4, c / 4) + const contains = (distance: number) => { + const a = Math.abs(distance * x), + b = Math.abs(distance * y) + if (a > h || b > h) return false + if (c === 0) return true + if (a > h - c && b > h - c) + return ( + a <= h - c + r && + b <= h - c + r && + Math.hypot(a - (h - c + r), b - (h - c + r)) >= r + ) + if (b > h - r && a > h - c - r) + return Math.hypot(a - (h - c - r), b - (h - r)) <= r + if (a > h - r && b > h - c - r) + return Math.hypot(a - (h - r), b - (h - c - r)) <= r + return true + } + let low = 0, + high = p.bodyWidth + for (let i = 0; i < 50; i++) { + const mid = (low + high) / 2 + if (contains(mid)) low = mid + else high = mid + } + radius = low + } const flatRadians = (p.shaftFlatAngle * Math.PI) / 180 return { frontface: { @@ -40,7 +70,10 @@ export function getNemaMotorReferencePoints(input: NemaMotorModelPropsInput) { position: { x: radius * x, y: radius * y, - z: -p.bodyLength + p.rearCapLength / 2, + z: + p.nemaSize === 34 + ? -p.bodyLength + p.rearCapLength + 3 + : -p.bodyLength + p.rearCapLength / 2, }, direction: { x, y, z: 0 }, }, diff --git a/src/nema-motor-schema.ts b/src/nema-motor-schema.ts index c68dc41..c9924fc 100644 --- a/src/nema-motor-schema.ts +++ b/src/nema-motor-schema.ts @@ -16,6 +16,7 @@ export const nemaSizeSchema = z.union([ z.literal(8), z.literal(17), z.literal(23), + z.literal(34), ]) export type NemaSize = z.infer @@ -24,6 +25,9 @@ export type NemaSize = z.infer * https://www.nanotec.com/fileadmin/files/Baureihenuebersichten/Schrittmotoren/Product_Overview_SCA2018.pdf * https://www.nanotec.com/fileadmin/files/Baureihenuebersichten/Schrittmotoren/Product_Overview_ST4118.pdf * https://www.nanotec.com/fileadmin/files/Baureihenuebersichten/Schrittmotoren/Product_Overview_ST5918.pdf + * NEMA 34 envelope, mounting/pilot and shaft: STEPPERONLINE 34HS31-6004S1: + * https://www.omc-stepperonline.com/download/34HS31-6004S1.pdf + * Its shaft keyway is omitted; the default shaft is round. * Cap thicknesses, chamfers and D cuts are illustrative, configurable details. * NEMA 8 also has 15.4 mm pitch / 16 mm pilot variants; override both together. */ @@ -94,6 +98,31 @@ export const nemaMotorDimensions = { faceCornerChamfer: 2, bodyCornerChamfer: 15, }, + 34: { + // Illustrative rear details; independent of the mounting flange. + backFaceHoleSpacing: 40, + rearCornerChamfer: 24, + backFaceScrewSize: "M5", + backFaceHoleDepth: 6, + bodyWidth: 86, + bodyLength: 80, + mountingHoleSpacing: 69.6, + mountingHoleDiameter: 6.5, + mountingHoleDepth: 10, + mountingHoleThrough: true, + pilotDiameter: 73, + pilotLength: 1.6, + shaftDiameter: 14, + shaftLength: 32, + shaftShape: "round", + shaftFlatDepth: 0.5, + shaftFlatLength: 15, + shaftFlatAngle: 0, + frontCapLength: 10, + rearCapLength: 8, + faceCornerChamfer: 2, + bodyCornerChamfer: 24, + }, } as const const shape = { @@ -135,6 +164,9 @@ const shape = { rearCapLength: positive.optional(), faceCornerChamfer: nonnegative.optional(), bodyCornerChamfer: nonnegative.optional(), + /** Rear outline is independent of the front mounting flange. */ + rearCornerChamfer: nonnegative.optional(), + cornerProfile: z.enum(["chamfered", "stepped"]).optional(), } const resolve = (p: z.output>) => { const d = nemaMotorDimensions[p.nemaSize] @@ -143,13 +175,17 @@ const resolve = (p: z.output>) => { return { nemaSize: p.nemaSize, wireConnection: p.wireConnection ?? "stubs", - wireSideAngle: (((p.wireSideAngle ?? 0) % 360) + 360) % 360, - wireLength: p.wireLength ?? 6, - wireDiameter: p.wireDiameter ?? 1.2, + wireSideAngle: + (((p.wireSideAngle ?? (p.nemaSize === 34 ? 90 : 0)) % 360) + 360) % 360, + wireLength: p.wireLength ?? (p.nemaSize === 34 ? 25 : 6), + wireDiameter: p.wireDiameter ?? (p.nemaSize === 34 ? 2 : 1.2), wireCount: p.wireCount ?? 4, - backFace: p.backFace ?? "screws", + backFace: p.backFace ?? (p.nemaSize === 34 ? "plain" : "screws"), backFaceHoleSpacing: - p.backFaceHoleSpacing ?? p.mountingHoleSpacing ?? d.mountingHoleSpacing, + p.backFaceHoleSpacing ?? + ("backFaceHoleSpacing" in d + ? d.backFaceHoleSpacing + : (p.mountingHoleSpacing ?? d.mountingHoleSpacing)), backFaceHoleDiameter: p.backFaceHoleDiameter ?? screw.diameter, backFaceHoleDepth: p.backFaceHoleDepth ?? d.backFaceHoleDepth, backFaceScrewSize, @@ -171,11 +207,30 @@ const resolve = (p: z.output>) => { rearCapLength: p.rearCapLength ?? d.rearCapLength, faceCornerChamfer: p.faceCornerChamfer ?? d.faceCornerChamfer, bodyCornerChamfer: p.bodyCornerChamfer ?? d.bodyCornerChamfer, + ...(p.nemaSize === 34 || p.cornerProfile !== undefined + ? { cornerProfile: p.cornerProfile ?? ("stepped" as const) } + : {}), + ...("rearCornerChamfer" in d || p.rearCornerChamfer !== undefined + ? { + rearCornerChamfer: + p.rearCornerChamfer ?? + ("rearCornerChamfer" in d + ? d.rearCornerChamfer + : (p.faceCornerChamfer ?? d.faceCornerChamfer)), + } + : {}), } } export type NemaMotorModelProps = ReturnType const validate = (p: NemaMotorModelProps, ctx: z.RefinementCtx) => { const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + const rearChamfer = + p.rearCornerChamfer ?? + (p.backFace === "plain" && p.mountingHoleThrough + ? p.bodyCornerChamfer + : p.faceCornerChamfer) + const rearEdge = + p.cornerProfile === "stepped" ? rearChamfer / Math.SQRT2 : rearChamfer if (p.backFace !== "plain") { const screw = hexSocketBoltDimensions[p.backFaceScrewSize] const radius = @@ -186,8 +241,10 @@ const validate = (p: NemaMotorModelProps, ctx: z.RefinementCtx) => { issue("Rear holes and screw heads must not overlap each other") if ( p.backFaceHoleSpacing + 2 * radius >= p.bodyWidth || - p.bodyWidth - p.faceCornerChamfer - p.backFaceHoleSpacing <= - radius * Math.SQRT2 + (p.cornerProfile === "stepped" + ? p.backFaceHoleSpacing + 2 * radius >= p.bodyWidth - 2 * rearEdge + : p.bodyWidth - rearChamfer - p.backFaceHoleSpacing <= + radius * Math.SQRT2) ) issue("Rear holes and screw heads must fit entirely inside the rear face") if (p.backFaceHoleDepth >= p.rearCapLength) @@ -196,23 +253,21 @@ const validate = (p: NemaMotorModelProps, ctx: z.RefinementCtx) => { issue("Rear holes must accommodate the screw diameter") } if (p.wireConnection === "stubs") { - if ( - p.wireDiameter * p.wireCount * 1.5 > - p.bodyWidth - 2 * p.faceCornerChamfer - ) + if (p.wireDiameter * p.wireCount * 1.5 > p.bodyWidth - 2 * rearEdge) issue("Wire bundle must fit on the cap side") if (p.wireDiameter >= p.rearCapLength) issue("Wire diameter must fit within the rear cap") } const connector = getJstMotorConnector(p.wireConnection) - if (connector && p.bodyWidth - 2 * p.faceCornerChamfer < connector.bodyWidth) + if (connector && p.bodyWidth - 2 * rearEdge < connector.bodyWidth) issue("JST header must fit on the motor side") const r = p.mountingHoleDiameter / 2 if (p.frontCapLength + p.rearCapLength >= p.bodyLength) issue("End caps must leave a positive body length") if ( p.faceCornerChamfer >= p.bodyWidth / 2 || - p.bodyCornerChamfer >= p.bodyWidth / 2 + p.bodyCornerChamfer >= p.bodyWidth / 2 || + rearChamfer >= p.bodyWidth / 2 ) issue("Chamfer must be smaller than half the body width") if ( @@ -230,7 +285,13 @@ const validate = (p: NemaMotorModelProps, ctx: z.RefinementCtx) => { issue("Blind mounting hole depth must be less than the front cap length") if ( p.mountingHoleThrough && - p.bodyWidth - p.bodyCornerChamfer - p.mountingHoleSpacing >= -r * Math.SQRT2 + (p.cornerProfile === "stepped" + ? p.bodyWidth / 2 - + p.bodyCornerChamfer / Math.SQRT2 + + Math.min(4, p.bodyCornerChamfer / Math.SQRT2 / 4) >= + p.mountingHoleSpacing / 2 - r + : p.bodyWidth - p.bodyCornerChamfer - p.mountingHoleSpacing >= + -r * Math.SQRT2) ) issue("Body must clear through front mounting holes") if (p.shaftLength <= p.pilotLength) diff --git a/src/normalize-nema-motor-model-string.ts b/src/normalize-nema-motor-model-string.ts index 20b3082..8b83d30 100644 --- a/src/normalize-nema-motor-model-string.ts +++ b/src/normalize-nema-motor-model-string.ts @@ -1,6 +1,6 @@ /** Accept assembly cable spellings without changing NEMA's resolved definition. */ export function normalizeNemaMotorModelString(value: string): string { - if (!/^nema(?:8|17|23)(?:_|$)/i.test(value)) return value + if (!/^nema(?:8|17|23|34)(?:_|$)/i.test(value)) return value return value .replace(/(^|_)jst([0-9]+)_(ph|sh)(?=_|$)/gi, "$1jst$3$2") .replace(/(^|_)jst[_-](ph|sh)[_-]([0-9]+)(?=_|$)/gi, "$1jst$2$3") diff --git a/src/parse-nema-motor-model-string.ts b/src/parse-nema-motor-model-string.ts index e63ac99..fd202f1 100644 --- a/src/parse-nema-motor-model-string.ts +++ b/src/parse-nema-motor-model-string.ts @@ -25,12 +25,13 @@ const lengths = { rearcap: "rearCapLength", facechamfer: "faceCornerChamfer", bodychamfer: "bodyCornerChamfer", + rearchamfer: "rearCornerChamfer", } as const export function parseNemaMotorModelParams(raw: RawModelprinterParams) { const tokens = normalizeNemaMotorModelString(raw.string).split("_") - const match = tokens[0]?.match(/^nema(8|17|23)$/i) - if (!match) throw new Error("Expected nema8, nema17 or nema23") + const match = tokens[0]?.match(/^nema(8|17|23|34)$/i) + if (!match) throw new Error("Expected nema8, nema17, nema23 or nema34") const props: Record = { fn: "nema", nemaSize: Number(match[1]), @@ -65,6 +66,10 @@ export function parseNemaMotorModelParams(raw: RawModelprinterParams) { : name === "nowires" ? "none" : "stubs" + } else if (name === "stepped" || name === "chamfered") { + if (value) throw new Error(`NEMA flag "${name}" does not accept a value`) + property = "cornerProfile" + parsed = name } else if (name === "wirecount") { property = "wireCount" if (!/^\d+$/.test(value)) throw new Error("wirecount requires an integer") diff --git a/tests/fixtures/assert-nema-motor.ts b/tests/fixtures/assert-nema-motor.ts index 477a1d1..167a78f 100644 --- a/tests/fixtures/assert-nema-motor.ts +++ b/tests/fixtures/assert-nema-motor.ts @@ -11,6 +11,7 @@ export function assertNemaMotor(size: NemaSize) { 8: { pitch: 16, diameter: 2, length: 33, shaft: 15 }, 17: { pitch: 31, diameter: 3, length: 38, shaft: 24 }, 23: { pitch: 47.14, diameter: 5, length: 51, shaft: 20.6 }, + 34: { pitch: 69.6, diameter: 6.5, length: 80, shaft: 32 }, }[size] const definition = mp.string(`NEMA${size}`).json() if (definition.fn !== "nema") throw new Error("Expected NEMA") @@ -19,10 +20,10 @@ export function assertNemaMotor(size: NemaSize) { expect(definition.mountingHoleSpacing).toBe(expected.pitch) expect(definition.mountingHoleDiameter).toBe(expected.diameter) const { fn, ...props } = definition - expect(props.backFace).toBe("screws") - expect(props.backFaceHoleSpacing).toBe(expected.pitch) + expect(props.backFace).toBe(size === 34 ? "plain" : "screws") + expect(props.backFaceHoleSpacing).toBe(size === 34 ? 40 : expected.pitch) expect(props.backFaceScrewSize).toBe( - ({ 8: "M2", 17: "M3", 23: "M4" } as const)[size], + ({ 8: "M2", 17: "M3", 23: "M4", 34: "M5" } as const)[size], ) for (const [token, mode] of [ ["backfaceholes", "holes"], diff --git a/tests/nema-motor-wire-references.test.ts b/tests/nema-motor-wire-references.test.ts index fe1b859..162384f 100644 --- a/tests/nema-motor-wire-references.test.ts +++ b/tests/nema-motor-wire-references.test.ts @@ -82,7 +82,7 @@ test("JST PH and SH variants accept canonical and legacy spellings", () => { } } } - for (const size of [8, 17, 23]) { + for (const size of [8, 17, 23, 34]) { for (const [alias, token] of [ ["none", "nowires"], ["stubs", "wirestubs"], diff --git a/tests/nema-motor.test.ts b/tests/nema-motor.test.ts index 4090d99..c52cb5f 100644 --- a/tests/nema-motor.test.ts +++ b/tests/nema-motor.test.ts @@ -1,5 +1,5 @@ import { test } from "bun:test" import { assertNemaMotor } from "./fixtures/assert-nema-motor" -for (const size of [8, 17, 23] as const) +for (const size of [8, 17, 23, 34] as const) test(`NEMA${size} parameter contract`, () => assertNemaMotor(size)) diff --git a/tests/nema34.test.ts b/tests/nema34.test.ts new file mode 100644 index 0000000..d510f8e --- /dev/null +++ b/tests/nema34.test.ts @@ -0,0 +1,101 @@ +import { expect, test } from "bun:test" +import { + mp, + nemaMotorModelPropsSchema, + getNemaMotorReferencePoints, +} from "../src" + +test("NEMA34 resolves its envelope and assembly reference points", () => { + const definition = mp.string("nema34").json() + if (definition.fn !== "nema") throw new Error("Expected NEMA") + expect(definition).toMatchObject({ + fn: "nema", + nemaSize: 34, + bodyWidth: 86, + bodyLength: 80, + mountingHoleSpacing: 69.6, + mountingHoleDiameter: 6.5, + mountingHoleThrough: true, + pilotDiameter: 73, + pilotLength: 1.6, + shaftDiameter: 14, + shaftLength: 32, + shaftShape: "round", + }) + const { fn, ...props } = definition + expect(nemaMotorModelPropsSchema.parse({ nemaSize: 34 })).toEqual(props) + const references = getNemaMotorReferencePoints({ nemaSize: 34 }) + expect(references.backface.position).toEqual({ x: 0, y: 0, z: -80 }) + expect(references.shafttip.position).toEqual({ x: 0, y: 0, z: 32 }) + expect(references.shaftflat).toBeUndefined() + expect( + mp.string("NEMA34_l10cm_shaftdiameter0.5in_shaftlength4cm").json(), + ).toMatchObject({ bodyLength: 100, shaftDiameter: 12.7, shaftLength: 40 }) +}) + +test("NEMA34 accepts wire aliases and rejects unsupported frames and conflicts", () => { + for (const family of ["ph", "sh"]) { + const canonical = mp.string(`nema34_jst${family}4_wireangle90deg`).json() + for (const alias of [ + `jst4_${family}`, + `jst_${family}_4`, + `jst-${family}-4`, + ]) + expect(mp.string(`NEMA34_${alias}_wireangle90deg`).json()).toEqual( + canonical, + ) + } + for (const source of [ + "nema340", + "nema34.5", + "nema35", + "nema34_jst4_ph_nowires", + "nema34_jst17_ph", + "nema34_round_dshaft", + "nema34_pilotdiameter86mm", + "nema34_holespacing85mm", + ]) + expect(() => mp.string(source).json()).toThrow() +}) + +test("NEMA34 rear profile and fasteners are independent of the front flange", () => { + expect(mp.string("nema34").json()).toMatchObject({ + backFaceHoleSpacing: 40, + rearCornerChamfer: 24, + mountingHoleSpacing: 69.6, + faceCornerChamfer: 2, + cornerProfile: "stepped", + backFace: "plain", + wireSideAngle: 90, + wireLength: 25, + }) + expect( + mp.string("nema34_holespacing68mm_facechamfer3mm").json(), + ).toMatchObject({ + backFaceHoleSpacing: 40, + rearCornerChamfer: 24, + }) + expect( + mp.string("nema34_rearchamfer1cm_backholespacing6cm").json(), + ).toMatchObject({ + rearCornerChamfer: 10, + backFaceHoleSpacing: 60, + }) + for (const source of [ + "nema34_backfacescrews_backholespacing69.6mm", + "nema34_backfacescrews_backholespacing58mm", + "nema34_plainbackface_rearchamfer43mm", + ]) + expect(() => mp.string(source).json()).toThrow() +}) + +test("NEMA34 wire exit follows the stepped body and stays consistent when hidden", () => { + const p = { nemaSize: 34 as const, wireSideAngle: 45 } + const visible = getNemaMotorReferencePoints(p).wireside + expect(visible.position.x).toBeCloseTo(27.2010101268, 5) + expect(visible.position.y).toBeCloseTo(visible.position.x, 5) + expect(visible.position.z).toBe(-69) + expect( + getNemaMotorReferencePoints({ ...p, wireConnection: "none" }).wireside, + ).toEqual(visible) +})