From 8cbf4e934b9477e0f63a2c5c73beeb5baacbe357 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Sat, 10 Oct 2026 12:31:47 -0700 Subject: [PATCH 1/3] Add NEMA34 motor parameter contract and model strings --- docs/nema-motors.md | 41 ++++++++-------- src/nema-motor-schema.ts | 26 +++++++++++ src/normalize-nema-motor-model-string.ts | 2 +- src/parse-nema-motor-model-string.ts | 4 +- tests/fixtures/assert-nema-motor.ts | 3 +- tests/nema-motor-wire-references.test.ts | 2 +- tests/nema-motor.test.ts | 2 +- tests/nema34.test.ts | 59 ++++++++++++++++++++++++ 8 files changed, 114 insertions(+), 25 deletions(-) create mode 100644 tests/nema34.test.ts diff --git a/docs/nema-motors.md b/docs/nema-motors.md index ecc46b4..26aa3a5 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. @@ -55,15 +58,15 @@ These flags are mutually exclusive. Direct props use `backFace: "holes" | "screw 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 +47.14 / 69.6 mm), independently overridden by `backFaceHoleSpacing` / `_backholespacing`. +The enabled rear face uses the front-face outline so the NEMA23/NEMA34 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 | +| 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-schema.ts b/src/nema-motor-schema.ts index c68dc41..a137fd0 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,28 @@ export const nemaMotorDimensions = { faceCornerChamfer: 2, bodyCornerChamfer: 15, }, + 34: { + 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 = { 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..7fa85a4 100644 --- a/src/parse-nema-motor-model-string.ts +++ b/src/parse-nema-motor-model-string.ts @@ -29,8 +29,8 @@ const lengths = { 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]), diff --git a/tests/fixtures/assert-nema-motor.ts b/tests/fixtures/assert-nema-motor.ts index 477a1d1..0ef7199 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") @@ -22,7 +23,7 @@ export function assertNemaMotor(size: NemaSize) { expect(props.backFace).toBe("screws") expect(props.backFaceHoleSpacing).toBe(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..88fb2bd --- /dev/null +++ b/tests/nema34.test.ts @@ -0,0 +1,59 @@ +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() +}) From 3a1f807810ff0e91e68c4ba98fbdc8649ad96719 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Sat, 10 Oct 2026 16:07:42 -0700 Subject: [PATCH 2/3] Give NEMA34 an independent chamfered rear cap and inset fasteners --- docs/nema-motors.md | 13 ++++++---- src/nema-motor-schema.ts | 37 +++++++++++++++++++++------- src/parse-nema-motor-model-string.ts | 1 + tests/fixtures/assert-nema-motor.ts | 2 +- tests/nema34.test.ts | 27 ++++++++++++++++++++ 5 files changed, 65 insertions(+), 15 deletions(-) diff --git a/docs/nema-motors.md b/docs/nema-motors.md index 26aa3a5..68e865d 100644 --- a/docs/nema-motors.md +++ b/docs/nema-motors.md @@ -42,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`. Duplicate, unknown and geometrically invalid parameters fail validation. This package defines parameters and parses model strings; it does not generate @@ -57,10 +57,13 @@ cap screws. `_backfaceholes` removes the screws and exposes four blind bores; 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 / 69.6 mm), independently overridden by `backFaceHoleSpacing` / `_backholespacing`. -The enabled rear face uses the front-face outline so the NEMA23/NEMA34 rear fasteners -have support outside the chamfered core. Front flange holes are unchanged. +frame number. The square rear pitches default to 16 / 31 / 47.14 / 58 mm, +independently overridden by `backFaceHoleSpacing` / `_backholespacing`. +NEMA34 uses an illustrative inset rear screw pattern and a 16 mm rear corner +chamfer, rather than a second mounting flange. `rearCornerChamfer` / `_rearchamfer` +configures the rear outline independently of `faceCornerChamfer` and the body. +These rear dimensions are not a manufacturer-specific drawing or mounting standard. +NEMA8/17/23 retain their existing rear profiles. Front flange holes are unchanged. | Rear default (mm) | NEMA8 | NEMA17 | NEMA23 | NEMA34 | | --- | --- | --- | --- | --- | diff --git a/src/nema-motor-schema.ts b/src/nema-motor-schema.ts index a137fd0..c538cc2 100644 --- a/src/nema-motor-schema.ts +++ b/src/nema-motor-schema.ts @@ -99,6 +99,9 @@ export const nemaMotorDimensions = { bodyCornerChamfer: 15, }, 34: { + // Illustrative rear details; independent of the mounting flange. + backFaceHoleSpacing: 58, + rearCornerChamfer: 16, backFaceScrewSize: "M5", backFaceHoleDepth: 6, bodyWidth: 86, @@ -161,6 +164,8 @@ const shape = { rearCapLength: positive.optional(), faceCornerChamfer: nonnegative.optional(), bodyCornerChamfer: nonnegative.optional(), + /** Rear outline is independent of the front mounting flange. */ + rearCornerChamfer: nonnegative.optional(), } const resolve = (p: z.output>) => { const d = nemaMotorDimensions[p.nemaSize] @@ -175,7 +180,10 @@ const resolve = (p: z.output>) => { wireCount: p.wireCount ?? 4, backFace: p.backFace ?? "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, @@ -197,11 +205,25 @@ const resolve = (p: z.output>) => { rearCapLength: p.rearCapLength ?? d.rearCapLength, faceCornerChamfer: p.faceCornerChamfer ?? d.faceCornerChamfer, bodyCornerChamfer: p.bodyCornerChamfer ?? d.bodyCornerChamfer, + ...("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) if (p.backFace !== "plain") { const screw = hexSocketBoltDimensions[p.backFaceScrewSize] const radius = @@ -212,8 +234,7 @@ 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.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) @@ -222,23 +243,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 * rearChamfer) 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 * rearChamfer < 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 ( diff --git a/src/parse-nema-motor-model-string.ts b/src/parse-nema-motor-model-string.ts index 7fa85a4..a4eb92c 100644 --- a/src/parse-nema-motor-model-string.ts +++ b/src/parse-nema-motor-model-string.ts @@ -25,6 +25,7 @@ const lengths = { rearcap: "rearCapLength", facechamfer: "faceCornerChamfer", bodychamfer: "bodyCornerChamfer", + rearchamfer: "rearCornerChamfer", } as const export function parseNemaMotorModelParams(raw: RawModelprinterParams) { diff --git a/tests/fixtures/assert-nema-motor.ts b/tests/fixtures/assert-nema-motor.ts index 0ef7199..abea282 100644 --- a/tests/fixtures/assert-nema-motor.ts +++ b/tests/fixtures/assert-nema-motor.ts @@ -21,7 +21,7 @@ export function assertNemaMotor(size: NemaSize) { expect(definition.mountingHoleDiameter).toBe(expected.diameter) const { fn, ...props } = definition expect(props.backFace).toBe("screws") - expect(props.backFaceHoleSpacing).toBe(expected.pitch) + expect(props.backFaceHoleSpacing).toBe(size === 34 ? 58 : expected.pitch) expect(props.backFaceScrewSize).toBe( ({ 8: "M2", 17: "M3", 23: "M4", 34: "M5" } as const)[size], ) diff --git a/tests/nema34.test.ts b/tests/nema34.test.ts index 88fb2bd..ffbfdb4 100644 --- a/tests/nema34.test.ts +++ b/tests/nema34.test.ts @@ -57,3 +57,30 @@ test("NEMA34 accepts wire aliases and rejects unsupported frames and conflicts", ]) 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: 58, + rearCornerChamfer: 16, + mountingHoleSpacing: 69.6, + faceCornerChamfer: 2, + }) + expect( + mp.string("nema34_holespacing68mm_facechamfer3mm").json(), + ).toMatchObject({ + backFaceHoleSpacing: 58, + rearCornerChamfer: 16, + }) + expect( + mp.string("nema34_rearchamfer1cm_backholespacing6cm").json(), + ).toMatchObject({ + rearCornerChamfer: 10, + backFaceHoleSpacing: 60, + }) + for (const source of [ + "nema34_backholespacing69.6mm", + "nema34_rearchamfer24mm", + "nema34_plainbackface_rearchamfer43mm", + ]) + expect(() => mp.string(source).json()).toThrow() +}) From 98b72a3435dcbd60b94fdbb8fc0820cc419be921 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Sat, 10 Oct 2026 16:17:46 -0700 Subject: [PATCH 3/3] Match NEMA34 photo with rounded shoulder reliefs and front assembly screws --- docs/nema-motors.md | 25 +++++++++++++------ src/nema-motor-reference-points.ts | 37 ++++++++++++++++++++++++++-- src/nema-motor-schema.ts | 36 +++++++++++++++++++-------- src/parse-nema-motor-model-string.ts | 4 +++ tests/fixtures/assert-nema-motor.ts | 4 +-- tests/nema34.test.ts | 27 +++++++++++++++----- 6 files changed, 105 insertions(+), 28 deletions(-) diff --git a/docs/nema-motors.md b/docs/nema-motors.md index 68e865d..0a8c4ba 100644 --- a/docs/nema-motors.md +++ b/docs/nema-motors.md @@ -42,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`, `rearchamfer`. +`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 @@ -51,18 +51,27 @@ 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 pitches default to 16 / 31 / 47.14 / 58 mm, +frame number. The square rear pitches default to 16 / 31 / 47.14 / 40 mm, independently overridden by `backFaceHoleSpacing` / `_backholespacing`. -NEMA34 uses an illustrative inset rear screw pattern and a 16 mm rear corner -chamfer, rather than a second mounting flange. `rearCornerChamfer` / `_rearchamfer` -configures the rear outline independently of `faceCornerChamfer` and the body. -These rear dimensions are not a manufacturer-specific drawing or mounting standard. +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 | 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 c538cc2..c9924fc 100644 --- a/src/nema-motor-schema.ts +++ b/src/nema-motor-schema.ts @@ -100,8 +100,8 @@ export const nemaMotorDimensions = { }, 34: { // Illustrative rear details; independent of the mounting flange. - backFaceHoleSpacing: 58, - rearCornerChamfer: 16, + backFaceHoleSpacing: 40, + rearCornerChamfer: 24, backFaceScrewSize: "M5", backFaceHoleDepth: 6, bodyWidth: 86, @@ -166,6 +166,7 @@ const shape = { 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] @@ -174,11 +175,12 @@ 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 ?? ("backFaceHoleSpacing" in d @@ -205,6 +207,9 @@ 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: @@ -224,6 +229,8 @@ const validate = (p: NemaMotorModelProps, ctx: z.RefinementCtx) => { (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 = @@ -234,7 +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 - rearChamfer - 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) @@ -243,13 +253,13 @@ 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 * rearChamfer) + 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 * rearChamfer < 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) @@ -275,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/parse-nema-motor-model-string.ts b/src/parse-nema-motor-model-string.ts index a4eb92c..fd202f1 100644 --- a/src/parse-nema-motor-model-string.ts +++ b/src/parse-nema-motor-model-string.ts @@ -66,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 abea282..167a78f 100644 --- a/tests/fixtures/assert-nema-motor.ts +++ b/tests/fixtures/assert-nema-motor.ts @@ -20,8 +20,8 @@ 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(size === 34 ? 58 : 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", 34: "M5" } as const)[size], ) diff --git a/tests/nema34.test.ts b/tests/nema34.test.ts index ffbfdb4..d510f8e 100644 --- a/tests/nema34.test.ts +++ b/tests/nema34.test.ts @@ -60,16 +60,20 @@ test("NEMA34 accepts wire aliases and rejects unsupported frames and conflicts", test("NEMA34 rear profile and fasteners are independent of the front flange", () => { expect(mp.string("nema34").json()).toMatchObject({ - backFaceHoleSpacing: 58, - rearCornerChamfer: 16, + 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: 58, - rearCornerChamfer: 16, + backFaceHoleSpacing: 40, + rearCornerChamfer: 24, }) expect( mp.string("nema34_rearchamfer1cm_backholespacing6cm").json(), @@ -78,9 +82,20 @@ test("NEMA34 rear profile and fasteners are independent of the front flange", () backFaceHoleSpacing: 60, }) for (const source of [ - "nema34_backholespacing69.6mm", - "nema34_rearchamfer24mm", + "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) +})