diff --git a/docs/balltransferunit.md b/docs/balltransferunit.md index 3d421ef..1521679 100644 --- a/docs/balltransferunit.md +++ b/docs/balltransferunit.md @@ -14,8 +14,8 @@ const definition = mp const dimensions = getBallTransferUnitDimensions() ``` -The roadmap spelling `_mount(face3hole)` is accepted as an alias for `_face3hole`. -Both produce `faceThreeHole: true`; repeated or mixed selectors are rejected. +The value-free flag `_face3hole` produces `faceThreeHole: true`. Repeated flags, +flags with values and enum selectors are rejected. Only this circular three-hole construction is supported. | Property | Tokens | Default (mm) | diff --git a/docs/button-screw.md b/docs/button-screw.md index 0b7ba7f..e34c557 100644 --- a/docs/button-screw.md +++ b/docs/button-screw.md @@ -1,6 +1,6 @@ # Button screw parameter contract -`buttonscrew_m3_l10mm` selects ISO 7380-1:2022 (second edition) by default. +`buttonscrew_m3_l10mm_hexsocket_fullthread_righthanded` selects ISO 7380-1:2022 (second edition) by default. The optional value-free flag in `buttonscrew_iso7380-1_m3_l10mm` selects the same dimensions and normalizes to `iso73801: true`. The schemas reject false ISO flags and the legacy `standard` property; `standard(...)` selectors, @@ -27,8 +27,13 @@ is not claimed. Longer or partially threaded screws require a separate contract. `m` and `l`/`length` are required. Length inputs accept millimeter numbers or complete numeric strings with `mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, or -`feet`; JSON normalizes them to mm. Selectors `drive(hexsocket)`, `thread(full)`, -`threadhand(right)`, and `threadclass(6g)` have those fixed defaults. +`feet`; JSON normalizes them to mm. The value-free flags `hexsocket` and +`fullthread` directly set the boolean props `hexSocket: true` and +`fullThread: true`; both fixed features default true. `righthanded` sets +`leftHanded: false`, its default and the only supported handedness in this family. +`lefthanded` is rejected. `threadclass(6g)` retains the ISO thread-class designation. +The former drive/thread/hand selector tokens and corresponding string properties +are rejected. Repeated and conflicting flags are rejected before schema validation. `threadpitch` may explicitly restate the coarse pitch, including equivalent units; fine pitch is rejected. `threads`/`nothreads` control `showThreads`, which defaults true. Flags have no value. Duplicate tokens, alias conflicts, malformed diff --git a/docs/cable-grommet.md b/docs/cable-grommet.md index affbcd5..169dad4 100644 --- a/docs/cable-grommet.md +++ b/docs/cable-grommet.md @@ -1,12 +1,14 @@ # Cable grommet model strings ``` -cablegrommet_panelhole20mm_id10mm_od24mm_h8mm_groovew3mm_grooved2mm_shape(symmetricring) +cablegrommet_panelhole20mm_id10mm_od24mm_h8mm_groovew3mm_grooved2mm_symmetricring ``` This custom nominal protection ring uses two equal circular flanges and a centered annular panel groove. It selects no supplier series or dimensional -standard. `shape(symmetricring)` is the sole shape and defaults when omitted. +standard. The `symmetricRing` boolean defaults to `true`; `symmetricring` is a +value-free flag. Valued flags, old `shape(...)` tokens and `shape` properties +are rejected. The six dimensions are required; no physical size is inferred. | Token | Property | Meaning | @@ -17,11 +19,12 @@ The six dimensions are required; no physical size is inferred. | `h` | `height` | Overall axial height | | `groovew` | `grooveWidth` | Axial groove width / nominal panel thickness | | `grooved` | `grooveDepth` | Radial groove depth, not a diametral reduction | +| `symmetricring` | `symmetricRing` | Value-free flag, defaults to `true` | The lowercase property spellings in the table also work as string tokens. All lengths accept numbers in millimeters or unit strings (`mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, `feet`) and normalize to millimeters. Tokens, aliases, and -selectors are case insensitive. Repeated properties and unknown tokens fail. +flags are case insensitive. Repeated properties and unknown tokens fail. The ring axis is Z. Its origin is the bore center in the panel midplane. The body spans `-height/2 .. +height/2`; the groove spans diff --git a/docs/clamping-shaft-collar.md b/docs/clamping-shaft-collar.md index 427bb90..ff2e85e 100644 --- a/docs/clamping-shaft-collar.md +++ b/docs/clamping-shaft-collar.md @@ -5,7 +5,7 @@ Roadmap [#13, proposal 0018](https://github.com/tscircuit/modelprinter/issues/13 ```ts import { mp, getClampingShaftCollarDimensions } from "@tscircuit/modelprinter" -const model = mp.string("clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4").json() +const model = mp.string("clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4").json() if (model.fn !== "clampingshaftcollar") throw new Error("Unexpected family") const { fn, ...props } = model const dimensions = getClampingShaftCollarDimensions(props) @@ -13,9 +13,9 @@ const dimensions = getClampingShaftCollarDimensions(props) The schema is `clampingShaftCollarModelPropsSchema`; the definition schema is `clampingShaftCollarModelDefinitionSchema`. Input/output/definition TypeScript types are exported, and the family participates in `modelDefinitionSchema` and `modelprinter.getModelNames()`. The dimension helper accepts properties without `fn`, validates them, and supplies exact cutter locations and depths for a downstream renderer. Modelprinter owns this contract; geometry is generated elsewhere. -All lengths normalize to millimeters. Numbers mean millimeters; strings accept a complete decimal with an optional `mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, or `feet` suffix. Angles are degrees. Model strings are case insensitive; selectors require parentheses. Unknown fields/tokens, duplicate tokens (including aliases), missing required values, unrecognized units, trailing text, nonfinite values, and incompatible dimensions fail validation. Counts are unitless integers. +All lengths normalize to millimeters. Numbers mean millimeters; strings accept a complete decimal with an optional `mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, or `feet` suffix. Angles are degrees. Model strings are case insensitive; flags take no values. Only the standard `_threadclass(6H)` designation uses parentheses. Unknown fields/tokens, duplicate tokens (including aliases), missing required values, unrecognized units, trailing text, nonfinite values, and incompatible dimensions fail validation. Counts are unitless integers. -The `_mN` token controls the female clamping/set-screw thread, not the axial shaft bore. It selects nominal ISO metric diameter, coarse pitch, and right hand. Supported nominal sizes/coarse pitches (mm): M2/0.4, M2.5/0.45, M3/0.5, M4/0.7, M5/0.8, M6/1, M8/1.25, M10/1.5, M12/1.75. `_threadpitch0.5mm` and `_threadhand(left)` override pitch and hand; pitch must be positive and less than diameter. `_threadclass(6H)` is the sole supported internal class and defaults to 6H. The contract defines nominal threaded cylindrical holes; tolerances and helical thread root/crest relief are not encoded. No screw is included. +The `_mN` token controls the female clamping/set-screw thread, not the axial shaft bore. It selects nominal ISO metric diameter, coarse pitch, and right hand. Supported nominal sizes/coarse pitches (mm): M2/0.4, M2.5/0.45, M3/0.5, M4/0.7, M5/0.8, M6/1, M8/1.25, M10/1.5, M12/1.75. `_threadpitch0.5mm` and `_lefthanded` override pitch and hand; pitch must be positive and less than diameter. `_threadclass(6H)` is the sole supported internal class and defaults to 6H. The contract defines nominal threaded cylindrical holes; tolerances and helical thread root/crest relief are not encoded. No screw is included. | Token | JSON property | Default/meaning | | --- | --- | --- | @@ -23,12 +23,12 @@ The `_mN` token controls the female clamping/set-screw thread, not the axial sha | `od` | `outerDiameter` | Required, cylindrical exterior | | `m` | `metricSize` | Required, female mounting thread | | `threadpitch` | `threadPitch` | Coarse pitch for `metricSize` | -| `threadhand(...)` | `threadHand` | `right`; also accepts `left` | +| `lefthanded`, `righthanded` | `leftHanded` | `false` (right handed); `_lefthanded` selects `true` | | `threadclass(...)` | `threadClass` | Fixed `6H` | | `chamfer` | `chamfer` | 0; equal 45-degree radial/axial setbacks at both outer end edges and both bore entries | | `w`, `width` | `width` | Required axial width | | `split` | `splitWidth` | Required slit width | -| `mount(...)` | `mount` | Fixed `singleclamp` | +| `singleclamp` | `singleClamp` | `true`; value-free fixed construction flag | | `screwz` | `screwZ` | `width / 2`, from the lower face | | `clampx` | `clampX` | `(outerDiameter+boreDiameter)/4`, halfway across the radial wall on +X | | `clearance` | `clearanceHoleDiameter` | Thread diameter + 0.5 mm | @@ -43,3 +43,5 @@ The clamp screw axis is +Y at X=`clampX`, Z=`screwZ`. It enters from -Y through The extents deliberately cover the curved outer surface across each cylinder's full diameter. Hole depth is a fixed through-arm construction, not a blind depth selected from the thread size. The thread hole carries pitch, hand, female gender, and class. Validation requires OD > bore, clearance diameter > thread diameter, and `bore/2 < clampX-clearance/2` with `clampX+clearance/2 < OD/2`, so neither cylindrical hole intersects the shaft bore or the radial exterior edges. The slit width must be less than the bore diameter and must leave a positive arm thickness at the outermost clearance-cylinder edge. The clearance hole must fit strictly between both end faces and their chamfers. A chamfer must be smaller than half the radial wall and half the width; derived dimensions must be finite. + +The resolved JSON and hole dimensions contain `leftHanded: false` by default. `_lefthanded` selects `true` and `_righthanded` selects `false`. Repeated or conflicting hand flags are rejected. The old `mount(...)` and `threadhand(...)` selector tokens and `mount`/`threadHand` JSON properties are rejected. diff --git a/docs/compression-spring.md b/docs/compression-spring.md index 9f62ef8..e241ba7 100644 --- a/docs/compression-spring.md +++ b/docs/compression-spring.md @@ -1,6 +1,6 @@ # Compression spring parameter contract -`compressionspring_spec(custom)_od8mm_wire1mm_l20mm_turns8_active6_ends(closedground)_hand(right)_state(free)` +`compressionspring_custom_od8mm_wire1mm_l20mm_turns8_active6_closedground_righthanded_free` selects custom construction version 1. This family defines an exact nominal spring for assembly visualization; it does not claim a standard profile, material stiffness, force, fatigue life or manufacturing tolerances. @@ -8,11 +8,9 @@ material stiffness, force, fatigue life or manufacturing tolerances. Required tokens are `od` (outside diameter), `wire` (round-wire diameter), `l`/`length`/`freelength` (free bearing-face separation), and `turns` (total complete revolutions). `active` is optional and defaults to totalTurns-2. -`spec(custom)`, `ends(closedground)`, `hand(right)` and `state(free)` are defaults. -`hand(left)` mirrors winding. Only this end construction and free state are +`custom`, `closedground`, and `free` are optional value-free flags whose JSON properties `custom`, `closedGround`, and `free` default to `true`. `leftHanded` defaults to `false`; `_righthanded` selects that default explicitly and `_lefthanded` selects `true`, mirroring the winding. Only this end construction and free state are supported; open ends, unground ends, variable-pitch coils and loaded/solid -poses require separate contracts. No `threadhand` token is used: winding is -independent of thread conventions. +poses require separate contracts. The old `spec(...)`, `ends(...)`, `hand(...)`, and `state(...)` selectors and their JSON properties are rejected. Repeated or conflicting hand flags are rejected. All lengths accept positive finite millimeter numbers or complete numeric strings with `mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, or `feet`, normalized to diff --git a/docs/flanged-bushings.md b/docs/flanged-bushings.md index 850594f..58c49ea 100644 --- a/docs/flanged-bushings.md +++ b/docs/flanged-bushings.md @@ -9,7 +9,7 @@ tolerances. Edges are square, with no implicit chamfers or shoulder fillets. ```ts import { flangedBushingModelPropsSchema, getFlangedBushingDimensions, mp } from "@tscircuit/modelprinter" -mp.string("flangedbushing_id8mm_od12mm_flangeod18mm_l15mm_flangethickness2mm_style(plainclosed)").json() +mp.string("flangedbushing_id8mm_od12mm_flangeod18mm_l15mm_flangethickness2mm_plainclosed").json() flangedBushingModelPropsSchema.parse({ innerDiameter: 8, outerDiameter: 12, flangeDiameter: 18, length: 15, flangeThickness: "2mm" }) getFlangedBushingDimensions({ innerDiameter: 8, outerDiameter: 12, flangeDiameter: 18, length: 15, flangeThickness: 2 }) ``` @@ -21,12 +21,14 @@ getFlangedBushingDimensions({ innerDiameter: 8, outerDiameter: 12, flangeDiamete | `flangeDiameter` | Required | `flangeod`, `flangediameter` | Flange outside diameter, greater than the sleeve | | `length` | Required | `l`, `length` | Overall length **including the flange** | | `flangeThickness` | Required | `flangethickness` | Flange axial thickness; less than overall length | -| `style` | `"plainclosed"` | `style(plainclosed)` | Continuous plain sleeve; other styles are rejected | +| `plainClosed` | `true` | `plainclosed` | Continuous plain sleeve | Lengths accept finite numbers in millimeters or complete numeric strings with optional `mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, or `feet` units. Outputs -use millimeters. Token names and style values are case-insensitive; direct props -use the casing shown above. Style tokens require parentheses. Unknown properties, +use millimeters. Token names and flags are case-insensitive; direct props +use the casing shown above. `plainclosed` is a value-free boolean flag. The +`plainClosed` property defaults to `true`; old `style(...)` tokens and `style` +properties are rejected. Unknown properties, malformed lengths, duplicate tokens/aliases, and empty tokens are rejected. The bore, sleeve and flange are concentric with the Z axis. The outer flange @@ -52,7 +54,7 @@ than overall length. These constraints retain a closed sleeve wall, a flange with radial overhang and a positive sleeve projection. The strict exported props/definition schemas share these checks. -`FlangedBushingModelPropsInput` accepts unit strings and optional default style; -`FlangedBushingModelProps` contains normalized lengths and the resolved style. +`FlangedBushingModelPropsInput` accepts unit strings and an optional `plainClosed` flag; +`FlangedBushingModelProps` contains normalized lengths and the resolved `plainClosed: true` flag. `FlangedBushingModelDefinition` adds `fn: "flangedbushing"` and is included in the public `modelDefinitionSchema` union. diff --git a/docs/flat-head-screw.md b/docs/flat-head-screw.md index 673d5a2..4557bc9 100644 --- a/docs/flat-head-screw.md +++ b/docs/flat-head-screw.md @@ -3,7 +3,7 @@ Implements roadmap #0002 from [issue #13](https://github.com/tscircuit/modelprinter/issues/13). ```ts -mp.string("flatheadscrew_m3_l10mm_drive(hexsocket)").json() +mp.string("flatheadscrew_m3_l10mm_hexsocket").json() ``` The model defaults to **ISO10642:2019**, with M3, M4, M5 and M6 supported. @@ -12,7 +12,7 @@ both forms normalize to `iso10642: true`. Legacy `standard(...)` selectors, other sizes/editions, drives, partial threading, thread classes other than 6g, and fine pitches are rejected. All normalized lengths are millimeters, angles are degrees, and the primary metric thread is male, coarse and right-hand. -`threadhand(left)` selects the same profile with opposite handedness. +`lefthanded` selects the same profile with opposite handedness. The [source table](https://www.westfieldfasteners.co.uk/Standards/ScrewBolt-SHCsk-M.html) is pinned by edition and copied into the exported `flatHeadScrewDimensions` table. @@ -36,8 +36,14 @@ d3 = d - 17H/12 root diameter. `showThreads: false` renders a smooth shank at the major diameter, retaining the same head, under-head blend, tip chamfer, length and thread identity. -`mN` / `metricsize(mN)` and `l` / `length` are aliases. `drive(...)`, `thread(full)`, `threadhand(...)`, `threadclass(6g)` and -`threadgender(male)` are selectors. `threads` / `nothreads` set visibility. +`mN` / `metricsize(mN)` and `l` / `length` are aliases. `hexsocket`, +`fullthread`, and `male` are value-free flags that directly set `hexSocket: true`, +`fullThread: true`, and `male: true`; those fixed features default to true. +`lefthanded` sets `leftHanded: true`, and `righthanded` restates its false default. +Repeated or conflicting hand flags are rejected. `threadclass(6g)` preserves the +ISO thread-class designation. `threads` / `nothreads` set visibility. +The former `drive`, `thread`, `threadHand`, and `threadGender` schema properties +and their selector tokens are rejected. Flags accept no values. Dimensional fields in the table can be repeated explicitly using their lowercase field names (for example `threadpitch0.5mm`), or supplied through the public schema. They must equal the pinned value; they cannot redefine the diff --git a/docs/flatgasket.md b/docs/flatgasket.md index a62031b..d0fd645 100644 --- a/docs/flatgasket.md +++ b/docs/flatgasket.md @@ -6,4 +6,4 @@ flatgasket_id20mm_od35mm_t2mm_flatannulus Flat annular seal centered on XY. The mating face is Z=0 and the opposite face is Z=thickness. Both bore and outside walls are straight, with no bevels. Custom dimensions specify nominal geometry without material or compression properties. -Lengths accept complete decimals with optional units and normalize to millimeters. The schema rejects unknown fields, duplicate aliases, malformed tokens and dimensions that eliminate required material. `getFlatGasketDimensions` exposes the resolved dimensions and installation datums. Legacy roadmap selectors map to value-free flags; there are no enum selectors in the resolved contract. +Lengths accept complete decimals with optional units and normalize to millimeters. The schema rejects unknown fields, duplicate aliases, malformed tokens and dimensions that eliminate required material. `getFlatGasketDimensions` exposes the resolved dimensions and installation datums. `_flatannulus` is a value-free boolean flag. Enum selectors and flags with values are rejected. diff --git a/docs/flex-screens.md b/docs/flex-screens.md index 10dcf6c..50dcd5b 100644 --- a/docs/flex-screens.md +++ b/docs/flex-screens.md @@ -5,7 +5,10 @@ Use a pin count after the model name and the usual `p` pitch token: ```ts mp.string("flexscreen30_w16_h10_flex5_p0.5mm_sitsflat").json() // { fn: "flexscreen", pinCount: 30, pitch: 0.5, -// width: 16, height: 10, flexCableLength: 5, orientation: "sitsFlat" } +// width: 16, height: 10, flexCableLength: 5, sitsFlat: true, +// sitsFlatBelowBoard: false, foldedToFaceAboveBoard: false, +// foldedToFaceBelowBoard: false, foldedToRightAngleAboveBoard: false, +// foldedToRightAngleBelowBoard: false } ``` The connector-facing properties match jscad-electronics' `FPC` component: @@ -42,3 +45,26 @@ Existing `conductorCount`, `conductorPitch`, `conductorWidth`, and `exposedContactLength` props and their long model-string tokens remain supported as compatibility aliases. The FPC-style props take precedence when both names are supplied. + +Placement uses boolean flags. Every parsed definition resolves all six flags, +with exactly one true. If placement is omitted, `sitsFlat` is true. + +| Bare model-string flag | Canonical property | +| --- | --- | +| `sitsflat` | `sitsFlat` | +| `sitsflatbelowboard` | `sitsFlatBelowBoard` | +| `foldedtofaceaboveboard` | `foldedToFaceAboveBoard` | +| `foldedtofacebelowboard` | `foldedToFaceBelowBoard` | +| `foldedtorightangleaboveboard` | `foldedToRightAngleAboveBoard` | +| `foldedtorightanglebelowboard` | `foldedToRightAngleBelowBoard` | + +The shorter bare flags `sitsflatbelow`, `foldsabove`, `foldsaboveboard`, +`foldsbelow`, `foldsbelowboard`, `rightangleabove`, `rightangleaboveboard`, +`rightanglebelow`, and `rightanglebelowboard` select the same corresponding +canonical flag. JSON aliases `foldsAboveBoard` and `foldsBelowBoard` normalize +to `foldedToFaceAboveBoard` and `foldedToFaceBelowBoard`. + +Supply exactly one true placement flag if any placement flags are supplied. +Repeating a flag or its alias in a model string is rejected. Flags accept no +values. The legacy `orientation` JSON field and `orientation(...)` selectors +are rejected. Dimensions, ratios, rotations, and color names remain values. diff --git a/docs/gears.md b/docs/gears.md index ca7240f..07ce4cf 100644 --- a/docs/gears.md +++ b/docs/gears.md @@ -104,11 +104,15 @@ getWormGearDimensions({ module: 1, pitchDiameter: 10, starts: 2 }) | `backlash` | 0 mm | `backlash` | Nonnegative axial tooth thinning at the pitch cylinder | | `clearance` | 0.25 mm | `clearance` | Nonnegative additional radial dedendum | | `boreDiameter` | 0 mm | `bore`, `borediameter` | Zero disables the through bore | -| `handedness` | `"right"` | `right`, `left` | Mutually exclusive flags without values | +| `leftHanded` | `false` | `right`, `left` | Boolean: `left` sets true and `right` sets false; duplicates and conflicts are rejected | | `phase` | 0° | `phase` | Rotation about +Z | | `radialSegments` | 96 | `segments` | Integer 24–256, divisible by four | | `segmentsPerTurn` | 32 | `turnsegments` | Integer 12–128; subdivisions per helical revolution | +Direct props use `leftHanded: true` for left hand and `false` for right hand. +The former `handedness` property and selector tokens such as `handedness(left)` +are rejected. + The worm axis is +Z and the body extends from Z=0 to Z=`length`. A right-hand thread advances from +X toward +Y as Z increases; a left-hand thread reverses that angular progression. Phase rotates all starts together about +Z. Threads @@ -161,7 +165,7 @@ getHelicalGearDimensions({ toothCount: 24, module: 1, helixAngle: 25 }) | Additional property | Default | Model-string modifier | Meaning | | --- | --- | --- | --- | | `helixAngle` | 20° | `ha`, `helixangle` | Finite unsigned angle to the axis at the pitch cylinder, 0 ≤ angle < 90° | -| `handedness` | `"right"` | `right`, `left` | Mutually exclusive flags, following the worm convention | +| `leftHanded` | `false` | `right`, `left` | Boolean, following the worm convention; duplicates and conflicts are rejected | | `segmentsPerTurn` | 32 | `turnsegments` | Integer 12–128; minimum subdivisions per full turn of twist | Inline tooth count uses `helicalgear24`; `teeth`, `segments`, and every spur @@ -201,13 +205,13 @@ transverse module and pressure angle equal to the worm's axial values, its helix angle equal to the worm's lead angle, and use the same hand: ```ts -const worm = { module: 1, pitchDiameter: 10, starts: 2, pressureAngle: 20, handedness: "right" as const } +const worm = { module: 1, pitchDiameter: 10, starts: 2, pressureAngle: 20, leftHanded: false } const wheel = helicalGearModelPropsSchema.parse({ toothCount: 32, module: worm.module, pressureAngle: worm.pressureAngle, helixAngle: getWormGearDimensions(worm).leadAngle, - handedness: worm.handedness, + leftHanded: worm.leftHanded, }) ``` diff --git a/docs/hex-bolt.md b/docs/hex-bolt.md index 5e92353..72f6be9 100644 --- a/docs/hex-bolt.md +++ b/docs/hex-bolt.md @@ -3,7 +3,7 @@ Implements roadmap #0001 from [issue #13](https://github.com/tscircuit/modelprinter/issues/13). ```ts -mp.string("hexbolt_m6_l25mm_thread(full)_drive(hex)").json() +mp.string("hexbolt_m6_l25mm_fullthread_hex").json() ``` The model pins `iso4017` to **ISO4017:2014**, with M3, M4, @@ -13,7 +13,7 @@ default; optional `_iso4017` restates it and normalizes to `iso4017: true`. The flag accepts no value, and schemas default it to true. Legacy `standard(...)` selectors and the `standard` property are unsupported. All normalized lengths are millimeters, angles are degrees, and the primary metric thread is male, coarse and right-hand. -`threadhand(left)` selects the same profile with opposite handedness. +`lefthanded` selects the same profile with opposite handedness. The [source table](https://cdn.standards.iteh.ai/samples/63206/dd63a69c4be3432aac1914bccda73b2c/ISO-4017-2014.pdf) is pinned by edition and copied into the exported `hexBoltDimensions` table. @@ -37,8 +37,14 @@ d3 = d - 17H/12 root diameter. `showThreads: false` renders a smooth shank at the major diameter, retaining the same head, under-head blend, tip chamfer, length and thread identity. -`mN` / `metricsize(mN)` and `l` / `length` are aliases. `drive(...)`, `thread(full)`, `threadhand(...)`, `threadclass(6g)` and -`threadgender(male)` are selectors. `threads` / `nothreads` set visibility. +`mN` / `metricsize(mN)` and `l` / `length` are aliases. `hex`, +`fullthread`, and `male` are value-free flags that directly set `hex: true`, +`fullThread: true`, and `male: true`; those fixed features default to true. +`lefthanded` sets `leftHanded: true`, and `righthanded` restates its false default. +Repeated or conflicting hand flags are rejected. `threadclass(6g)` preserves the +ISO thread-class designation. `threads` / `nothreads` set visibility. +The former `drive`, `thread`, `threadHand`, and `threadGender` schema properties +and their selector tokens are rejected. Flags accept no values. Dimensional fields in the table can be repeated explicitly using their lowercase field names (for example `threadpitch0.5mm`), or supplied through the public schema. They must equal the pinned value; they cannot redefine the diff --git a/docs/hex-nut.md b/docs/hex-nut.md index 1853c16..2c67c15 100644 --- a/docs/hex-nut.md +++ b/docs/hex-nut.md @@ -26,7 +26,7 @@ one untoleranced nominal visualization, not a manufactured tolerance model. Exactly one of `m` or `imperial(...)` is required. ISO, DIN and ASME family flags default by size as above. `threadpitch` can restate the coarse pitch, using a millimeter number or complete numeric unit string (`mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, `feet`); all lengths normalize to mm. A -contradictory/fine pitch is rejected. `threadhand(right)` and `threadclass(6H)` +contradictory/fine pitch is rejected. `righthanded` and `threadclass(6H)` are fixed defaults. Other hands/classes and washer-face variants require a separate contract. `showThreads` defaults true; `threads`/`nothreads` are value-free visibility flags. Strings are case insensitive; the normalized diff --git a/docs/model-string-flags.md b/docs/model-string-flags.md index a7f4c5c..ce1a108 100644 --- a/docs/model-string-flags.md +++ b/docs/model-string-flags.md @@ -1,67 +1,18 @@ -# Mechanical model string flags - -Prefer value-free flags for mechanical options. Fastener standard flags and -`threadedrod` use boolean JSON properties. Other aliases below preserve their -existing JSON schema and legacy raw `.params()` behavior. Those aliases validate -new flag strings during `.string()`; legacy strings retain deferred model -validation during `.json()`. - -`threadedrod` accepts `_lefthanded` and `_righthanded` and uses `leftHand: boolean`. -Its fixed properties need no JSON fields or flags, and enum selectors are -rejected. See [Threaded rod](threaded-rods.md) for its contract. - -| Legacy selector | Preferred syntax | Models | -| --- | --- | --- | -| `mount(setscrew)` | `_setscrew` | shaftcollar, rigidcoupler | -| `mount(singleclamp)` | `_singleclamp` | clampingshaftcollar | -| `threadhand(left)` | `_lefthanded` | shaftcollar, clampingshaftcollar, rigidcoupler, hexbolt, flatheadscrew, panscrew | -| `hand(left)` | `_lefthanded` | compressionspring | -| `ends(closedground)` | `_closedground` | compressionspring | -| `thread(full)` | `_fullthread` | hexbolt, flatheadscrew, panscrew, buttonscrew | -| `drive(hex)` | `_hex` | hexbolt | -| `drive(hexsocket)` | `_hexsocket` | flatheadscrew, buttonscrew | -| `drive(phillips)` | `_phillips` | panscrew | -| `profile(fourtsolid)` | `_fourtsolid` | tslotextrusion | -| `shape(righttriangle)` | `_righttriangle` | tslotgusset | -| `shape(symmetricring)` | `_symmetricring` | cablegrommet | - -Omit redundant default selectors: `spec(custom)`, `state(free)`, -`style(plainclosed)`, `ends(flat)`, right handedness and male thread gender. -The corresponding `_custom`, `_free`, `_plainclosed`, `_flatends`, -`_righthanded` and `_male` aliases are accepted only in applicable models and -removed when normalizing flag syntax. All existing default values remain intact. -Fastener standards use optional value-free flags, with the same geometry when -the flag is omitted: - -| Model | Optional standard flag | -| --- | --- | -| hexbolt | `_iso4017` | -| flatheadscrew | `_iso10642` | -| panscrew | `_iso7045` and the independent recess flag `_iso4757` | -| buttonscrew | `_iso7380-1` | -| hexnut | `_iso4032`, `_din934`, or `_asmeb18.2.2` | - -Hex nuts choose their existing size-based family by default: ISO for supported -ISO metric sizes, DIN for other supported metric sizes, and ASME for imperial -sizes. Explicit family flags remain strict about supported sizes. These models -reject `standard(...)`; normalized contracts contain boolean standard flags. - -Dimensions and identifiers such as `m6` and `threadclass(6H)` retain their -established syntax. Existing thread visibility -flags `_threads` and `_nothreads` continue to work. - -For example: - -```ts -mp.string("shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_lefthanded").json() -mp.string("compressionspring_od8mm_wire1mm_l20mm_turns8_closedground_lefthanded").json() -mp.string("plainbushing_id8mm_od12mm_l20mm").json() -mp.string("threadedrod_m6_l100mm_lefthanded").json() -``` - -Flags are case insensitive, model-local and value-free. Repeating a flag or -combining it with a selector for the same property is an error, even if they -agree. Unsupported options remain unsupported; these aliases do not add new -geometry variants. New models should use boolean JSON properties for options -from the outset. Apart from the standard flags above, aliases on released models -preserve their existing JSON API. +# Model string flags + +Geometry choices use value-free flags and boolean properties. For example, +`plainbushing_id8mm_od12mm_l20mm_plainclosed` resolves `plainClosed: true`, +`tslotextrusion_w20mm_h20mm_l100mm_fourtsolid_slot6mm_pocket10mm_pocketd2mm_lip2mm_bore4mm_corner1mm` +resolves `fourTSolid: true`, and `hexbolt_m4_l20mm_hex_fullthread_lefthanded` +resolves `hex: true`, `fullThread: true`, and `leftHanded: true`. + +Flags belong to each model's parser. A flag takes no value; duplicate flags, +conflicting alternatives, unsupported flags and old enum-style selectors are +rejected. Supported default geometry is selected when its flag is omitted. +Single supported features have true boolean defaults; handedness is expressed +as a boolean instead of a string. Schemas reject obsolete selector properties +and preserve validation for unsupported geometry. + +Size designations, thread classes, numeric dimensions, and coordinate tuples +retain their named values. Every parsed model remains suitable for a schema +roundtrip and for React and vanilla renderer routing. diff --git a/docs/nema-motors.md b/docs/nema-motors.md index 0a8c4ba..9ec0d0a 100644 --- a/docs/nema-motors.md +++ b/docs/nema-motors.md @@ -44,6 +44,12 @@ Modifiers: `l` / `length` / `bodylength`, `bodywidth`, `shaftlength`, `holespacing`, `holediameter`, `holedepth`, `throughholes` / `blindholes`, `pilotdiameter`, `pilotlength`, `frontcap`, `rearcap`, `facechamfer`, `bodychamfer`, `rearchamfer`, `stepped` / `chamfered`. Duplicate, unknown and geometrically invalid parameters fail validation. +Direct props use mutually exclusive `roundShaft` / `dShaft` and +`chamfered` / `stepped` booleans. Each resolved group contains one true flag +and false for its other flags. Explicit groups must select exactly one true flag. +`_roundshaft` and `_backfaceplain` also match their prop names; `_round` and +`_plainbackface` remain value-free flag spellings. Enum properties `shaftShape`, +`backFace` and `cornerProfile`, and value-bearing selector tokens, are rejected. This package defines parameters and parses model strings; it does not generate motor geometry. The corresponding components in jscad-electronics build the @@ -54,7 +60,9 @@ JSCAD solids and own the visual snapshots and geometry tests. 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"`. +These flags are mutually exclusive. Direct props use `backFaceHoles`, +`backFaceScrews` or `backFacePlain` booleans. Resolved definitions contain exactly +one true flag and false for the other rear-face flags. Rear fasteners are representative configurable details, not guaranteed by a NEMA frame number. The square rear pitches default to 16 / 31 / 47.14 / 40 mm, @@ -67,7 +75,7 @@ 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` +`stepped` / `chamfered` booleans 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 @@ -104,5 +112,9 @@ and `nema17_jstsh4` produce the same definition. Resolved definitions retain the existing `jst-family-N` spelling; existing PH definitions and dimensions are preserved. Header dimensions vary with family and pin count, and headers that cannot fit on the motor are rejected. -`none` / `stubs` remain aliases for `nowires` / `wirestubs`. Duplicate, +`none` / `stubs` remain value-free aliases for `nowires` / `wirestubs`. +Direct props use `noWires` or `wireStubs` booleans, with visible stubs by default. +`wireConnection` accepts only JST catalog identifiers; it cannot be combined with +a true `noWires` or `wireStubs` flag. Resolved wire booleans are both false for +JST terminations, and `wireConnection` is omitted for no-wire and stub choices. Duplicate, unsupported, and out-of-range connections still fail validation. diff --git a/docs/pan-screw.md b/docs/pan-screw.md index bebb948..54fe00d 100644 --- a/docs/pan-screw.md +++ b/docs/pan-screw.md @@ -3,7 +3,7 @@ Implements roadmap #0003 from [issue #13](https://github.com/tscircuit/modelprinter/issues/13). ```ts -mp.string("panscrew_m3_l10mm_drive(phillips)").json() +mp.string("panscrew_m3_l10mm_phillips").json() ``` The model pins `iso7045` to **ISO7045:2011**, with M3, M4, @@ -13,7 +13,7 @@ default; optional `_iso7045` restates it and normalizes to `iso7045: true`. The flag accepts no value, and schemas default it to true. Legacy `standard(...)` selectors and the `standard` property are unsupported. All normalized lengths are millimeters, angles are degrees, and the primary metric thread is male, coarse and right-hand. -`threadhand(left)` selects the same profile with opposite handedness. +`lefthanded` selects the same profile with opposite handedness. The [source table](https://cdn.standards.iteh.ai/samples/57372/08630d724c1544b195c2e6bb92cfb888/ISO-7045-2011.pdf) is pinned by edition and copied into the exported `panScrewDimensions` table. @@ -37,8 +37,14 @@ d3 = d - 17H/12 root diameter. `showThreads: false` renders a smooth shank at the major diameter, retaining the same head, under-head blend, tip chamfer, length and thread identity. -`mN` / `metricsize(mN)` and `l` / `length` are aliases. `drive(...)`, `thread(full)`, `threadhand(...)`, `threadclass(6g)` and -`threadgender(male)` are selectors. `threads` / `nothreads` set visibility. +`mN` / `metricsize(mN)` and `l` / `length` are aliases. `phillips`, +`fullthread`, and `male` are value-free flags that directly set `phillips: true`, +`fullThread: true`, and `male: true`; those fixed features default to true. +`lefthanded` sets `leftHanded: true`, and `righthanded` restates its false default. +Repeated or conflicting hand flags are rejected. `threadclass(6g)` preserves the +ISO thread-class designation. `threads` / `nothreads` set visibility. +The former `drive`, `thread`, `threadHand`, and `threadGender` schema properties +and their selector tokens are rejected. Flags accept no values. Dimensional fields in the table can be repeated explicitly using their lowercase field names (for example `threadpitch0.5mm`), or supplied through the public schema. They must equal the pinned value; they cannot redefine the diff --git a/docs/plain-bushings.md b/docs/plain-bushings.md index 30ae61f..8c5ad01 100644 --- a/docs/plain-bushings.md +++ b/docs/plain-bushings.md @@ -8,7 +8,7 @@ rolling elements, a slit, a flange, fits, or manufacturing tolerances. ```ts import { getPlainBushingDimensions, mp, plainBushingModelPropsSchema } from "@tscircuit/modelprinter" -mp.string("plainbushing_id8mm_od12mm_l20mm_style(plainclosed)_edgechamfer0.5mm").json() +mp.string("plainbushing_id8mm_od12mm_l20mm_plainclosed_edgechamfer0.5mm").json() plainBushingModelPropsSchema.parse({ innerDiameter: "8mm", outerDiameter: "12mm", length: "2cm" }) getPlainBushingDimensions({ innerDiameter: 8, outerDiameter: 12, length: 20, edgeChamfer: 0.5 }) ``` @@ -18,13 +18,15 @@ getPlainBushingDimensions({ innerDiameter: 8, outerDiameter: 12, length: 20, edg | `innerDiameter` | Required | `id`, `innerdiameter` | Positive through-bore diameter | | `outerDiameter` | Required | `od`, `outerdiameter` | Outside sleeve diameter, greater than the bore | | `length` | Required | `l`, `length` | Overall end-plane distance, including chamfers | -| `style` | `"plainclosed"` | `style(plainclosed)` | Continuous plain sleeve; other styles are rejected | +| `plainClosed` | `true` | `plainclosed` | Continuous plain sleeve | | `edgeChamfer` | 0 mm | `edgechamfer` | Equal axial/radial setbacks of all four 45-degree inner/outer rim chamfers | Lengths accept finite numbers in millimeters or complete numeric strings with optional `mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, or `feet` units. Outputs -use millimeters. Token names and style values are case-insensitive; direct props -use the casing shown above. Style tokens require parentheses. Unknown properties, +use millimeters. Token names and flags are case-insensitive; direct props +use the casing shown above. `plainclosed` is a value-free boolean flag. The +`plainClosed` property defaults to `true`; old `style(...)` tokens and `style` +properties are rejected. Unknown properties, duplicate tokens/aliases, empty tokens, and malformed lengths are rejected. The bore and sleeve are concentric with the Z axis. The first annular end face diff --git a/docs/rectangulargasket.md b/docs/rectangulargasket.md index 3853436..f41d662 100644 --- a/docs/rectangulargasket.md +++ b/docs/rectangulargasket.md @@ -6,4 +6,4 @@ rectangulargasket_w80mm_h50mm_border5mm_t2mm_cornerr5mm_flatframe Closed rounded rectangular flat seal centered on XY, with mating face Z=0 and top Z=thickness. Width and height are outside XY extents; border is the straight-side setback of the inner opening. The inner corner radius is max(0, outer corner radius minus border), giving concentric rounded corners when the border is thinner than the radius and square inner corners otherwise. -Lengths accept complete decimals with optional units and normalize to millimeters. The schema rejects unknown fields, duplicate aliases, malformed tokens and dimensions that eliminate required material. `getRectangularGasketDimensions` exposes the resolved dimensions and installation datums. Legacy roadmap selectors map to value-free flags; there are no enum selectors in the resolved contract. +Lengths accept complete decimals with optional units and normalize to millimeters. The schema rejects unknown fields, duplicate aliases, malformed tokens and dimensions that eliminate required material. `getRectangularGasketDimensions` exposes the resolved dimensions and installation datums. `_flatframe` is a value-free boolean flag. Enum selectors and flags with values are rejected. diff --git a/docs/rigid-coupler.md b/docs/rigid-coupler.md index 83aeda7..248d607 100644 --- a/docs/rigid-coupler.md +++ b/docs/rigid-coupler.md @@ -5,7 +5,7 @@ Roadmap [#13, proposal 0019](https://github.com/tscircuit/modelprinter/issues/13 ```ts import { mp, getRigidCouplerDimensions } from "@tscircuit/modelprinter" -const model = mp.string("rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4").json() +const model = mp.string("rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4").json() if (model.fn !== "rigidcoupler") throw new Error("Unexpected family") const { fn, ...props } = model const dimensions = getRigidCouplerDimensions(props) @@ -13,9 +13,9 @@ const dimensions = getRigidCouplerDimensions(props) The schema is `rigidCouplerModelPropsSchema`; the definition schema is `rigidCouplerModelDefinitionSchema`. Input/output/definition TypeScript types are exported, and the family participates in `modelDefinitionSchema` and `modelprinter.getModelNames()`. The dimension helper accepts properties without `fn`, validates them, and supplies exact cutter locations and depths for a downstream renderer. Modelprinter owns this contract; geometry is generated elsewhere. -All lengths normalize to millimeters. Numbers mean millimeters; strings accept a complete decimal with an optional `mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, or `feet` suffix. Angles are degrees. Model strings are case insensitive; selectors require parentheses. Unknown fields/tokens, duplicate tokens (including aliases), missing required values, unrecognized units, trailing text, nonfinite values, and incompatible dimensions fail validation. Counts are unitless integers. +All lengths normalize to millimeters. Numbers mean millimeters; strings accept a complete decimal with an optional `mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, or `feet` suffix. Angles are degrees. Model strings are case insensitive; flags take no values. Only the standard `_threadclass(6H)` designation uses parentheses. Unknown fields/tokens, duplicate tokens (including aliases), missing required values, unrecognized units, trailing text, nonfinite values, and incompatible dimensions fail validation. Counts are unitless integers. -The `_mN` token controls the female clamping/set-screw thread, not the axial shaft bore. It selects nominal ISO metric diameter, coarse pitch, and right hand. Supported nominal sizes/coarse pitches (mm): M2/0.4, M2.5/0.45, M3/0.5, M4/0.7, M5/0.8, M6/1, M8/1.25, M10/1.5, M12/1.75. `_threadpitch0.5mm` and `_threadhand(left)` override pitch and hand; pitch must be positive and less than diameter. `_threadclass(6H)` is the sole supported internal class and defaults to 6H. The contract defines nominal threaded cylindrical holes; tolerances and helical thread root/crest relief are not encoded. No screw is included. +The `_mN` token controls the female clamping/set-screw thread, not the axial shaft bore. It selects nominal ISO metric diameter, coarse pitch, and right hand. Supported nominal sizes/coarse pitches (mm): M2/0.4, M2.5/0.45, M3/0.5, M4/0.7, M5/0.8, M6/1, M8/1.25, M10/1.5, M12/1.75. `_threadpitch0.5mm` and `_lefthanded` override pitch and hand; pitch must be positive and less than diameter. `_threadclass(6H)` is the sole supported internal class and defaults to 6H. The contract defines nominal threaded cylindrical holes; tolerances and helical thread root/crest relief are not encoded. No screw is included. | Token | JSON property | Default/meaning | | --- | --- | --- | @@ -23,12 +23,12 @@ The `_mN` token controls the female clamping/set-screw thread, not the axial sha | `od` | `outerDiameter` | Required, cylindrical exterior | | `m` | `metricSize` | Required, female mounting thread | | `threadpitch` | `threadPitch` | Coarse pitch for `metricSize` | -| `threadhand(...)` | `threadHand` | `right`; also accepts `left` | +| `lefthanded`, `righthanded` | `leftHanded` | `false` (right handed); `_lefthanded` selects `true` | | `threadclass(...)` | `threadClass` | Fixed `6H` | | `chamfer` | `chamfer` | 0; equal 45-degree radial/axial setbacks at both outer end edges and both bore entries | | `boreb` | `boreBDiameter` | Same as `boreDiameter`; independent shaft-B bore | | `l`, `length` | `length` | Required overall axial length | -| `mount(...)` | `mount` | Fixed `setscrew` | +| `setscrew` | `setScrew` | `true`; value-free fixed construction flag | | `screwcount` | `screwCount` | Fixed 4 (two per shaft end) | | `screwendoffset` | `screwEndOffset` | `length / 4`, measured inward from each end | | `screwangle` | `screwAngle` | 0; first hole counterclockwise from +X about +Z, normalized to [0,360) | @@ -40,3 +40,5 @@ There are exactly two radial set-screw holes per end. End A's plane is Z=`screwE `getRigidCouplerDimensions` returns both bore depths (`length/2`) and `screwHoles`, ordered A-first, A-second, B-first, B-second. For each hole, R=`outerDiameter/2`, r=its shaft bore radius, d=thread diameter, and a=its angle. The cutter starts at `[R*cos(a),R*sin(a),z]`, points inward along `[-cos(a),-sin(a),0]`, has diameter d, and depth `R-sqrt(r*r-(d/2)^2)`. This fully opens into the corresponding shaft bore; the complete material-bearing hole is threaded. Each returned hole carries pitch, hand, female gender, and class. Validation requires both bores smaller than OD and strictly larger than `sqrt(2)*threadDiameter`. This retains material between the two perpendicular screw holes outside each shaft bore; smaller shaft bores would let those holes intersect in the sleeve wall. Holes must clear end chamfers (`screwEndOffset-d/2 > chamfer`) and the two end planes must be separated by more than d (`length-2*screwEndOffset > d`). These conditions keep each hole fully inside its own half and prevent axial overlap. Chamfers must be smaller than half the thinnest radial wall and half the length. Every derived dimension must remain finite. + +The resolved JSON and hole dimensions contain `leftHanded: false` by default. `_lefthanded` selects `true` and `_righthanded` selects `false`. Repeated or conflicting hand flags are rejected. The old `mount(...)` and `threadhand(...)` selector tokens and `mount`/`threadHand` JSON properties are rejected. diff --git a/docs/shaft-collar.md b/docs/shaft-collar.md index b6fc863..7446148 100644 --- a/docs/shaft-collar.md +++ b/docs/shaft-collar.md @@ -5,7 +5,7 @@ Roadmap [#13, proposal 0017](https://github.com/tscircuit/modelprinter/issues/13 ```ts import { mp, getShaftCollarDimensions } from "@tscircuit/modelprinter" -const model = mp.string("shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4").json() +const model = mp.string("shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4").json() if (model.fn !== "shaftcollar") throw new Error("Unexpected family") const { fn, ...props } = model const dimensions = getShaftCollarDimensions(props) @@ -13,9 +13,9 @@ const dimensions = getShaftCollarDimensions(props) The schema is `shaftCollarModelPropsSchema`; the definition schema is `shaftCollarModelDefinitionSchema`. Input/output/definition TypeScript types are exported, and the family participates in `modelDefinitionSchema` and `modelprinter.getModelNames()`. The dimension helper accepts properties without `fn`, validates them, and supplies exact cutter locations and depths for a downstream renderer. Modelprinter owns this contract; geometry is generated elsewhere. -All lengths normalize to millimeters. Numbers mean millimeters; strings accept a complete decimal with an optional `mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, or `feet` suffix. Angles are degrees. Model strings are case insensitive; selectors require parentheses. Unknown fields/tokens, duplicate tokens (including aliases), missing required values, unrecognized units, trailing text, nonfinite values, and incompatible dimensions fail validation. Counts are unitless integers. +All lengths normalize to millimeters. Numbers mean millimeters; strings accept a complete decimal with an optional `mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, or `feet` suffix. Angles are degrees. Model strings are case insensitive; flags take no values. Only the standard `_threadclass(6H)` designation uses parentheses. Unknown fields/tokens, duplicate tokens (including aliases), missing required values, unrecognized units, trailing text, nonfinite values, and incompatible dimensions fail validation. Counts are unitless integers. -The `_mN` token controls the female clamping/set-screw thread, not the axial shaft bore. It selects nominal ISO metric diameter, coarse pitch, and right hand. Supported nominal sizes/coarse pitches (mm): M2/0.4, M2.5/0.45, M3/0.5, M4/0.7, M5/0.8, M6/1, M8/1.25, M10/1.5, M12/1.75. `_threadpitch0.5mm` and `_threadhand(left)` override pitch and hand; pitch must be positive and less than diameter. `_threadclass(6H)` is the sole supported internal class and defaults to 6H. The contract defines nominal threaded cylindrical holes; tolerances and helical thread root/crest relief are not encoded. No screw is included. +The `_mN` token controls the female clamping/set-screw thread, not the axial shaft bore. It selects nominal ISO metric diameter, coarse pitch, and right hand. Supported nominal sizes/coarse pitches (mm): M2/0.4, M2.5/0.45, M3/0.5, M4/0.7, M5/0.8, M6/1, M8/1.25, M10/1.5, M12/1.75. `_threadpitch0.5mm` and `_lefthanded` override pitch and hand; pitch must be positive and less than diameter. `_threadclass(6H)` is the sole supported internal class and defaults to 6H. The contract defines nominal threaded cylindrical holes; tolerances and helical thread root/crest relief are not encoded. No screw is included. | Token | JSON property | Default/meaning | | --- | --- | --- | @@ -23,11 +23,11 @@ The `_mN` token controls the female clamping/set-screw thread, not the axial sha | `od` | `outerDiameter` | Required, cylindrical exterior | | `m` | `metricSize` | Required, female mounting thread | | `threadpitch` | `threadPitch` | Coarse pitch for `metricSize` | -| `threadhand(...)` | `threadHand` | `right`; also accepts `left` | +| `lefthanded`, `righthanded` | `leftHanded` | `false` (right handed); `_lefthanded` selects `true` | | `threadclass(...)` | `threadClass` | Fixed `6H` | | `chamfer` | `chamfer` | 0; equal 45-degree radial/axial setbacks at both outer end edges and both bore entries | | `w`, `width` | `width` | Required axial width | -| `mount(...)` | `mount` | Fixed `setscrew` | +| `setscrew` | `setScrew` | `true`; value-free fixed construction flag | | `screwz` | `screwZ` | `width / 2`, from the lower face | | `screwangle` | `screwAngle` | 0; counterclockwise from +X about +Z, normalized to [0, 360) | @@ -36,3 +36,5 @@ The shaft axis is +Z. The mounting datum is the center of the flat lower face at There is exactly one radial hole, aimed inward from the exterior toward the shaft axis. Let R=`outerDiameter/2`, r=`boreDiameter/2`, d=thread diameter, and a=`screwAngle`. `getShaftCollarDimensions` returns `wallThickness=R-r` and `screwHole` with start `[R*cos(a), R*sin(a), screwZ]`, unit direction `[-cos(a), -sin(a), 0]`, diameter d, and depth `R-sqrt(r*r-(d/2)^2)`. This depth breaks into the axial bore across the full cutter diameter, rather than stopping at its nearest tangent. The returned hole also carries pitch, hand, female gender, and class. The complete material-bearing radial bore is threaded. Validation requires OD > bore > thread diameter; the hole must fit strictly between both end faces and their chamfers (`chamfer < screwZ-d/2` and `screwZ+d/2 < width-chamfer`). A chamfer must be smaller than half the radial wall and half the width. Every derived dimension must remain finite. + +The resolved JSON and hole dimensions contain `leftHanded: false` by default. `_lefthanded` selects `true` and `_righthanded` selects `false`. Repeated or conflicting hand flags are rejected. The old `mount(...)` and `threadhand(...)` selector tokens and `mount`/`threadHand` JSON properties are rejected. diff --git a/docs/sheet-metal.md b/docs/sheet-metal.md index 8245e78..653c237 100644 --- a/docs/sheet-metal.md +++ b/docs/sheet-metal.md @@ -8,9 +8,10 @@ sheetmetal_angle_w24mm_l20mm_h16mm_t1.2mm_r2mm_hole1(r3mm_bottomface_leftofcente `w` is sheet width, `l` is straight base length, `h` is straight flange height, `t` is thickness, and the outer `r` is inside bend radius. Straight lengths -exclude the curved bends. Profiles are `plate`, `angle` (one 90-degree upright), -and `channel` (two 90-degree uprights). This is formed geometry, not a flat -pattern or bend-allowance calculation. +exclude the curved bends. The value-free flags are `plate`, `angle` (one +90-degree upright), and `channel` (two 90-degree uprights). Omitting them selects +`plate`; repeated flags or multiple form choices are rejected. This is formed +geometry, not a flat pattern or bend-allowance calculation. ## Features @@ -50,6 +51,10 @@ and malformed parentheses are rejected when parsing. Geometry generation in jsca rejects cutouts touching/crossing an edge or bend tangent and overlapping cutout bounding boxes (a conservative restriction). -The parser compiles this language to the parameter schema's `panel`, `u`, and `v` -coordinates. Application examples and visual snapshots should use complete model -strings rather than duplicating those internal coordinates. +The parser compiles this language to boolean `round`/`slot` flags and the +parameter schema's `panel`, `u`, and `v` coordinates. JSON inputs use the boolean +`plate`/`angle`/`channel` flags and exactly one true `round`/`slot` flag per cutout. +The former string selectors `profile` and `shape` are rejected. Panel labels +`base`/`left`/`right` and slot axes `u`/`v` remain coordinate identifiers. +Application examples and visual snapshots should use complete model strings +rather than duplicating those internal coordinates. diff --git a/docs/tslot-extrusion.md b/docs/tslot-extrusion.md index f356797..32fde33 100644 --- a/docs/tslot-extrusion.md +++ b/docs/tslot-extrusion.md @@ -1,7 +1,7 @@ # T-slot extrusion ```text -tslotextrusion_w20mm_h20mm_l100mm_profile(fourtsolid)_slot6mm_pocket10mm_pocketd2mm_lip2mm_bore4mm_corner1mm +tslotextrusion_w20mm_h20mm_l100mm_fourtsolid_slot6mm_pocket10mm_pocketd2mm_lip2mm_bore4mm_corner1mm ``` `fourtsolid` is an explicitly dimensioned custom solid rectangular extrusion, @@ -14,7 +14,7 @@ The square roadmap example is one instance; unequal width and height are allowed | `w`, `width` | `width` | 20 | | `h`, `height` | `height` | 20 | | `l`, `length` | `length` | 100 | -| `profile(...)` | `profile` | `fourtsolid` | +| `fourtsolid` | `fourTSolid` | `true` | | `slot`, `slotwidth` | `slotWidth` | 6 | | `pocket`, `pocketwidth` | `pocketWidth` | 10 | | `pocketd`, `pocketdepth` | `pocketDepth` | 2 | @@ -38,8 +38,9 @@ implied. Every omitted fitting feature is fixed by this contract. Lengths accept finite numbers in millimeters or complete strings with supported units (`mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, `feet`), normalized to mm. -Parameters cannot repeat, including aliases. Unknown tokens and profile names -are errors. Direct props and definitions are strict schemas with the same rules. +Parameters cannot repeat, including aliases. Unknown tokens, repeated flags and valued flags +are errors. `fourTSolid` defaults to `true`; old `profile(...)` tokens and +`profile` properties are rejected. Direct props and definitions are strict schemas with the same rules. The pocket must be wider than the opening; adjacent pockets, the bore, and the outside corner regions must retain positive separating material. The corner region check conservatively reserves `cornerRadius` beside each pocket. diff --git a/docs/tslot-gusset.md b/docs/tslot-gusset.md index 6584309..c58d946 100644 --- a/docs/tslot-gusset.md +++ b/docs/tslot-gusset.md @@ -1,20 +1,21 @@ # T-slot gusset ```text -tslotgusset_w40mm_h40mm_t4mm_shape(righttriangle)_slots2_slot(5mm,12mm)_centers(12mm,28mm) +tslotgusset_w40mm_h40mm_t4mm_righttriangle_slots2_slot(5mm,12mm)_centers(12mm,28mm) ``` This custom right triangular flat corner plate has one capsule-shaped fixing slot parallel to each perpendicular edge. No supplier series is implied. -The shape and two-slot layout are fixed identities; other shapes or slot counts -are rejected. Unequal triangle width and height are supported. +The `rightTriangle` boolean defaults to `true`, and the two-slot layout is +fixed. `righttriangle` is a value-free flag. Valued flags, old `shape(...)` +tokens and `shape` properties are rejected. Unequal triangle width and height are supported. | Token | JSON property | Default | | --- | --- | --- | | `w`, `width` | `width` | 40 mm | | `h`, `height` | `height` | 40 mm | | `t`, `thickness` | `thickness` | 4 mm | -| `shape(...)` | `shape` | `righttriangle` | +| `righttriangle` | `rightTriangle` | `true` | | `slots`, `slotcount` | `slotCount` | 2 | | `slot(width,length)` | `slot` | [5 mm,12 mm] | | `centers(xDistance,yDistance)` | `centers` | [12 mm,28 mm] | diff --git a/src/helical-gear-schema.ts b/src/helical-gear-schema.ts index 72592fc..ce21619 100644 --- a/src/helical-gear-schema.ts +++ b/src/helical-gear-schema.ts @@ -9,14 +9,14 @@ const shape = { /** Unsigned pitch-cylinder helix angle to +Z, in degrees. */ helixAngle: z.number().finite().min(0).lt(90).default(20), /** Right-hand teeth advance counterclockwise as Z increases. */ - handedness: z.enum(["right", "left"]).default("right"), + leftHanded: z.boolean().default(false), segmentsPerTurn: z.number().int().min(12).max(128).default(32), } type ResolvedHelicalGearProps = z.output> function dimensions(props: ResolvedHelicalGearProps) { - const { helixAngle, handedness, segmentsPerTurn, ...spur } = props + const { helixAngle, leftHanded, segmentsPerTurn, ...spur } = props const d = getSpurGearDimensions(spur) const beta = (helixAngle * Math.PI) / 180 return { @@ -33,7 +33,7 @@ function dimensions(props: ResolvedHelicalGearProps) { twistAngle: helixAngle === 0 ? 0 - : ((handedness === "right" ? 1 : -1) * + : ((leftHanded ? -1 : 1) * (((2 * props.faceWidth) / d.pitchDiameter) * Math.tan(beta) * 180)) / @@ -45,7 +45,7 @@ function validate( props: ResolvedHelicalGearProps & { fn?: string }, context: z.RefinementCtx, ) { - const { fn, helixAngle, handedness, segmentsPerTurn, ...spur } = props + const { fn, helixAngle, leftHanded, segmentsPerTurn, ...spur } = props const result = spurGearModelPropsSchema.safeParse(spur) if (!result.success) { for (const issue of result.error.issues) context.addIssue(issue) @@ -53,7 +53,7 @@ function validate( } if ( Object.values( - dimensions({ ...result.data, helixAngle, handedness, segmentsPerTurn }), + dimensions({ ...result.data, helixAngle, leftHanded, segmentsPerTurn }), ).some((value) => !Number.isFinite(value)) ) context.addIssue({ diff --git a/src/models/balltransferunit/parse-model-string.ts b/src/models/balltransferunit/parse-model-string.ts index 3945df8..aa0576a 100644 --- a/src/models/balltransferunit/parse-model-string.ts +++ b/src/models/balltransferunit/parse-model-string.ts @@ -32,7 +32,7 @@ export function parseBallTransferUnitModelParams(raw: RawModelprinterParams) { const lower = token.toLowerCase() let property: string let value: unknown - if (lower === "face3hole" || lower === "mount(face3hole)") { + if (lower === "face3hole") { property = "faceThreeHole" value = true } else { diff --git a/src/models/buttonscrew/parse-model-string.ts b/src/models/buttonscrew/parse-model-string.ts index ab8411f..8b13f50 100644 --- a/src/models/buttonscrew/parse-model-string.ts +++ b/src/models/buttonscrew/parse-model-string.ts @@ -1,13 +1,9 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { buttonScrewModelDefinitionSchema } from "./schema" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" export function parseButtonScrewModelParams(raw: RawModelprinterParams) { - const tokens = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const tokens = splitModelStringTokens(raw.string) if (raw.fn !== "buttonscrew" || tokens[0]?.toLowerCase() !== "buttonscrew") throw new Error("Expected buttonscrew without an inline argument") const props: Record = { fn: "buttonscrew" } @@ -17,11 +13,14 @@ export function parseButtonScrewModelParams(raw: RawModelprinterParams) { threadpitch: "threadPitch", } as const const selectors = { - drive: "drive", - thread: "thread", - threadhand: "threadHand", threadclass: "threadClass", } as const + const booleanFlags: Record = { + hexsocket: { property: "hexSocket", value: true }, + fullthread: { property: "fullThread", value: true }, + righthanded: { property: "leftHanded", value: false }, + lefthanded: { property: "leftHanded", value: true }, + } for (const token of tokens.slice(1)) { if (token.toLowerCase() === "iso7380-1") { if ("iso73801" in props) @@ -35,11 +34,16 @@ export function parseButtonScrewModelParams(raw: RawModelprinterParams) { const value = match[2]! let property: string let parsed: unknown = value - if (name === "m") { + if (Object.hasOwn(booleanFlags, name)) { + if (value) throw new Error(`Flag "${name}" does not accept a value`) + property = booleanFlags[name]!.property + parsed = booleanFlags[name]!.value + } else if (name === "m") { property = "metricSize" parsed = `M${value}` - } else if (name in lengths) property = lengths[name as keyof typeof lengths] - else if (name in selectors) { + } else if (Object.hasOwn(lengths, name)) + property = lengths[name as keyof typeof lengths] + else if (Object.hasOwn(selectors, name)) { property = selectors[name as keyof typeof selectors] if (!/^\([^()]+\)$/.test(value)) throw new Error(`Expected a parenthesized ${name}`) diff --git a/src/models/buttonscrew/register.ts b/src/models/buttonscrew/register.ts index 8bfd4c5..4fc7ed7 100644 --- a/src/models/buttonscrew/register.ts +++ b/src/models/buttonscrew/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parseButtonScrewModelParams } from "./parse-model-string" import { buttonScrewModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "buttonscrew", schema: buttonScrewModelDefinitionSchema, parse: parseButtonScrewModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parseButtonScrewModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/buttonscrew/schema.ts b/src/models/buttonscrew/schema.ts index ad16ed0..17198e4 100644 --- a/src/models/buttonscrew/schema.ts +++ b/src/models/buttonscrew/schema.ts @@ -74,10 +74,10 @@ const shape = { metricSize: buttonScrewMetricSizeSchema, /** Under-head length; this family supports short fully threaded screws only. */ length, - drive: z.literal("hexsocket").default("hexsocket"), - thread: z.literal("full").default("full"), + hexSocket: z.literal(true).default(true), + fullThread: z.literal(true).default(true), threadPitch: length.optional(), - threadHand: z.literal("right").default("right"), + leftHanded: z.literal(false).default(false), threadClass: z.literal("6g").default("6g"), showThreads: z.boolean().default(true), } diff --git a/src/models/buttonscrew/string-flags.ts b/src/models/buttonscrew/string-flags.ts deleted file mode 100644 index 1dee033..0000000 --- a/src/models/buttonscrew/string-flags.ts +++ /dev/null @@ -1,8 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - hexsocket: "drive(hexsocket)", - fullthread: "thread(full)", - righthanded: "threadhand(right)", -} as const - -export const omittedStringFlags = ["righthanded"] as const diff --git a/src/models/cablegrommet/parse-model-string.ts b/src/models/cablegrommet/parse-model-string.ts index 5065c11..810cb76 100644 --- a/src/models/cablegrommet/parse-model-string.ts +++ b/src/models/cablegrommet/parse-model-string.ts @@ -1,5 +1,3 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { cableGrommetModelDefinitionSchema } from "./schema" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" @@ -20,9 +18,7 @@ const lengths = { } as const export function parseCableGrommetModelParams(raw: RawModelprinterParams) { - const tokens = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const tokens = splitModelStringTokens(raw.string) if (raw.fn !== "cablegrommet" || !/^cablegrommet$/i.test(tokens[0]!)) throw new Error("Expected cablegrommet without an inline value") const props: Record = { fn: "cablegrommet" } @@ -30,15 +26,14 @@ export function parseCableGrommetModelParams(raw: RawModelprinterParams) { const match = token.match(/^([a-z]+)(.*)$/i) const name = match?.[1]?.toLowerCase() const value = match?.[2] - if (!name || !value) throw new Error(`Invalid grommet token "${token}"`) + if (!name) throw new Error(`Invalid grommet token "${token}"`) let property: string let parsed: unknown = value if (name in lengths) property = lengths[name as keyof typeof lengths] - else if (name === "shape") { - property = "shape" - const selector = value.match(/^\(([a-z]+)\)$/i) - if (!selector) throw new Error("Grommet shape requires parentheses") - parsed = selector[1]!.toLowerCase() + else if (name === "symmetricring") { + property = "symmetricRing" + if (value !== "") throw new Error("symmetricring is a value-free flag") + parsed = true } else throw new Error(`Unknown grommet token "${token}"`) if (property in props) throw new Error(`Grommet property "${property}" is set more than once`) diff --git a/src/models/cablegrommet/register.ts b/src/models/cablegrommet/register.ts index 8d2264a..1aa4060 100644 --- a/src/models/cablegrommet/register.ts +++ b/src/models/cablegrommet/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parseCableGrommetModelParams } from "./parse-model-string" import { cableGrommetModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "cablegrommet", schema: cableGrommetModelDefinitionSchema, parse: parseCableGrommetModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parseCableGrommetModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/cablegrommet/schema.ts b/src/models/cablegrommet/schema.ts index f1ab37e..ee10e70 100644 --- a/src/models/cablegrommet/schema.ts +++ b/src/models/cablegrommet/schema.ts @@ -21,7 +21,7 @@ const shape = { grooveWidth: length, /** Radial recess from the flange outside diameter. */ grooveDepth: length, - shape: z.literal("symmetricring").default("symmetricring"), + symmetricRing: z.literal(true).default(true), } type ResolvedProps = z.output> diff --git a/src/models/cablegrommet/string-flags.ts b/src/models/cablegrommet/string-flags.ts deleted file mode 100644 index c1bcba6..0000000 --- a/src/models/cablegrommet/string-flags.ts +++ /dev/null @@ -1,6 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - symmetricring: "shape(symmetricring)", -} as const - -export const omittedStringFlags = [] as const diff --git a/src/models/clampingshaftcollar/parse-model-string.ts b/src/models/clampingshaftcollar/parse-model-string.ts index 7e7eab5..c0f1e22 100644 --- a/src/models/clampingshaftcollar/parse-model-string.ts +++ b/src/models/clampingshaftcollar/parse-model-string.ts @@ -1,5 +1,3 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { clampingShaftCollarModelDefinitionSchema } from "./schema" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" @@ -7,10 +5,8 @@ import { splitModelStringTokens } from "../../split-model-string-tokens" const tokenProperties: Record = { bore: "boreDiameter", od: "outerDiameter", - mount: "mount", m: "metricSize", threadpitch: "threadPitch", - threadhand: "threadHand", threadclass: "threadClass", chamfer: "chamfer", w: "width", @@ -24,9 +20,7 @@ const tokenProperties: Record = { export function parseClampingShaftCollarModelParams( raw: RawModelprinterParams, ) { - const tokens = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const tokens = splitModelStringTokens(raw.string) if ( raw.fn !== "clampingshaftcollar" || tokens[0]?.toLowerCase() !== "clampingshaftcollar" @@ -36,21 +30,28 @@ export function parseClampingShaftCollarModelParams( for (const token of tokens.slice(1)) { const match = token.match(/^([a-z]+)(.*)$/i) const name = match?.[1]?.toLowerCase() ?? "" + const value = match?.[2] ?? "" + if (["singleclamp", "lefthanded", "righthanded"].includes(name)) { + if (value) throw new Error(`Flag "${name}" does not take a value`) + const property = name === "singleclamp" ? "singleClamp" : "leftHanded" + if (property in props) + throw new Error( + `Duplicate or conflicting clampingshaftcollar flag "${property}"`, + ) + props[property] = name !== "righthanded" + continue + } const property = tokenProperties[name] if (!property) throw new Error(`Unknown clampingshaftcollar token "${token}"`) if (property in props) throw new Error(`Duplicate clampingshaftcollar property "${property}"`) - const value = match![2]! if (!value) throw new Error(`Token "${name}" requires a value`) - if (["mount", "threadHand", "threadClass"].includes(property)) { + if (property === "threadClass") { const argument = value.match(/^\(([^()]+)\)$/) if (!argument) throw new Error(`Token "${name}" requires one parenthesized argument`) - props[property] = - property === "threadClass" - ? argument[1]!.toUpperCase() - : argument[1]!.toLowerCase() + props[property] = argument[1]!.toUpperCase() } else if (property === "metricSize") { if (!/^\d+(?:\.\d+)?$/.test(value)) throw new Error("m requires a unitless metric designation") diff --git a/src/models/clampingshaftcollar/register.ts b/src/models/clampingshaftcollar/register.ts index f2fedfc..59dcd94 100644 --- a/src/models/clampingshaftcollar/register.ts +++ b/src/models/clampingshaftcollar/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parseClampingShaftCollarModelParams } from "./parse-model-string" import { clampingShaftCollarModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "clampingshaftcollar", schema: clampingShaftCollarModelDefinitionSchema, parse: parseClampingShaftCollarModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parseClampingShaftCollarModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/clampingshaftcollar/schema.ts b/src/models/clampingshaftcollar/schema.ts index 3febf61..30d710f 100644 --- a/src/models/clampingshaftcollar/schema.ts +++ b/src/models/clampingshaftcollar/schema.ts @@ -32,10 +32,10 @@ const shape = { splitWidth: positive, clampX: positive.optional(), clearanceHoleDiameter: positive.optional(), - mount: z.literal("singleclamp").default("singleclamp"), + singleClamp: z.literal(true).default(true), metricSize: metricBoltSizeSchema, threadPitch: positive.optional(), - threadHand: z.enum(["right", "left"]).default("right"), + leftHanded: z.boolean().default(false), threadClass: z.literal("6H").default("6H"), chamfer: nonnegative.default(0), } @@ -147,7 +147,7 @@ export function getClampingShaftCollarDimensions( diameter: threadDiameter, depth: threadedExtent - p.splitWidth / 2, threadPitch: p.threadPitch, - threadHand: p.threadHand, + leftHanded: p.leftHanded, threadGender: "female" as const, threadClass: p.threadClass, }, diff --git a/src/models/clampingshaftcollar/string-flags.ts b/src/models/clampingshaftcollar/string-flags.ts deleted file mode 100644 index cd30fe1..0000000 --- a/src/models/clampingshaftcollar/string-flags.ts +++ /dev/null @@ -1,8 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - singleclamp: "mount(singleclamp)", - lefthanded: "threadhand(left)", - righthanded: "threadhand(right)", -} as const - -export const omittedStringFlags = ["righthanded"] as const diff --git a/src/models/compressionspring/parse-model-string.ts b/src/models/compressionspring/parse-model-string.ts index c3c46a3..d17931a 100644 --- a/src/models/compressionspring/parse-model-string.ts +++ b/src/models/compressionspring/parse-model-string.ts @@ -1,13 +1,9 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { compressionSpringModelDefinitionSchema } from "./schema" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" export function parseCompressionSpringModelParams(raw: RawModelprinterParams) { - const tokens = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const tokens = splitModelStringTokens(raw.string) if ( raw.fn !== "compressionspring" || tokens[0]?.toLowerCase() !== "compressionspring" @@ -21,12 +17,13 @@ export function parseCompressionSpringModelParams(raw: RawModelprinterParams) { length: "freeLength", freelength: "freeLength", } as const - const selectors = { - spec: "spec", - ends: "ends", - hand: "hand", - state: "state", - } as const + const flags: Record = { + custom: "custom", + closedground: "closedGround", + free: "free", + lefthanded: "leftHanded", + righthanded: "leftHanded", + } for (const token of tokens.slice(1)) { const match = token.match(/^([a-z]+)(.*)$/i) if (!match) throw new Error(`Invalid compression spring token "${token}"`) @@ -40,11 +37,10 @@ export function parseCompressionSpringModelParams(raw: RawModelprinterParams) { if (!/^\d+$/.test(value)) throw new Error(`${name} requires a unitless integer`) parsed = Number(value) - } else if (name in selectors) { - property = selectors[name as keyof typeof selectors] - if (!/^\([^()]+\)$/.test(value)) - throw new Error(`Expected a parenthesized ${name}`) - parsed = value.slice(1, -1).toLowerCase() + } else if (Object.hasOwn(flags, name)) { + if (value) throw new Error(`Flag "${name}" does not take a value`) + property = flags[name]! + parsed = name !== "righthanded" } else throw new Error(`Unknown compression spring token "${token}"`) if (property in props) throw new Error(`Duplicate compression spring property "${property}"`) diff --git a/src/models/compressionspring/register.ts b/src/models/compressionspring/register.ts index 5f54aea..adc1ada 100644 --- a/src/models/compressionspring/register.ts +++ b/src/models/compressionspring/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parseCompressionSpringModelParams } from "./parse-model-string" import { compressionSpringModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "compressionspring", schema: compressionSpringModelDefinitionSchema, parse: parseCompressionSpringModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parseCompressionSpringModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/compressionspring/schema.ts b/src/models/compressionspring/schema.ts index f1535a1..be1a528 100644 --- a/src/models/compressionspring/schema.ts +++ b/src/models/compressionspring/schema.ts @@ -13,16 +13,16 @@ const length = z .refine((value) => value > 0, "Length must be positive") const turnCount = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER) const shape = { - spec: z.literal("custom").default("custom"), + custom: z.literal(true).default(true), outerDiameter: length, wireDiameter: length, freeLength: length, totalTurns: turnCount.min(3), /** Closed-ground ends have exactly one inactive turn at each end. */ activeTurns: turnCount.optional(), - ends: z.literal("closedground").default("closedground"), - hand: z.enum(["right", "left"]).default("right"), - state: z.literal("free").default("free"), + closedGround: z.literal(true).default(true), + leftHanded: z.boolean().default(false), + free: z.literal(true).default(true), } function validate( props: z.output>, @@ -113,7 +113,7 @@ export function getCompressionSpringCenterlinePoint( throw new Error("Turn coordinate must be finite and within [0,totalTurns]") const radius = (props.outerDiameter - props.wireDiameter) / 2 const pitch = (props.freeLength - 2 * props.wireDiameter) / props.activeTurns - const phase = (props.hand === "right" ? 1 : -1) * 2 * Math.PI * (turn % 1) + const phase = (props.leftHanded ? -1 : 1) * 2 * Math.PI * (turn % 1) const z = turn <= 1 ? props.wireDiameter * turn diff --git a/src/models/compressionspring/string-flags.ts b/src/models/compressionspring/string-flags.ts deleted file mode 100644 index 663fc1b..0000000 --- a/src/models/compressionspring/string-flags.ts +++ /dev/null @@ -1,10 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - closedground: "ends(closedground)", - lefthanded: "hand(left)", - righthanded: "hand(right)", - custom: "spec(custom)", - free: "state(free)", -} as const - -export const omittedStringFlags = ["righthanded", "custom", "free"] as const diff --git a/src/models/flangedbushing/parse-model-string.ts b/src/models/flangedbushing/parse-model-string.ts index 50b22d5..0617cd7 100644 --- a/src/models/flangedbushing/parse-model-string.ts +++ b/src/models/flangedbushing/parse-model-string.ts @@ -1,5 +1,3 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { flangedBushingModelDefinitionSchema } from "./schema" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" @@ -17,9 +15,7 @@ const lengths = { } as const export function parseFlangedBushingModelParams(raw: RawModelprinterParams) { - const tokens = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const tokens = splitModelStringTokens(raw.string) if ( raw.fn !== "flangedbushing" || tokens[0]?.toLowerCase() !== "flangedbushing" @@ -36,11 +32,10 @@ export function parseFlangedBushingModelParams(raw: RawModelprinterParams) { if (Object.hasOwn(lengths, name)) { property = lengths[name as keyof typeof lengths] parsed = value - } else if (name === "style") { - property = "style" - const style = value.match(/^\(([a-z]+)\)$/i) - if (!style) throw new Error("Style requires a parenthesized value") - parsed = style[1]!.toLowerCase() + } else if (name === "plainclosed") { + property = "plainClosed" + if (value !== "") throw new Error("plainclosed is a value-free flag") + parsed = true } else throw new Error(`Unknown flanged bushing token "${token}"`) if (Object.hasOwn(props, property)) throw new Error( diff --git a/src/models/flangedbushing/register.ts b/src/models/flangedbushing/register.ts index fbc4674..4fdc982 100644 --- a/src/models/flangedbushing/register.ts +++ b/src/models/flangedbushing/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parseFlangedBushingModelParams } from "./parse-model-string" import { flangedBushingModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "flangedbushing", schema: flangedBushingModelDefinitionSchema, parse: parseFlangedBushingModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parseFlangedBushingModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/flangedbushing/schema.ts b/src/models/flangedbushing/schema.ts index 850e89a..5a77efc 100644 --- a/src/models/flangedbushing/schema.ts +++ b/src/models/flangedbushing/schema.ts @@ -19,7 +19,7 @@ const shape = { /** Overall end-plane distance including the integral flange. */ length: positiveLength, flangeThickness: positiveLength, - style: z.literal("plainclosed").default("plainclosed"), + plainClosed: z.literal(true).default(true), } type ResolvedProps = z.output> diff --git a/src/models/flangedbushing/string-flags.ts b/src/models/flangedbushing/string-flags.ts deleted file mode 100644 index 0b97ed0..0000000 --- a/src/models/flangedbushing/string-flags.ts +++ /dev/null @@ -1,6 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - plainclosed: "style(plainclosed)", -} as const - -export const omittedStringFlags = ["plainclosed"] as const diff --git a/src/models/flatgasket/parse-model-string.ts b/src/models/flatgasket/parse-model-string.ts index 5913470..998fff7 100644 --- a/src/models/flatgasket/parse-model-string.ts +++ b/src/models/flatgasket/parse-model-string.ts @@ -10,9 +10,6 @@ const lengths: Record = { thickness: "thickness", } const flags: Record = { flatannulus: "flatAnnulus" } -const selectors: Record = { - profile: { value: "flatannulus", property: "flatAnnulus" }, -} const integers: Record = {} const tuples: Record = {} export function parseFlatGasketModelParams(raw: RawModelprinterParams) { @@ -32,11 +29,6 @@ export function parseFlatGasketModelParams(raw: RawModelprinterParams) { value = match[2]! if (Object.hasOwn(lengths, key) && value) assign(lengths[key]!, value) else if (Object.hasOwn(flags, key) && !value) assign(flags[key]!, true) - else if ( - Object.hasOwn(selectors, key) && - value.toLowerCase() === `(${selectors[key]!.value})` - ) - assign(selectors[key]!.property, true) else if (Object.hasOwn(integers, key) && /^\d+$/.test(value)) assign(integers[key]!, Number(value)) else if (Object.hasOwn(tuples, key)) { diff --git a/src/models/flatheadscrew/parse-model-string.ts b/src/models/flatheadscrew/parse-model-string.ts index c389ab0..cd3d01b 100644 --- a/src/models/flatheadscrew/parse-model-string.ts +++ b/src/models/flatheadscrew/parse-model-string.ts @@ -1,5 +1,3 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { flatHeadScrewModelDefinitionSchema } from "./schema" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" @@ -9,11 +7,7 @@ const aliases: Record = { metricsize: "metricSize", l: "length", length: "length", - thread: "thread", - drive: "drive", - threadhand: "threadHand", threadclass: "threadClass", - threadgender: "threadGender", diameter: "diameter", threadpitch: "threadPitch", headdiameter: "headDiameter", @@ -34,20 +28,19 @@ const aliases: Record = { socketaf: "socketAcrossFlats", socketh: "socketDepth", } -const selectors = new Set([ - "thread", - "drive", - "threadHand", - "threadClass", - "threadGender", -]) +const selectors = new Set(["threadClass"]) +const booleanFlags: Record = { + hexsocket: { property: "hexSocket", value: true }, + fullthread: { property: "fullThread", value: true }, + male: { property: "male", value: true }, + lefthanded: { property: "leftHanded", value: true }, + righthanded: { property: "leftHanded", value: false }, +} export const parseFlatHeadScrewModelParams = (raw: RawModelprinterParams) => { if (raw.fn !== "flatheadscrew") throw new Error(`Expected flatheadscrew params, got "${raw.fn}"`) - const [root, ...tokens] = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const [root, ...tokens] = splitModelStringTokens(raw.string) if (root?.toLowerCase() !== "flatheadscrew") throw new Error( "The flatheadscrew function does not accept an inline value", @@ -65,13 +58,17 @@ export const parseFlatHeadScrewModelParams = (raw: RawModelprinterParams) => { const value = match?.[2] let property: string let parsed: unknown - if (key === "threads" || key === "nothreads") { + if (key && Object.hasOwn(booleanFlags, key)) { + if (value !== "") throw new Error(`Flag "${key}" does not accept a value`) + property = booleanFlags[key]!.property + parsed = booleanFlags[key]!.value + } else if (key === "threads" || key === "nothreads") { if (value !== "") throw new Error(`Token "${key}" does not accept a value`) property = "showThreads" parsed = key === "threads" } else { - property = key ? (aliases[key] ?? "") : "" + property = key && Object.hasOwn(aliases, key) ? aliases[key]! : "" if (!property || !value) throw new Error(`Unknown or missing flatheadscrew token "${token}"`) if (selectors.has(property)) { diff --git a/src/models/flatheadscrew/register.ts b/src/models/flatheadscrew/register.ts index 31cb780..7bd1939 100644 --- a/src/models/flatheadscrew/register.ts +++ b/src/models/flatheadscrew/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parseFlatHeadScrewModelParams } from "./parse-model-string" import { flatHeadScrewModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "flatheadscrew", schema: flatHeadScrewModelDefinitionSchema, parse: parseFlatHeadScrewModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parseFlatHeadScrewModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/flatheadscrew/schema.ts b/src/models/flatheadscrew/schema.ts index f8a864b..5fb1696 100644 --- a/src/models/flatheadscrew/schema.ts +++ b/src/models/flatheadscrew/schema.ts @@ -88,11 +88,11 @@ const flatHeadScrewModelPropsShape = { iso10642: z.literal(true).default(true), metricSize: flatHeadScrewMetricSizeSchema, length: positiveModelLengthSchema, - thread: z.literal("full").default("full"), - drive: z.literal("hexsocket").default("hexsocket"), - threadHand: z.enum(["right", "left"]).default("right"), + fullThread: z.literal(true).default(true), + hexSocket: z.literal(true).default(true), + leftHanded: z.boolean().default(false), threadClass: z.literal("6g").default("6g"), - threadGender: z.literal("male").default("male"), + male: z.literal(true).default(true), showThreads: z.boolean().default(true), diameter: positiveModelLengthSchema.optional(), threadPitch: positiveModelLengthSchema.optional(), diff --git a/src/models/flatheadscrew/string-flags.ts b/src/models/flatheadscrew/string-flags.ts deleted file mode 100644 index 4a284df..0000000 --- a/src/models/flatheadscrew/string-flags.ts +++ /dev/null @@ -1,10 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - hexsocket: "drive(hexsocket)", - fullthread: "thread(full)", - lefthanded: "threadhand(left)", - righthanded: "threadhand(right)", - male: "threadgender(male)", -} as const - -export const omittedStringFlags = ["righthanded", "male"] as const diff --git a/src/models/flexscreen/schema.ts b/src/models/flexscreen/schema.ts index b81ea15..dea06d8 100644 --- a/src/models/flexscreen/schema.ts +++ b/src/models/flexscreen/schema.ts @@ -10,15 +10,6 @@ export { nonnegativeModelLengthSchema, } from "../../model-length-schema" -export const flexScreenOrientationSchema = z.enum([ - "sitsFlat", - "sitsFlatBelowBoard", - "foldedToFaceAboveBoard", - "foldedToFaceBelowBoard", - "foldedToRightAngleAboveBoard", - "foldedToRightAngleBelowBoard", -]) - const positiveFiniteNumberSchema = z.number().finite().positive() const aspectRatioStringSchema = z @@ -78,7 +69,6 @@ const flexScreenModelPropsShape = { ratio: flexScreenAspectRatioSchema.optional(), defaultDiagonal: positiveModelLengthSchema.optional(), - orientation: flexScreenOrientationSchema.optional(), sitsFlat: z.boolean().optional(), sitsFlatBelowBoard: z.boolean().optional(), foldedToFaceAboveBoard: z.boolean().optional(), @@ -151,12 +141,60 @@ const flexScreenModelPropsShape = { const addOrientationShortcutIssue = ( props: Record, - addIssue: (path: string) => void, + addIssue: (path: string, message: string) => void, ) => { const selectedShortcuts = orientationShortcutKeys.filter( (key) => props[key] === true, ) - if (selectedShortcuts.length > 1) addIssue(selectedShortcuts[1]!) + if (selectedShortcuts.length > 1) { + addIssue( + selectedShortcuts[1]!, + "Only one FlexScreen orientation shortcut can be true", + ) + } else if ( + selectedShortcuts.length === 0 && + orientationShortcutKeys.some((key) => props[key] !== undefined) + ) { + addIssue( + "sitsFlat", + "A FlexScreen orientation flag must be true when placement flags are supplied", + ) + } +} + +type OrientationFlags = Partial< + Record<(typeof orientationShortcutKeys)[number], boolean> +> + +const normalizeOrientationFlags = (props: T) => { + const { + sitsFlat, + sitsFlatBelowBoard, + foldedToFaceAboveBoard, + foldedToFaceBelowBoard, + foldedToRightAngleAboveBoard, + foldedToRightAngleBelowBoard, + foldsAboveBoard, + foldsBelowBoard, + ...rest + } = props + const above = foldedToFaceAboveBoard === true || foldsAboveBoard === true + const below = foldedToFaceBelowBoard === true || foldsBelowBoard === true + const selected = + sitsFlatBelowBoard === true || + above || + below || + foldedToRightAngleAboveBoard === true || + foldedToRightAngleBelowBoard === true + return { + ...rest, + sitsFlat: sitsFlat === true || !selected, + sitsFlatBelowBoard: sitsFlatBelowBoard === true, + foldedToFaceAboveBoard: above, + foldedToFaceBelowBoard: below, + foldedToRightAngleAboveBoard: foldedToRightAngleAboveBoard === true, + foldedToRightAngleBelowBoard: foldedToRightAngleBelowBoard === true, + } } const validateTailLengths = ( @@ -200,16 +238,16 @@ export const flexScreenModelPropsSchema = z .strict() .superRefine((props, context) => { validateTailLengths(props, context) - addOrientationShortcutIssue(props, (path) => { + addOrientationShortcutIssue(props, (path, message) => { context.addIssue({ code: "custom", - message: "Only one FlexScreen orientation shortcut can be true", + message, path: [path], }) }) }) + .transform(normalizeOrientationFlags) -export type FlexScreenOrientation = z.infer export type FlexScreenAspectRatio = z.infer export type FlexScreenModelPropsInput = z.input< typeof flexScreenModelPropsSchema @@ -224,14 +262,15 @@ export const flexScreenModelDefinitionSchema = z .strict() .superRefine((model, context) => { validateTailLengths(model, context) - addOrientationShortcutIssue(model, (path) => { + addOrientationShortcutIssue(model, (path, message) => { context.addIssue({ code: "custom", - message: "Only one FlexScreen orientation shortcut can be true", + message, path: [path], }) }) }) + .transform(normalizeOrientationFlags) export type FlexScreenModelDefinition = z.infer< typeof flexScreenModelDefinitionSchema diff --git a/src/models/hexbolt/parse-model-string.ts b/src/models/hexbolt/parse-model-string.ts index 9fcd274..bf2bbe8 100644 --- a/src/models/hexbolt/parse-model-string.ts +++ b/src/models/hexbolt/parse-model-string.ts @@ -1,5 +1,3 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { hexBoltModelDefinitionSchema } from "./schema" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" @@ -9,11 +7,7 @@ const aliases: Record = { metricsize: "metricSize", l: "length", length: "length", - thread: "thread", - drive: "drive", - threadhand: "threadHand", threadclass: "threadClass", - threadgender: "threadGender", diameter: "diameter", threadpitch: "threadPitch", headacrossflats: "headAcrossFlats", @@ -31,20 +25,19 @@ const aliases: Record = { headh: "headHeight", af: "headAcrossFlats", } -const selectors = new Set([ - "thread", - "drive", - "threadHand", - "threadClass", - "threadGender", -]) +const selectors = new Set(["threadClass"]) +const booleanFlags: Record = { + hex: { property: "hex", value: true }, + fullthread: { property: "fullThread", value: true }, + male: { property: "male", value: true }, + lefthanded: { property: "leftHanded", value: true }, + righthanded: { property: "leftHanded", value: false }, +} export const parseHexBoltModelParams = (raw: RawModelprinterParams) => { if (raw.fn !== "hexbolt") throw new Error(`Expected hexbolt params, got "${raw.fn}"`) - const [root, ...tokens] = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const [root, ...tokens] = splitModelStringTokens(raw.string) if (root?.toLowerCase() !== "hexbolt") throw new Error("The hexbolt function does not accept an inline value") const props: Record = {} @@ -60,13 +53,17 @@ export const parseHexBoltModelParams = (raw: RawModelprinterParams) => { const value = match?.[2] let property: string let parsed: unknown - if (key === "threads" || key === "nothreads") { + if (key && Object.hasOwn(booleanFlags, key)) { + if (value !== "") throw new Error(`Flag "${key}" does not accept a value`) + property = booleanFlags[key]!.property + parsed = booleanFlags[key]!.value + } else if (key === "threads" || key === "nothreads") { if (value !== "") throw new Error(`Token "${key}" does not accept a value`) property = "showThreads" parsed = key === "threads" } else { - property = key ? (aliases[key] ?? "") : "" + property = key && Object.hasOwn(aliases, key) ? aliases[key]! : "" if (!property || !value) throw new Error(`Unknown or missing hexbolt token "${token}"`) if (selectors.has(property)) { diff --git a/src/models/hexbolt/register.ts b/src/models/hexbolt/register.ts index e5a277f..f48f5e1 100644 --- a/src/models/hexbolt/register.ts +++ b/src/models/hexbolt/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { hexBoltModelDefinitionSchema } from "./schema" import { parseHexBoltModelParams } from "./parse-model-string" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "hexbolt", schema: hexBoltModelDefinitionSchema, parse: parseHexBoltModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parseHexBoltModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/hexbolt/schema.ts b/src/models/hexbolt/schema.ts index 4723e31..1c3e487 100644 --- a/src/models/hexbolt/schema.ts +++ b/src/models/hexbolt/schema.ts @@ -84,11 +84,11 @@ const hexBoltModelPropsShape = { iso4017: z.literal(true).default(true), metricSize: hexBoltMetricSizeSchema, length: positiveModelLengthSchema, - thread: z.literal("full").default("full"), - drive: z.literal("hex").default("hex"), - threadHand: z.enum(["right", "left"]).default("right"), + fullThread: z.literal(true).default(true), + hex: z.literal(true).default(true), + leftHanded: z.boolean().default(false), threadClass: z.literal("6g").default("6g"), - threadGender: z.literal("male").default("male"), + male: z.literal(true).default(true), showThreads: z.boolean().default(true), diameter: positiveModelLengthSchema.optional(), threadPitch: positiveModelLengthSchema.optional(), diff --git a/src/models/hexbolt/string-flags.ts b/src/models/hexbolt/string-flags.ts deleted file mode 100644 index 44f3be2..0000000 --- a/src/models/hexbolt/string-flags.ts +++ /dev/null @@ -1,10 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - hex: "drive(hex)", - fullthread: "thread(full)", - lefthanded: "threadhand(left)", - righthanded: "threadhand(right)", - male: "threadgender(male)", -} as const - -export const omittedStringFlags = ["righthanded", "male"] as const diff --git a/src/models/hexnut/parse-model-string.ts b/src/models/hexnut/parse-model-string.ts index 9b76d5b..2f3983b 100644 --- a/src/models/hexnut/parse-model-string.ts +++ b/src/models/hexnut/parse-model-string.ts @@ -1,18 +1,13 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { hexNutModelDefinitionSchema } from "./schema" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" export function parseHexNutModelParams(raw: RawModelprinterParams) { - const tokens = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const tokens = splitModelStringTokens(raw.string) if (raw.fn !== "hexnut" || tokens[0]?.toLowerCase() !== "hexnut") throw new Error("Expected hexnut without an inline argument") const props: Record = { fn: "hexnut" } const selectors = { - threadhand: "threadHand", threadclass: "threadClass", } as const let explicitFamily = false @@ -49,6 +44,10 @@ export function parseHexNutModelParams(raw: RawModelprinterParams) { throw new Error("Duplicate hex nut property threadPitch") props.threadPitch = 25.4 / Number(parts[1]) } + } else if (name === "righthanded") { + if (value) throw new Error("Handedness flags cannot have a value") + property = "rightHanded" + parsed = true } else if (name === "threadpitch") property = "threadPitch" else if (name in selectors) { property = selectors[name as keyof typeof selectors] diff --git a/src/models/hexnut/register.ts b/src/models/hexnut/register.ts index fdb4af9..bac2a66 100644 --- a/src/models/hexnut/register.ts +++ b/src/models/hexnut/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parseHexNutModelParams } from "./parse-model-string" import { hexNutModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "hexnut", schema: hexNutModelDefinitionSchema, parse: parseHexNutModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parseHexNutModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/hexnut/schema.ts b/src/models/hexnut/schema.ts index b4d7135..7841d17 100644 --- a/src/models/hexnut/schema.ts +++ b/src/models/hexnut/schema.ts @@ -268,7 +268,7 @@ const shape = { metricSize: hexNutMetricSizeSchema.optional(), imperialSize: hexNutImperialSizeSchema.optional(), threadPitch: length.optional(), - threadHand: z.literal("right").default("right"), + rightHanded: z.literal(true).default(true), threadClass: z.enum(["6H", "2B"]).optional(), showThreads: z.boolean().default(true), } diff --git a/src/models/hexnut/string-flags.ts b/src/models/hexnut/string-flags.ts deleted file mode 100644 index 0a8cd43..0000000 --- a/src/models/hexnut/string-flags.ts +++ /dev/null @@ -1,6 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - righthanded: "threadhand(right)", -} as const - -export const omittedStringFlags = ["righthanded"] as const diff --git a/src/models/panscrew/parse-model-string.ts b/src/models/panscrew/parse-model-string.ts index 14421ee..8573e0c 100644 --- a/src/models/panscrew/parse-model-string.ts +++ b/src/models/panscrew/parse-model-string.ts @@ -1,5 +1,3 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { panScrewModelDefinitionSchema } from "./schema" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" @@ -9,11 +7,7 @@ const aliases: Record = { metricsize: "metricSize", l: "length", length: "length", - thread: "thread", - drive: "drive", - threadhand: "threadHand", threadclass: "threadClass", - threadgender: "threadGender", diameter: "diameter", threadpitch: "threadPitch", headdiameter: "headDiameter", @@ -44,20 +38,19 @@ const aliases: Record = { headh: "headHeight", headd: "headDiameter", } -const selectors = new Set([ - "thread", - "drive", - "threadHand", - "threadClass", - "threadGender", -]) +const selectors = new Set(["threadClass"]) +const booleanFlags: Record = { + phillips: { property: "phillips", value: true }, + fullthread: { property: "fullThread", value: true }, + male: { property: "male", value: true }, + lefthanded: { property: "leftHanded", value: true }, + righthanded: { property: "leftHanded", value: false }, +} export const parsePanScrewModelParams = (raw: RawModelprinterParams) => { if (raw.fn !== "panscrew") throw new Error(`Expected panscrew params, got "${raw.fn}"`) - const [root, ...tokens] = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const [root, ...tokens] = splitModelStringTokens(raw.string) if (root?.toLowerCase() !== "panscrew") throw new Error("The panscrew function does not accept an inline value") const props: Record = {} @@ -75,13 +68,17 @@ export const parsePanScrewModelParams = (raw: RawModelprinterParams) => { const value = match?.[2] let property: string let parsed: unknown - if (key === "threads" || key === "nothreads") { + if (key && Object.hasOwn(booleanFlags, key)) { + if (value !== "") throw new Error(`Flag "${key}" does not accept a value`) + property = booleanFlags[key]!.property + parsed = booleanFlags[key]!.value + } else if (key === "threads" || key === "nothreads") { if (value !== "") throw new Error(`Token "${key}" does not accept a value`) property = "showThreads" parsed = key === "threads" } else { - property = key ? (aliases[key] ?? "") : "" + property = key && Object.hasOwn(aliases, key) ? aliases[key]! : "" if (!property || !value) throw new Error(`Unknown or missing panscrew token "${token}"`) if (selectors.has(property)) { diff --git a/src/models/panscrew/register.ts b/src/models/panscrew/register.ts index a70fd32..6a5367a 100644 --- a/src/models/panscrew/register.ts +++ b/src/models/panscrew/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parsePanScrewModelParams } from "./parse-model-string" import { panScrewModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "panscrew", schema: panScrewModelDefinitionSchema, parse: parsePanScrewModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parsePanScrewModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/panscrew/schema.ts b/src/models/panscrew/schema.ts index 2dee052..f6ae931 100644 --- a/src/models/panscrew/schema.ts +++ b/src/models/panscrew/schema.ts @@ -136,11 +136,11 @@ const panScrewModelPropsShape = { iso7045: z.literal(true).default(true), metricSize: panScrewMetricSizeSchema, length: positiveModelLengthSchema, - thread: z.literal("full").default("full"), - drive: z.literal("phillips").default("phillips"), - threadHand: z.enum(["right", "left"]).default("right"), + fullThread: z.literal(true).default(true), + phillips: z.literal(true).default(true), + leftHanded: z.boolean().default(false), threadClass: z.literal("6g").default("6g"), - threadGender: z.literal("male").default("male"), + male: z.literal(true).default(true), showThreads: z.boolean().default(true), iso4757: z.literal(true).default(true), recessType: z.literal("H").default("H"), diff --git a/src/models/panscrew/string-flags.ts b/src/models/panscrew/string-flags.ts deleted file mode 100644 index 4b3192d..0000000 --- a/src/models/panscrew/string-flags.ts +++ /dev/null @@ -1,10 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - phillips: "drive(phillips)", - fullthread: "thread(full)", - lefthanded: "threadhand(left)", - righthanded: "threadhand(right)", - male: "threadgender(male)", -} as const - -export const omittedStringFlags = ["righthanded", "male"] as const diff --git a/src/models/plainbushing/parse-model-string.ts b/src/models/plainbushing/parse-model-string.ts index 05c4b05..81f655c 100644 --- a/src/models/plainbushing/parse-model-string.ts +++ b/src/models/plainbushing/parse-model-string.ts @@ -1,5 +1,3 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import type { RawModelprinterParams } from "../../parse-model-string" import { plainBushingModelDefinitionSchema } from "./schema" import { splitModelStringTokens } from "../../split-model-string-tokens" @@ -15,9 +13,7 @@ const lengths = { } as const export function parsePlainBushingModelParams(raw: RawModelprinterParams) { - const tokens = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const tokens = splitModelStringTokens(raw.string) if (raw.fn !== "plainbushing" || tokens[0]?.toLowerCase() !== "plainbushing") throw new Error("Expected plainbushing without an inline value") const props: Record = { fn: "plainbushing" } @@ -31,11 +27,10 @@ export function parsePlainBushingModelParams(raw: RawModelprinterParams) { if (Object.hasOwn(lengths, name)) { property = lengths[name as keyof typeof lengths] parsed = value - } else if (name === "style") { - property = "style" - const style = value.match(/^\(([a-z]+)\)$/i) - if (!style) throw new Error("Style requires a parenthesized value") - parsed = style[1]!.toLowerCase() + } else if (name === "plainclosed") { + property = "plainClosed" + if (value !== "") throw new Error("plainclosed is a value-free flag") + parsed = true } else throw new Error(`Unknown plain bushing token "${token}"`) if (Object.hasOwn(props, property)) throw new Error( diff --git a/src/models/plainbushing/register.ts b/src/models/plainbushing/register.ts index ecd3308..462ba5d 100644 --- a/src/models/plainbushing/register.ts +++ b/src/models/plainbushing/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parsePlainBushingModelParams } from "./parse-model-string" import { plainBushingModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "plainbushing", schema: plainBushingModelDefinitionSchema, parse: parsePlainBushingModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parsePlainBushingModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/plainbushing/schema.ts b/src/models/plainbushing/schema.ts index ab6a36a..c335eea 100644 --- a/src/models/plainbushing/schema.ts +++ b/src/models/plainbushing/schema.ts @@ -17,7 +17,7 @@ const shape = { outerDiameter: positiveLength, /** Overall end-plane distance, including the chamfers. */ length: positiveLength, - style: z.literal("plainclosed").default("plainclosed"), + plainClosed: z.literal(true).default(true), /** Equal axial/radial setbacks of all four 45-degree bore/outside rim chamfers. */ edgeChamfer: length.refine((value) => value >= 0).default(0), } diff --git a/src/models/plainbushing/string-flags.ts b/src/models/plainbushing/string-flags.ts deleted file mode 100644 index 0b97ed0..0000000 --- a/src/models/plainbushing/string-flags.ts +++ /dev/null @@ -1,6 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - plainclosed: "style(plainclosed)", -} as const - -export const omittedStringFlags = ["plainclosed"] as const diff --git a/src/models/rectangulargasket/parse-model-string.ts b/src/models/rectangulargasket/parse-model-string.ts index 259c3e7..46ce29a 100644 --- a/src/models/rectangulargasket/parse-model-string.ts +++ b/src/models/rectangulargasket/parse-model-string.ts @@ -13,9 +13,6 @@ const lengths: Record = { cornerradius: "cornerRadius", } const flags: Record = { flatframe: "flatFrame" } -const selectors: Record = { - profile: { value: "flatframe", property: "flatFrame" }, -} const integers: Record = {} const tuples: Record = {} export function parseRectangularGasketModelParams(raw: RawModelprinterParams) { @@ -38,11 +35,6 @@ export function parseRectangularGasketModelParams(raw: RawModelprinterParams) { value = match[2]! if (Object.hasOwn(lengths, key) && value) assign(lengths[key]!, value) else if (Object.hasOwn(flags, key) && !value) assign(flags[key]!, true) - else if ( - Object.hasOwn(selectors, key) && - value.toLowerCase() === `(${selectors[key]!.value})` - ) - assign(selectors[key]!.property, true) else if (Object.hasOwn(integers, key) && /^\d+$/.test(value)) assign(integers[key]!, Number(value)) else if (Object.hasOwn(tuples, key)) { diff --git a/src/models/rigidcoupler/parse-model-string.ts b/src/models/rigidcoupler/parse-model-string.ts index 3c3689e..7e21075 100644 --- a/src/models/rigidcoupler/parse-model-string.ts +++ b/src/models/rigidcoupler/parse-model-string.ts @@ -1,5 +1,3 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { rigidCouplerModelDefinitionSchema } from "./schema" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" @@ -7,10 +5,8 @@ import { splitModelStringTokens } from "../../split-model-string-tokens" const tokenProperties: Record = { bore: "boreDiameter", od: "outerDiameter", - mount: "mount", m: "metricSize", threadpitch: "threadPitch", - threadhand: "threadHand", threadclass: "threadClass", chamfer: "chamfer", boreb: "boreBDiameter", @@ -22,29 +18,34 @@ const tokenProperties: Record = { } export function parseRigidCouplerModelParams(raw: RawModelprinterParams) { - const tokens = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const tokens = splitModelStringTokens(raw.string) if (raw.fn !== "rigidcoupler" || tokens[0]?.toLowerCase() !== "rigidcoupler") throw new Error("Expected an unadorned rigidcoupler function") const props: Record = {} for (const token of tokens.slice(1)) { const match = token.match(/^([a-z]+)(.*)$/i) const name = match?.[1]?.toLowerCase() ?? "" + const value = match?.[2] ?? "" + if (["setscrew", "lefthanded", "righthanded"].includes(name)) { + if (value) throw new Error(`Flag "${name}" does not take a value`) + const property = name === "setscrew" ? "setScrew" : "leftHanded" + if (property in props) + throw new Error( + `Duplicate or conflicting rigidcoupler flag "${property}"`, + ) + props[property] = name !== "righthanded" + continue + } const property = tokenProperties[name] if (!property) throw new Error(`Unknown rigidcoupler token "${token}"`) if (property in props) throw new Error(`Duplicate rigidcoupler property "${property}"`) - const value = match![2]! if (!value) throw new Error(`Token "${name}" requires a value`) - if (["mount", "threadHand", "threadClass"].includes(property)) { + if (property === "threadClass") { const argument = value.match(/^\(([^()]+)\)$/) if (!argument) throw new Error(`Token "${name}" requires one parenthesized argument`) - props[property] = - property === "threadClass" - ? argument[1]!.toUpperCase() - : argument[1]!.toLowerCase() + props[property] = argument[1]!.toUpperCase() } else if (property === "metricSize") { if (!/^\d+(?:\.\d+)?$/.test(value)) throw new Error("m requires a unitless metric designation") diff --git a/src/models/rigidcoupler/register.ts b/src/models/rigidcoupler/register.ts index c9b465d..3fbbe91 100644 --- a/src/models/rigidcoupler/register.ts +++ b/src/models/rigidcoupler/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parseRigidCouplerModelParams } from "./parse-model-string" import { rigidCouplerModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "rigidcoupler", schema: rigidCouplerModelDefinitionSchema, parse: parseRigidCouplerModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parseRigidCouplerModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/rigidcoupler/schema.ts b/src/models/rigidcoupler/schema.ts index 8151cb2..91ce79a 100644 --- a/src/models/rigidcoupler/schema.ts +++ b/src/models/rigidcoupler/schema.ts @@ -32,10 +32,10 @@ const shape = { screwCount: z.literal(4).default(4), screwEndOffset: positive.optional(), screwAngle: z.number().finite().default(0), - mount: z.literal("setscrew").default("setscrew"), + setScrew: z.literal(true).default(true), metricSize: metricBoltSizeSchema, threadPitch: positive.optional(), - threadHand: z.enum(["right", "left"]).default("right"), + leftHanded: z.boolean().default(false), threadClass: z.literal("6H").default("6H"), chamfer: nonnegative.default(0), } @@ -131,7 +131,7 @@ export function getRigidCouplerDimensions(input: RigidCouplerModelPropsInput) { depth: radius - Math.sqrt((hole.bore / 2) ** 2 - (threadDiameter / 2) ** 2), threadPitch: p.threadPitch, - threadHand: p.threadHand, + leftHanded: p.leftHanded, threadGender: "female" as const, threadClass: p.threadClass, } diff --git a/src/models/rigidcoupler/string-flags.ts b/src/models/rigidcoupler/string-flags.ts deleted file mode 100644 index c14525b..0000000 --- a/src/models/rigidcoupler/string-flags.ts +++ /dev/null @@ -1,8 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - setscrew: "mount(setscrew)", - lefthanded: "threadhand(left)", - righthanded: "threadhand(right)", -} as const - -export const omittedStringFlags = ["righthanded"] as const diff --git a/src/models/shaftcollar/parse-model-string.ts b/src/models/shaftcollar/parse-model-string.ts index 1e43a06..cd1e58d 100644 --- a/src/models/shaftcollar/parse-model-string.ts +++ b/src/models/shaftcollar/parse-model-string.ts @@ -1,5 +1,3 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { shaftCollarModelDefinitionSchema } from "./schema" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" @@ -7,10 +5,8 @@ import { splitModelStringTokens } from "../../split-model-string-tokens" const tokenProperties: Record = { bore: "boreDiameter", od: "outerDiameter", - mount: "mount", m: "metricSize", threadpitch: "threadPitch", - threadhand: "threadHand", threadclass: "threadClass", chamfer: "chamfer", w: "width", @@ -20,29 +16,34 @@ const tokenProperties: Record = { } export function parseShaftCollarModelParams(raw: RawModelprinterParams) { - const tokens = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const tokens = splitModelStringTokens(raw.string) if (raw.fn !== "shaftcollar" || tokens[0]?.toLowerCase() !== "shaftcollar") throw new Error("Expected an unadorned shaftcollar function") const props: Record = {} for (const token of tokens.slice(1)) { const match = token.match(/^([a-z]+)(.*)$/i) const name = match?.[1]?.toLowerCase() ?? "" + const value = match?.[2] ?? "" + if (["setscrew", "lefthanded", "righthanded"].includes(name)) { + if (value) throw new Error(`Flag "${name}" does not take a value`) + const property = name === "setscrew" ? "setScrew" : "leftHanded" + if (property in props) + throw new Error( + `Duplicate or conflicting shaftcollar flag "${property}"`, + ) + props[property] = name !== "righthanded" + continue + } const property = tokenProperties[name] if (!property) throw new Error(`Unknown shaftcollar token "${token}"`) if (property in props) throw new Error(`Duplicate shaftcollar property "${property}"`) - const value = match![2]! if (!value) throw new Error(`Token "${name}" requires a value`) - if (["mount", "threadHand", "threadClass"].includes(property)) { + if (property === "threadClass") { const argument = value.match(/^\(([^()]+)\)$/) if (!argument) throw new Error(`Token "${name}" requires one parenthesized argument`) - props[property] = - property === "threadClass" - ? argument[1]!.toUpperCase() - : argument[1]!.toLowerCase() + props[property] = argument[1]!.toUpperCase() } else if (property === "metricSize") { if (!/^\d+(?:\.\d+)?$/.test(value)) throw new Error("m requires a unitless metric designation") diff --git a/src/models/shaftcollar/register.ts b/src/models/shaftcollar/register.ts index 3134b59..78aa924 100644 --- a/src/models/shaftcollar/register.ts +++ b/src/models/shaftcollar/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parseShaftCollarModelParams } from "./parse-model-string" import { shaftCollarModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "shaftcollar", schema: shaftCollarModelDefinitionSchema, parse: parseShaftCollarModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parseShaftCollarModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/shaftcollar/schema.ts b/src/models/shaftcollar/schema.ts index 4d2051f..5e04cd2 100644 --- a/src/models/shaftcollar/schema.ts +++ b/src/models/shaftcollar/schema.ts @@ -30,10 +30,10 @@ const shape = { width: positive, screwZ: positive.optional(), screwAngle: z.number().finite().default(0), - mount: z.literal("setscrew").default("setscrew"), + setScrew: z.literal(true).default(true), metricSize: metricBoltSizeSchema, threadPitch: positive.optional(), - threadHand: z.enum(["right", "left"]).default("right"), + leftHanded: z.boolean().default(false), threadClass: z.literal("6H").default("6H"), chamfer: nonnegative.default(0), } @@ -111,7 +111,7 @@ export function getShaftCollarDimensions(input: ShaftCollarModelPropsInput) { radius - Math.sqrt((p.boreDiameter / 2) ** 2 - (threadDiameter / 2) ** 2), threadPitch: p.threadPitch, - threadHand: p.threadHand, + leftHanded: p.leftHanded, threadGender: "female" as const, threadClass: p.threadClass, }, diff --git a/src/models/shaftcollar/string-flags.ts b/src/models/shaftcollar/string-flags.ts deleted file mode 100644 index c14525b..0000000 --- a/src/models/shaftcollar/string-flags.ts +++ /dev/null @@ -1,8 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - setscrew: "mount(setscrew)", - lefthanded: "threadhand(left)", - righthanded: "threadhand(right)", -} as const - -export const omittedStringFlags = ["righthanded"] as const diff --git a/src/models/tslotcoverstrip/parse-model-string.ts b/src/models/tslotcoverstrip/parse-model-string.ts index dae11fa..f959441 100644 --- a/src/models/tslotcoverstrip/parse-model-string.ts +++ b/src/models/tslotcoverstrip/parse-model-string.ts @@ -20,9 +20,6 @@ const aliases: Record = { const flags: Record = { tee: "tee", } -const literals: Record = { - profile: ["tee", "tee"], -} export function parseTSlotCoverStripModelParams(raw: RawModelprinterParams) { const [name, ...tokens] = splitModelStringTokens(raw.string) if (raw.fn !== "tslotcoverstrip" || name?.toLowerCase() !== "tslotcoverstrip") @@ -43,11 +40,6 @@ export function parseTSlotCoverStripModelParams(raw: RawModelprinterParams) { if (value !== "") throw new Error(`tslotcoverstrip flag "${key}" takes no value`) assign(flags[key]!, true) - } else if (Object.hasOwn(literals, key)) { - const [literal, property] = literals[key]! - if (value.toLowerCase() !== `(${literal})`) - throw new Error(`Expected tslotcoverstrip ${key}(${literal})`) - assign(property, true) } else if (Object.hasOwn(aliases, key) && value) { const property = aliases[key]! assign(property, property === "pinCount" ? Number(value) : value) diff --git a/src/models/tslotcoverstrip/schema.ts b/src/models/tslotcoverstrip/schema.ts index 9ebd87b..0131e67 100644 --- a/src/models/tslotcoverstrip/schema.ts +++ b/src/models/tslotcoverstrip/schema.ts @@ -77,7 +77,7 @@ export type TSlotCoverStripModelDefinition = z.output< typeof tSlotCoverStripModelDefinitionSchema > -/** A continuous T-section groove cover: a broad top strip, narrow insertion stem, and wider rectangular retention bead at the stem tip. Length runs along +Z from Z=0. The cover underside mates at Y=0, its top is Y=thickness, and its inserted tip is Y=-stemHeight. barbHeight defaults to 0.5mm and is included in stemHeight. _tee is the value-free profile flag; _profile(tee) is accepted as the roadmap alias. All normalized dimensions are millimeters. */ +/** A continuous T-section groove cover: a broad top strip, narrow insertion stem, and wider rectangular retention bead at the stem tip. Length runs along +Z from Z=0. The cover underside mates at Y=0, its top is Y=thickness, and its inserted tip is Y=-stemHeight. barbHeight defaults to 0.5mm and is included in stemHeight. _tee is the value-free profile flag. All normalized dimensions are millimeters. */ export function getTSlotCoverStripDimensions( input: TSlotCoverStripModelPropsInput, ) { diff --git a/src/models/tslotextrusion/parse-model-string.ts b/src/models/tslotextrusion/parse-model-string.ts index 596dfee..68628e1 100644 --- a/src/models/tslotextrusion/parse-model-string.ts +++ b/src/models/tslotextrusion/parse-model-string.ts @@ -1,5 +1,3 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { parseGearToken } from "../../gear-parameter-schemas" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" @@ -27,9 +25,7 @@ const lengths = { } as const export function parseTSlotExtrusionModelParams(raw: RawModelprinterParams) { - const tokens = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const tokens = splitModelStringTokens(raw.string) if (raw.fn !== "tslotextrusion" || tokens[0]?.toLowerCase() !== raw.fn) throw new Error("Expected tslotextrusion without an inline value") const props: Record = { fn: "tslotextrusion" } @@ -38,11 +34,10 @@ export function parseTSlotExtrusionModelParams(raw: RawModelprinterParams) { let property: string let parsed: unknown = value if (name in lengths) property = lengths[name as keyof typeof lengths] - else if (name === "profile") { - property = "profile" - const match = value.match(/^\(([a-z]+)\)$/i) - if (!match) throw new Error("Profile requires one parenthesized identity") - parsed = match[1]!.toLowerCase() + else if (name === "fourtsolid") { + property = "fourTSolid" + if (value !== "") throw new Error("fourtsolid is a value-free flag") + parsed = true } else throw new Error(`Unknown T-slot extrusion token "${token}"`) if (property in props) throw new Error(`T-slot extrusion property "${property}" is repeated`) diff --git a/src/models/tslotextrusion/register.ts b/src/models/tslotextrusion/register.ts index 19d12e3..0fe6275 100644 --- a/src/models/tslotextrusion/register.ts +++ b/src/models/tslotextrusion/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parseTSlotExtrusionModelParams } from "./parse-model-string" import { tSlotExtrusionModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "tslotextrusion", schema: tSlotExtrusionModelDefinitionSchema, parse: parseTSlotExtrusionModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parseTSlotExtrusionModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/tslotextrusion/schema.ts b/src/models/tslotextrusion/schema.ts index 833b4bc..c7e4211 100644 --- a/src/models/tslotextrusion/schema.ts +++ b/src/models/tslotextrusion/schema.ts @@ -8,7 +8,7 @@ const shape = { width: positive.default(20), height: positive.default(20), length: positive.default(100), - profile: z.literal("fourtsolid").default("fourtsolid"), + fourTSolid: z.literal(true).default(true), slotWidth: positive.default(6), pocketWidth: positive.default(10), pocketDepth: positive.default(2), diff --git a/src/models/tslotextrusion/string-flags.ts b/src/models/tslotextrusion/string-flags.ts deleted file mode 100644 index f08d2ea..0000000 --- a/src/models/tslotextrusion/string-flags.ts +++ /dev/null @@ -1,6 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - fourtsolid: "profile(fourtsolid)", -} as const - -export const omittedStringFlags = [] as const diff --git a/src/models/tslotgusset/parse-model-string.ts b/src/models/tslotgusset/parse-model-string.ts index 8c4d2fc..afbdade 100644 --- a/src/models/tslotgusset/parse-model-string.ts +++ b/src/models/tslotgusset/parse-model-string.ts @@ -1,5 +1,3 @@ -import { expandModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags } from "./string-flags" import { parseGearInteger, parseGearToken } from "../../gear-parameter-schemas" import type { RawModelprinterParams } from "../../parse-model-string" import { splitModelStringTokens } from "../../split-model-string-tokens" @@ -16,9 +14,7 @@ const lengths = { } as const export function parseTSlotGussetModelParams(raw: RawModelprinterParams) { - const tokens = splitModelStringTokens( - expandModelStringFlags(raw.string, stringFlags), - ) + const tokens = splitModelStringTokens(raw.string) if (raw.fn !== "tslotgusset" || tokens[0]?.toLowerCase() !== raw.fn) throw new Error("Expected tslotgusset without an inline value") const props: Record = { fn: "tslotgusset" } @@ -30,11 +26,10 @@ export function parseTSlotGussetModelParams(raw: RawModelprinterParams) { else if (name === "slots" || name === "slotcount") { property = "slotCount" parsed = parseGearInteger(value, "slotCount") - } else if (name === "shape") { - property = "shape" - const match = value.match(/^\(([a-z]+)\)$/i) - if (!match) throw new Error("Shape requires one parenthesized identity") - parsed = match[1]!.toLowerCase() + } else if (name === "righttriangle") { + property = "rightTriangle" + if (value !== "") throw new Error("righttriangle is a value-free flag") + parsed = true } else if (name === "slot" || name === "centers") { property = name const match = value.match(/^\(([^(),]+),([^(),]+)\)$/) diff --git a/src/models/tslotgusset/register.ts b/src/models/tslotgusset/register.ts index a994e9d..15373bb 100644 --- a/src/models/tslotgusset/register.ts +++ b/src/models/tslotgusset/register.ts @@ -1,5 +1,3 @@ -import { normalizeModelStringFlags } from "../../utils/model-string-flags" -import { stringFlags, omittedStringFlags } from "./string-flags" import { defineModel, type ModelRegistry } from "../../model-registry" import { parseTSlotGussetModelParams } from "./parse-model-string" import { tSlotGussetModelDefinitionSchema } from "./schema" @@ -8,13 +6,6 @@ export const model = defineModel({ name: "tslotgusset", schema: tSlotGussetModelDefinitionSchema, parse: parseTSlotGussetModelParams, - normalizeString: (value) => - normalizeModelStringFlags( - value, - stringFlags, - parseTSlotGussetModelParams, - omittedStringFlags, - ), }) export function register(registry: ModelRegistry): void { diff --git a/src/models/tslotgusset/schema.ts b/src/models/tslotgusset/schema.ts index 0360e46..a7cccee 100644 --- a/src/models/tslotgusset/schema.ts +++ b/src/models/tslotgusset/schema.ts @@ -5,7 +5,7 @@ const shape = { width: positive.default(40), height: positive.default(40), thickness: positive.default(4), - shape: z.literal("righttriangle").default("righttriangle"), + rightTriangle: z.literal(true).default(true), slotCount: z.literal(2).default(2), /** Capsule width and overall length, including both semicircular ends. */ slot: z.tuple([positive, positive]).default([5, 12]), diff --git a/src/models/tslotgusset/string-flags.ts b/src/models/tslotgusset/string-flags.ts deleted file mode 100644 index d8a1797..0000000 --- a/src/models/tslotgusset/string-flags.ts +++ /dev/null @@ -1,6 +0,0 @@ -// String aliases preserve the released JSON schema used by renderers. -export const stringFlags = { - righttriangle: "shape(righttriangle)", -} as const - -export const omittedStringFlags = [] as const diff --git a/src/nema-motor-reference-points.ts b/src/nema-motor-reference-points.ts index 67159ca..4d61626 100644 --- a/src/nema-motor-reference-points.ts +++ b/src/nema-motor-reference-points.ts @@ -22,7 +22,7 @@ export function getNemaMotorReferencePoints(input: NemaMotorModelPropsInput) { 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") { + if (p.nemaSize === 34 && p.stepped) { const h = p.bodyWidth / 2, c = p.bodyCornerChamfer / Math.SQRT2, r = Math.min(4, c / 4) @@ -77,24 +77,19 @@ export function getNemaMotorReferencePoints(input: NemaMotorModelPropsInput) { }, direction: { x, y, z: 0 }, }, - shaftflat: - p.shaftShape === "d" - ? { - position: { - x: - (p.shaftDiameter / 2 - p.shaftFlatDepth) * - Math.cos(flatRadians), - y: - (p.shaftDiameter / 2 - p.shaftFlatDepth) * - Math.sin(flatRadians), - z: p.shaftLength - p.shaftFlatLength / 2, - }, - direction: { - x: Math.cos(flatRadians), - y: Math.sin(flatRadians), - z: 0, - }, - } - : undefined, + shaftflat: p.dShaft + ? { + position: { + x: (p.shaftDiameter / 2 - p.shaftFlatDepth) * Math.cos(flatRadians), + y: (p.shaftDiameter / 2 - p.shaftFlatDepth) * Math.sin(flatRadians), + z: p.shaftLength - p.shaftFlatLength / 2, + }, + direction: { + x: Math.cos(flatRadians), + y: Math.sin(flatRadians), + z: 0, + }, + } + : undefined, } satisfies Record } diff --git a/src/nema-motor-schema.ts b/src/nema-motor-schema.ts index c9924fc..04e6f1d 100644 --- a/src/nema-motor-schema.ts +++ b/src/nema-motor-schema.ts @@ -45,7 +45,8 @@ export const nemaMotorDimensions = { pilotLength: 1.5, shaftDiameter: 4, shaftLength: 15, - shaftShape: "round", + roundShaft: true, + dShaft: false, shaftFlatDepth: 0.5, shaftFlatLength: 10, shaftFlatAngle: 0, @@ -67,7 +68,8 @@ export const nemaMotorDimensions = { pilotLength: 2, shaftDiameter: 5, shaftLength: 24, - shaftShape: "d", + roundShaft: false, + dShaft: true, shaftFlatDepth: 0.5, shaftFlatLength: 15, shaftFlatAngle: 0, @@ -89,7 +91,8 @@ export const nemaMotorDimensions = { pilotLength: 1.6, shaftDiameter: 6.35, shaftLength: 20.6, - shaftShape: "d", + roundShaft: false, + dShaft: true, shaftFlatDepth: 0.5, shaftFlatLength: 15, shaftFlatAngle: 0, @@ -114,7 +117,8 @@ export const nemaMotorDimensions = { pilotLength: 1.6, shaftDiameter: 14, shaftLength: 32, - shaftShape: "round", + roundShaft: true, + dShaft: false, shaftFlatDepth: 0.5, shaftFlatLength: 15, shaftFlatAngle: 0, @@ -128,15 +132,18 @@ export const nemaMotorDimensions = { const shape = { nemaSize: nemaSizeSchema, /** Rear cap at Z=-bodyLength: bare, open bores or installed cap screws. */ - backFace: z.enum(["plain", "holes", "screws"]).optional(), + backFacePlain: z.boolean().optional(), + backFaceHoles: z.boolean().optional(), + backFaceScrews: z.boolean().optional(), backFaceHoleSpacing: positive.optional(), backFaceHoleDiameter: positive.optional(), backFaceHoleDepth: positive.optional(), backFaceScrewSize: metricBoltSizeSchema.optional(), /** Visual termination; the wireside reference exists even when hidden. */ - wireConnection: z - .union([z.enum(["none", "stubs"]), jstMotorWireConnectionSchema]) - .optional(), + noWires: z.boolean().optional(), + wireStubs: z.boolean().optional(), + /** JST catalog designation, mutually exclusive with the visible-wire flags. */ + wireConnection: jstMotorWireConnectionSchema.optional(), /** Counterclockwise angle about local +Z; 0 is the +X side. */ wireSideAngle: z.number().finite().optional(), wireLength: positive.optional(), @@ -153,7 +160,8 @@ const shape = { /** Distance from mounting face Z=0 to shaft tip, including pilot height. */ shaftLength: positive.optional(), shaftDiameter: positive.optional(), - shaftShape: z.enum(["round", "d"]).optional(), + roundShaft: z.boolean().optional(), + dShaft: z.boolean().optional(), /** Radial material removed from the +X side before rotation. */ shaftFlatDepth: nonnegative.optional(), /** Flat extends back from the tip; leaves a round shoulder above the pilot. */ @@ -166,21 +174,70 @@ const shape = { bodyCornerChamfer: nonnegative.optional(), /** Rear outline is independent of the front mounting flange. */ rearCornerChamfer: nonnegative.optional(), - cornerProfile: z.enum(["chamfered", "stepped"]).optional(), + chamfered: z.boolean().optional(), + stepped: z.boolean().optional(), } -const resolve = (p: z.output>) => { +const resolve = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const select = ( + flags: readonly K[], + fallback: K, + ) => { + const selected = flags.filter((flag) => p[flag] === true) + const specified = flags.some((flag) => p[flag] !== undefined) + if (selected.length > 1 || (specified && selected.length === 0)) + ctx.addIssue({ + code: "custom", + message: `Select exactly one of ${flags.join(", ")}`, + }) + return selected[0] ?? fallback + } + const back = select( + ["backFacePlain", "backFaceHoles", "backFaceScrews"], + p.nemaSize === 34 ? "backFacePlain" : "backFaceScrews", + ) + const shaft = select( + ["roundShaft", "dShaft"], + nemaMotorDimensions[p.nemaSize].roundShaft ? "roundShaft" : "dShaft", + ) + const corner = select( + ["chamfered", "stepped"], + p.nemaSize === 34 ? "stepped" : "chamfered", + ) + const wireSpecified = + p.noWires !== undefined || + p.wireStubs !== undefined || + p.wireConnection !== undefined + const wireSelections = + Number(p.noWires === true) + + Number(p.wireStubs === true) + + Number(p.wireConnection !== undefined) + if (wireSelections > 1 || (wireSpecified && wireSelections === 0)) + ctx.addIssue({ + code: "custom", + message: + "Select exactly one of noWires, wireStubs or a JST wireConnection", + }) const d = nemaMotorDimensions[p.nemaSize] const backFaceScrewSize = p.backFaceScrewSize ?? d.backFaceScrewSize const screw = hexSocketBoltDimensions[backFaceScrewSize] return { nemaSize: p.nemaSize, - wireConnection: p.wireConnection ?? "stubs", + noWires: p.noWires === true, + wireStubs: p.wireStubs === true || !wireSpecified, + ...(p.wireConnection !== undefined + ? { wireConnection: p.wireConnection } + : {}), 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 ?? (p.nemaSize === 34 ? "plain" : "screws"), + backFacePlain: back === "backFacePlain", + backFaceHoles: back === "backFaceHoles", + backFaceScrews: back === "backFaceScrews", backFaceHoleSpacing: p.backFaceHoleSpacing ?? ("backFaceHoleSpacing" in d @@ -199,7 +256,8 @@ const resolve = (p: z.output>) => { pilotLength: p.pilotLength ?? d.pilotLength, shaftDiameter: p.shaftDiameter ?? d.shaftDiameter, shaftLength: p.shaftLength ?? d.shaftLength, - shaftShape: p.shaftShape ?? d.shaftShape, + roundShaft: shaft === "roundShaft", + dShaft: shaft === "dShaft", shaftFlatDepth: p.shaftFlatDepth ?? d.shaftFlatDepth, shaftFlatLength: p.shaftFlatLength ?? d.shaftFlatLength, shaftFlatAngle: p.shaftFlatAngle ?? d.shaftFlatAngle, @@ -207,9 +265,8 @@ 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) } - : {}), + chamfered: corner === "chamfered", + stepped: corner === "stepped", ...("rearCornerChamfer" in d || p.rearCornerChamfer !== undefined ? { rearCornerChamfer: @@ -226,22 +283,21 @@ 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.backFacePlain && p.mountingHoleThrough ? p.bodyCornerChamfer : p.faceCornerChamfer) - const rearEdge = - p.cornerProfile === "stepped" ? rearChamfer / Math.SQRT2 : rearChamfer - if (p.backFace !== "plain") { + const rearEdge = p.stepped ? rearChamfer / Math.SQRT2 : rearChamfer + if (!p.backFacePlain) { const screw = hexSocketBoltDimensions[p.backFaceScrewSize] const radius = - (p.backFace === "screws" + (p.backFaceScrews ? Math.max(p.backFaceHoleDiameter, screw.headDiameter) : p.backFaceHoleDiameter) / 2 if (p.backFaceHoleSpacing <= 2 * radius) issue("Rear holes and screw heads must not overlap each other") if ( p.backFaceHoleSpacing + 2 * radius >= p.bodyWidth || - (p.cornerProfile === "stepped" + (p.stepped ? p.backFaceHoleSpacing + 2 * radius >= p.bodyWidth - 2 * rearEdge : p.bodyWidth - rearChamfer - p.backFaceHoleSpacing <= radius * Math.SQRT2) @@ -249,16 +305,19 @@ const validate = (p: NemaMotorModelProps, ctx: z.RefinementCtx) => { issue("Rear holes and screw heads must fit entirely inside the rear face") if (p.backFaceHoleDepth >= p.rearCapLength) issue("Rear hole depth must be less than the rear cap length") - if (p.backFace === "screws" && p.backFaceHoleDiameter < screw.diameter) + if (p.backFaceScrews && p.backFaceHoleDiameter < screw.diameter) issue("Rear holes must accommodate the screw diameter") } - if (p.wireConnection === "stubs") { + if (p.wireStubs) { 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) + const connector = + p.wireConnection === undefined + ? undefined + : getJstMotorConnector(p.wireConnection) if (connector && p.bodyWidth - 2 * rearEdge < connector.bodyWidth) issue("JST header must fit on the motor side") const r = p.mountingHoleDiameter / 2 @@ -285,7 +344,7 @@ const validate = (p: NemaMotorModelProps, ctx: z.RefinementCtx) => { issue("Blind mounting hole depth must be less than the front cap length") if ( p.mountingHoleThrough && - (p.cornerProfile === "stepped" + (p.stepped ? p.bodyWidth / 2 - p.bodyCornerChamfer / Math.SQRT2 + Math.min(4, p.bodyCornerChamfer / Math.SQRT2 / 4) >= @@ -297,7 +356,7 @@ const validate = (p: NemaMotorModelProps, ctx: z.RefinementCtx) => { if (p.shaftLength <= p.pilotLength) issue("Shaft tip must extend beyond the pilot") if ( - p.shaftShape === "d" && + p.dShaft && (p.shaftFlatDepth <= 0 || p.shaftFlatDepth >= p.shaftDiameter / 2 || p.shaftFlatLength <= 0 || @@ -316,7 +375,7 @@ export const nemaMotorModelPropsSchema = z export const nemaMotorModelDefinitionSchema = z .object({ fn: z.literal("nema"), ...shape }) .strict() - .transform(({ fn, ...props }) => ({ fn, ...resolve(props) })) + .transform(({ fn, ...props }, ctx) => ({ fn, ...resolve(props, ctx) })) .superRefine(validate) export type NemaMotorModelPropsInput = z.input export type NemaMotorModelDefinition = z.output< diff --git a/src/parse-flex-screen-model-string.ts b/src/parse-flex-screen-model-string.ts index 84ce07e..e4f4a94 100644 --- a/src/parse-flex-screen-model-string.ts +++ b/src/parse-flex-screen-model-string.ts @@ -2,11 +2,19 @@ import { flexScreenModelDefinitionSchema, type FlexScreenModelDefinition, type FlexScreenModelProps, - type FlexScreenOrientation, } from "./models/flexscreen/schema" import type { RawModelprinterParams } from "./parse-model-string" +import { splitModelStringTokens } from "./split-model-string-tokens" -const orientationTokens: Record = { +type OrientationFlag = + | "sitsFlat" + | "sitsFlatBelowBoard" + | "foldedToFaceAboveBoard" + | "foldedToFaceBelowBoard" + | "foldedToRightAngleAboveBoard" + | "foldedToRightAngleBelowBoard" + +const orientationTokens: Record = { sitsflat: "sitsFlat", sitsflatbelow: "sitsFlatBelowBoard", sitsflatbelowboard: "sitsFlatBelowBoard", @@ -148,7 +156,28 @@ export const parseFlexScreenModelParams = ( } const props: Record = {} - let orientation: FlexScreenOrientation | undefined + let orientation: OrientationFlag | undefined + // Raw parameter objects collapse repeated tokens; inspect the original string + // to reject repeated canonical flags and their aliases before that collapse. + for (const part of splitModelStringTokens(rawParams.string).slice(1)) { + const match = part.match(/^([a-zA-Z]+)(.*)$/) + if (!match) continue + const token = match[1]!.toLowerCase() + const selected = orientationTokens[token] + if (!selected) continue + if (match[2] !== "") { + throw new Error(`FlexScreen token "${token}" does not accept a value`) + } + if (orientation !== undefined) { + throw new Error( + orientation === selected + ? `Duplicate FlexScreen orientation flag "${token}"` + : "A FlexScreen model string can only set one orientation", + ) + } + orientation = selected + props[selected] = true + } let relativeDistance: unknown for (const [token, value] of Object.entries(rawParams)) { @@ -159,13 +188,6 @@ export const parseFlexScreenModelParams = ( const tokenOrientation = orientationTokens[token] if (tokenOrientation) { assertBareToken(value, token) - if (orientation && orientation !== tokenOrientation) { - throw new Error( - "A FlexScreen model string can only set one orientation", - ) - } - orientation = tokenOrientation - props.orientation = tokenOrientation continue } diff --git a/src/parse-helical-gear-model-string.ts b/src/parse-helical-gear-model-string.ts index 1db22df..4519bfe 100644 --- a/src/parse-helical-gear-model-string.ts +++ b/src/parse-helical-gear-model-string.ts @@ -31,7 +31,7 @@ export function parseHelicalGearModelParams(raw: RawModelprinterParams) { const [name, value] = parseGearToken(token) let property: string let parsed: unknown - if (name in lengths) { + if (Object.hasOwn(lengths, name)) { property = lengths[name as keyof typeof lengths] parsed = value } else if ( @@ -63,8 +63,8 @@ export function parseHelicalGearModelParams(raw: RawModelprinterParams) { } else if (name === "right" || name === "left") { if (value) throw new Error(`Helical flag "${name}" does not accept a value`) - property = "handedness" - parsed = name + property = "leftHanded" + parsed = name === "left" } else throw new Error(`Unknown helical gear token "${token}"`) if (property in props) throw new Error( diff --git a/src/parse-nema-motor-model-string.ts b/src/parse-nema-motor-model-string.ts index fd202f1..9447903 100644 --- a/src/parse-nema-motor-model-string.ts +++ b/src/parse-nema-motor-model-string.ts @@ -28,6 +28,20 @@ const lengths = { rearchamfer: "rearCornerChamfer", } as const +const flags = { + backfaceplain: "backFacePlain", + plainbackface: "backFacePlain", + backfaceholes: "backFaceHoles", + backfacescrews: "backFaceScrews", + round: "roundShaft", + roundshaft: "roundShaft", + dshaft: "dShaft", + chamfered: "chamfered", + stepped: "stepped", + nowires: "noWires", + wirestubs: "wireStubs", +} as const + export function parseNemaMotorModelParams(raw: RawModelprinterParams) { const tokens = normalizeNemaMotorModelString(raw.string).split("_") const match = tokens[0]?.match(/^nema(8|17|23|34)$/i) @@ -45,31 +59,15 @@ export function parseNemaMotorModelParams(raw: RawModelprinterParams) { if (name in lengths) { property = lengths[name as keyof typeof lengths] parsed = value - } else if ( - ["backfaceholes", "backfacescrews", "plainbackface"].includes(name) - ) { + } else if (name in flags) { if (value) throw new Error(`NEMA flag "${name}" does not accept a value`) - property = "backFace" - parsed = - name === "backfaceholes" - ? "holes" - : name === "backfacescrews" - ? "screws" - : "plain" - } else if (["wirestubs", "nowires", "jstph", "jstsh"].includes(name)) { - const isJst = name === "jstph" || name === "jstsh" - if ((isJst && !/^[1-9][0-9]*$/.test(value)) || (!isJst && value)) + property = flags[name as keyof typeof flags] + parsed = true + } else if (name === "jstph" || name === "jstsh") { + if (!/^[1-9][0-9]*$/.test(value)) throw new Error(`Invalid wire connection token "${token}"`) property = "wireConnection" - parsed = isJst - ? `jst-${name.slice(3)}-${value}` - : 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 + parsed = `jst-${name.slice(3)}-${value}` } else if (name === "wirecount") { property = "wireCount" if (!/^\d+$/.test(value)) throw new Error("wirecount requires an integer") @@ -82,20 +80,10 @@ export function parseNemaMotorModelParams(raw: RawModelprinterParams) { if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:deg)?$/i.test(value)) throw new Error(`${name} requires a numeric angle in degrees`) parsed = Number(value.replace(/deg$/i, "")) - } else if ( - ["round", "dshaft", "throughholes", "blindholes"].includes(name) - ) { + } else if (name === "throughholes" || name === "blindholes") { if (value) throw new Error(`NEMA flag "${name}" does not accept a value`) - property = - name === "round" || name === "dshaft" - ? "shaftShape" - : "mountingHoleThrough" - parsed = - name === "round" - ? "round" - : name === "dshaft" - ? "d" - : name === "throughholes" + property = "mountingHoleThrough" + parsed = name === "throughholes" } else throw new Error(`Unknown NEMA motor token "${token}"`) if (property in props) throw new Error(`NEMA property "${property}" is set more than once`) diff --git a/src/parse-sheet-metal-feature.ts b/src/parse-sheet-metal-feature.ts index 8d8ab71..decfd1d 100644 --- a/src/parse-sheet-metal-feature.ts +++ b/src/parse-sheet-metal-feature.ts @@ -8,7 +8,7 @@ import { /** Compile face-relative feature language to panel-local coordinates. */ export function parseSheetMetalFeature( feature: string, - profile: SheetMetalModelProps["profile"], + flags: Pick, ): SheetMetalHole { const match = feature.match(/^(hole|slot)[1-9]\d*\(([^()]+)\)$/) if (!match) throw new Error(`Invalid sheet metal feature "${feature}"`) @@ -43,10 +43,10 @@ export function parseSheetMetalFeature( throw new Error(`Specify exactly one face in ${feature}`) const face = faces[0]! if ( - (face === "angledface" && profile !== "angle") || - (["leftface", "rightface"].includes(face) && profile !== "channel") + (face === "angledface" && !flags.angle) || + (["leftface", "rightface"].includes(face) && !flags.channel) ) - throw new Error(`No ${face} on ${profile}`) + throw new Error(`No ${face} on the selected sheet metal form`) for (const [a, b] of [ ["leftofcenter", "rightofcenter"], ["abovecenter", "belowcenter"], @@ -72,7 +72,7 @@ export function parseSheetMetalFeature( throw new Error(`Hole requires r or d in ${feature}`) return sheetMetalHoleSchema.parse({ ...position, - shape: "round", + round: true, diameter: "r" in values ? 2 * distance("r") : distance("d"), }) } @@ -83,7 +83,7 @@ export function parseSheetMetalFeature( if (length < width) throw new Error("Slot length must be at least its width") return sheetMetalHoleSchema.parse({ ...position, - shape: "slot", + slot: true, length, width, axis: (panel === "base") === !("vertical" in values) ? "u" : "v", diff --git a/src/parse-sheet-metal-model-string.ts b/src/parse-sheet-metal-model-string.ts index 720e2c2..d43e162 100644 --- a/src/parse-sheet-metal-model-string.ts +++ b/src/parse-sheet-metal-model-string.ts @@ -25,8 +25,9 @@ export function parseSheetMetalModelParams(raw: RawModelprinterParams) { continue } if (["plate", "angle", "channel"].includes(token)) { - if (props.profile) throw new Error("Duplicate sheet metal profile") - props.profile = token + if (["plate", "angle", "channel"].some((flag) => flag in props)) + throw new Error("Duplicate sheet metal form flag") + props[token] = true continue } const key = lengths[token[0]!] @@ -40,7 +41,7 @@ export function parseSheetMetalModelParams(raw: RawModelprinterParams) { ...props, }) definition.holes = features.map((feature) => - parseSheetMetalFeature(feature, definition.profile), + parseSheetMetalFeature(feature, definition), ) return definition } diff --git a/src/parse-worm-gear-model-string.ts b/src/parse-worm-gear-model-string.ts index 301faf8..cd2e9b6 100644 --- a/src/parse-worm-gear-model-string.ts +++ b/src/parse-worm-gear-model-string.ts @@ -28,7 +28,7 @@ export function parseWormGearModelParams(raw: RawModelprinterParams) { const [name, value] = parseGearToken(token) let property: string let parsed: unknown - if (name in lengths) { + if (Object.hasOwn(lengths, name)) { property = lengths[name as keyof typeof lengths] parsed = value } else if (["starts", "segments", "turnsegments"].includes(name)) { @@ -44,8 +44,8 @@ export function parseWormGearModelParams(raw: RawModelprinterParams) { parsed = parseGearAngle(value, property) } else if (name === "right" || name === "left") { if (value) throw new Error(`Worm flag "${name}" does not accept a value`) - property = "handedness" - parsed = name + property = "leftHanded" + parsed = name === "left" } else throw new Error(`Unknown worm gear token "${token}"`) if (property in props) throw new Error(`Worm gear property "${property}" is set more than once`) diff --git a/src/sheet-metal-schema.ts b/src/sheet-metal-schema.ts index b3b8e26..a9f2889 100644 --- a/src/sheet-metal-schema.ts +++ b/src/sheet-metal-schema.ts @@ -11,18 +11,20 @@ const holePosition = { u: modelLengthSchema, v: modelLengthSchema, } -export const sheetMetalHoleSchema = z.discriminatedUnion("shape", [ +export const sheetMetalHoleSchema = z.union([ z .object({ ...holePosition, - shape: z.literal("round"), + round: z.literal(true), + slot: z.literal(false).default(false), diameter: positiveModelLengthSchema, }) .strict(), z .object({ ...holePosition, - shape: z.literal("slot"), + round: z.literal(false).default(false), + slot: z.literal(true), length: positiveModelLengthSchema, width: positiveModelLengthSchema, axis: z.enum(["u", "v"]).default("v"), @@ -30,7 +32,9 @@ export const sheetMetalHoleSchema = z.discriminatedUnion("shape", [ .strict(), ]) const sheetMetalShape = { - profile: z.enum(["plate", "angle", "channel"]).default("plate"), + plate: z.boolean().default(false), + angle: z.boolean().default(false), + channel: z.boolean().default(false), width: positiveModelLengthSchema, /** Straight base length between bend tangencies, not overall width. */ baseLength: positiveModelLengthSchema, @@ -40,10 +44,30 @@ const sheetMetalShape = { insideBendRadius: positiveModelLengthSchema.default(1), holes: z.array(sheetMetalHoleSchema).max(100).default([]), } -export const sheetMetalModelPropsSchema = z.object(sheetMetalShape).strict() +type InputProps = z.output> + +function validateFlags(props: InputProps, context: z.RefinementCtx) { + if ([props.plate, props.angle, props.channel].filter(Boolean).length > 1) + context.addIssue({ + code: "custom", + message: "Specify at most one of plate, angle, or channel", + }) +} + +function resolveFlags(props: InputProps) { + return { ...props, plate: props.plate || !(props.angle || props.channel) } +} + +export const sheetMetalModelPropsSchema = z + .object(sheetMetalShape) + .strict() + .superRefine(validateFlags) + .transform(resolveFlags) export const sheetMetalModelDefinitionSchema = z .object({ fn: z.literal("sheetmetal"), ...sheetMetalShape }) .strict() + .superRefine(validateFlags) + .transform((props) => ({ fn: props.fn, ...resolveFlags(props) })) export type SheetMetalModelPropsInput = z.input< typeof sheetMetalModelPropsSchema > diff --git a/src/utils/model-string-flags.ts b/src/utils/model-string-flags.ts deleted file mode 100644 index e627292..0000000 --- a/src/utils/model-string-flags.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - parseModelStringParams, - type RawModelprinterParams, -} from "../parse-model-string" -import { splitModelStringTokens } from "../split-model-string-tokens" - -type StringFlags = Readonly> - -/** Translate only complete, model-local flags; leave validation to the parser. */ -export function expandModelStringFlags( - value: string, - flags: StringFlags, -): string { - return splitModelStringTokens(value) - .map((token, index) => - index > 0 && Object.hasOwn(flags, token.toLowerCase()) - ? flags[token.toLowerCase()]! - : token, - ) - .join("_") -} - -/** Released syntax retains its raw params; new flags use the preferred spelling. */ -export function normalizeModelStringFlags( - value: string, - flags: StringFlags, - parse: (params: RawModelprinterParams) => unknown, - omittedFlags: readonly string[], -): string { - const tokens = splitModelStringTokens(value) - if ( - !tokens.slice(1).some((token) => Object.hasOwn(flags, token.toLowerCase())) - ) - return value - - // Validate before removing defaults, so conflicting or repeated selectors - // cannot disappear during normalization. - const expanded = expandModelStringFlags(value, flags) - parse(parseModelStringParams(expanded)) - const preferred = new Map( - Object.entries(flags).map(([flag, selector]) => [selector, flag]), - ) - const omitted = new Set(omittedFlags.map((flag) => flags[flag]!)) - return splitModelStringTokens(expanded) - .flatMap((token, index) => { - if (index === 0) return [token.toLowerCase()] - const lower = token.toLowerCase() - if (omitted.has(lower)) return [] - return [preferred.get(lower) ?? token] - }) - .join("_") -} diff --git a/src/worm-gear-schema.ts b/src/worm-gear-schema.ts index 6ae17f1..475d79c 100644 --- a/src/worm-gear-schema.ts +++ b/src/worm-gear-schema.ts @@ -18,7 +18,7 @@ const shape = { /** Additional radial dedendum beyond one module, in millimeters. */ clearance: nonnegative.default(0.25), boreDiameter: nonnegative.default(0), - handedness: z.enum(["right", "left"]).default("right"), + leftHanded: z.boolean().default(false), /** Counterclockwise degrees about +Z, beginning at the Z=0 end. */ phase: z.number().finite().default(0), radialSegments: z diff --git a/tests/balltransferunit.01-contract.test.ts b/tests/balltransferunit.01-contract.test.ts index 58b8107..a7a6fc6 100644 --- a/tests/balltransferunit.01-contract.test.ts +++ b/tests/balltransferunit.01-contract.test.ts @@ -13,14 +13,13 @@ import { ModelRegistry } from "../src/model-registry" import { parseModelStringParams } from "../src/parse-model-string" import { register } from "../src/models/balltransferunit/register" -test("balltransferunit roadmap and flag strings resolve to the complete contract", () => { +test("balltransferunit default and flag strings resolve to the complete contract", () => { const expected: BallTransferUnitModelDefinition = { fn: "balltransferunit", ...ballTransferUnitDefaults, } for (const source of [ "balltransferunit", - "balltransferunit_balld25mm_flangeod45mm_h30mm_mount(face3hole)_pcd36mm_holed4mm", "balltransferunit_balld25mm_flangeod45mm_h30mm_face3hole_pcd36mm_holed4mm", "BALLTRANSFERUNIT_BALLDIAMETER2.5CM_BODYDIAMETER3.1CM_HEIGHT3CM_BALLPROTRUSION7.5MM_FLANGEDIAMETER4.5CM_FLANGETHICKNESS3MM_PITCHCIRCLEDIAMETER3.6CM_HOLEDIAMETER4MM_SOCKETCLEARANCE0.25MM_FACE3HOLE", ]) { diff --git a/tests/balltransferunit.03-flags.test.ts b/tests/balltransferunit.03-flags.test.ts new file mode 100644 index 0000000..b5d09bb --- /dev/null +++ b/tests/balltransferunit.03-flags.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from "bun:test" +import { mp } from "../src" + +test("balltransferunit accepts value-free flags and rejects standalone enum selectors", () => { + const source = + "balltransferunit_balld25mm_flangeod45mm_h30mm_pcd36mm_holed4mm" + expect(mp.string(`${source}_face3hole`).json()).toEqual( + mp.string(source).json(), + ) + for (const token of [ + "mount(face3hole)", + "style(face3hole)", + "face3hole(true)", + "face3hole(false)", + "face3hole_face3hole", + ]) { + expect(() => mp.string(`${source}_${token}`).json(), token).toThrow() + } +}) diff --git a/tests/button-screw-boolean-flags.test.ts b/tests/button-screw-boolean-flags.test.ts new file mode 100644 index 0000000..c892228 --- /dev/null +++ b/tests/button-screw-boolean-flags.test.ts @@ -0,0 +1,80 @@ +import { expect, test } from "bun:test" +import { mp, buttonScrewModelPropsSchema } from "../src" +const source = "buttonscrew_m3_l10mm" + +test("buttonscrew directly exposes boolean selectors", () => { + const complete = source + "_hexsocket_fullthread_righthanded" + const builder = mp.string(complete) + expect(builder.params().string).toBe(complete) + const model = builder.json() + expect(model).toMatchObject({ + hexSocket: true, + fullThread: true, + leftHanded: false, + }) + for (const property of ["drive", "thread", "threadHand", "threadGender"]) + expect(model).not.toHaveProperty(property) + expect( + buttonScrewModelPropsSchema.parse({ + metricSize: "M3", + length: 10, + hexSocket: true, + fullThread: true, + leftHanded: false, + }), + ).toMatchObject({ hexSocket: true, fullThread: true, leftHanded: false }) +}) +test("buttonscrew boolean flags accept no values or repetitions", () => { + for (const tail of [ + "hexsocket_hexsocket", + "hexsocket(true)", + "hexsocket1", + "fullthread_fullthread", + "fullthread(true)", + "righthanded_righthanded", + "righthanded(false)", + "righthanded_lefthanded", + "lefthanded_righthanded", + "lefthanded_lefthanded", + ]) + expect(() => mp.string(source + "_" + tail).json()).toThrow() +}) +test("buttonscrew rejects deprecated string selectors and schema properties", () => { + for (const tail of [ + "drive(hexsocket)", + "thread(full)", + "threadhand(right)", + "threadhand(left)", + "threadgender(male)", + ]) + expect(() => mp.string(source + "_" + tail).json()).toThrow() + for (const invalid of [ + { drive: "hexsocket" }, + { thread: "full" }, + { threadHand: "right" }, + { threadGender: "male" }, + { hexSocket: false }, + { fullThread: false }, + { leftHanded: "left" }, + ]) + expect(() => + buttonScrewModelPropsSchema.parse({ + metricSize: "M3", + length: 10, + ...invalid, + }), + ).toThrow() +}) +test("buttonscrew supports only right handed threads", () => { + expect(mp.string(source + "_righthanded").json()).toMatchObject({ + leftHanded: false, + }) + expect(() => mp.string(source + "_lefthanded").json()).toThrow() + expect(() => + buttonScrewModelPropsSchema.parse({ + metricSize: "M3", + length: 10, + leftHanded: true, + }), + ).toThrow() +}) diff --git a/tests/button-screw.test.ts b/tests/button-screw.test.ts index 27b7d80..ee1efe3 100644 --- a/tests/button-screw.test.ts +++ b/tests/button-screw.test.ts @@ -20,10 +20,10 @@ test("button screw defaults to ISO 7380-1 dimensions and public registry", () => iso73801: true, metricSize: "M3", length: 10, - drive: "hexsocket", - thread: "full", + hexSocket: true, + fullThread: true, threadPitch: 0.5, - threadHand: "right", + leftHanded: false, threadClass: "6g", showThreads: true, }) diff --git a/tests/cable-grommet.test.ts b/tests/cable-grommet.test.ts index 297ffd7..0ae7d30 100644 --- a/tests/cable-grommet.test.ts +++ b/tests/cable-grommet.test.ts @@ -9,7 +9,7 @@ import { } from "../src" const example = - "cablegrommet_panelhole20mm_id10mm_od24mm_h8mm_groovew3mm_grooved2mm_shape(symmetricring)" + "cablegrommet_panelhole20mm_id10mm_od24mm_h8mm_groovew3mm_grooved2mm_symmetricring" test("cable grommet roadmap example, registry and union", () => { const builder = mp.string(example) @@ -27,7 +27,7 @@ test("cable grommet roadmap example, registry and union", () => { height: 8, grooveWidth: 3, grooveDepth: 2, - shape: "symmetricring", + symmetricRing: true, }) expect(modelDefinitionSchema.parse(model)).toEqual(model) expect(modelprinter.getModelNames()).toContain("cablegrommet") @@ -86,15 +86,15 @@ test("cable grommet rejects conflicting contracts and malformed strings", () => example.replace("id10mm", "id20mm"), example.replace("groovew3mm", "groovew8mm"), example.replace("grooved2mm", "grooved12mm"), - example.replace("shape(symmetricring)", "shape(split)"), - example.replace("shape(symmetricring)", "shapesymmetricring"), + example.replace("symmetricring", "shape(split)"), + example.replace("symmetricring", "shapesymmetricring"), example.replace("id10mm", "id"), example.replace("id10mm", "id1.0.1mm"), example.replace("id10mm", "id0mm"), example.replace("id10mm", "id-1mm"), example.replace("cablegrommet_", "cablegrommet2_"), `${example}_`, - example.replace("(symmetricring)", "((symmetricring))"), + example.replace("symmetricring", "symmetricring(true)"), ]) expect(() => mp.string(source).json()).toThrow() expect(() => @@ -127,3 +127,37 @@ test("cable grommet direct schema enforces dimensions and strictness", () => { cableGrommetModelPropsSchema.parse({ ...props, ...changes }), ).toThrow() }) + +test("cablegrommet uses a direct boolean flag and rejects legacy selectors", () => { + const base = + "cablegrommet_panelhole20mm_id10mm_od24mm_h8mm_groovew3mm_grooved2mm" + const omitted = mp.string(base).json() + if (omitted.fn !== "cablegrommet") throw new Error("Unexpected model") + expect(omitted.symmetricRing).toBe(true) + expect(omitted).not.toHaveProperty("shape") + expect(mp.string(base + "_symmetricring").json()).toEqual(omitted) + expect(mp.string((base + "_symmetricring").toUpperCase()).json()).toEqual( + omitted, + ) + expect(mp.string(base + "_symmetricring").params()).toMatchObject({ + symmetricring: true, + string: base + "_symmetricring", + }) + for (const suffix of [ + "symmetricring_symmetricring", + "symmetricring1", + "symmetricring(true)", + "symmetricring(false)", + "shape(symmetricring)", + "shape(unsupported)", + ]) + expect(() => mp.string(base + "_" + suffix).json()).toThrow() + const { fn, ...props } = omitted + for (const value of [false, "true", 1, null]) + expect(() => + cableGrommetModelPropsSchema.parse({ ...props, symmetricRing: value }), + ).toThrow() + expect(() => + cableGrommetModelPropsSchema.parse({ ...props, shape: "symmetricring" }), + ).toThrow() +}) diff --git a/tests/compression-spring.test.ts b/tests/compression-spring.test.ts index b0733ea..1b92ac5 100644 --- a/tests/compression-spring.test.ts +++ b/tests/compression-spring.test.ts @@ -17,7 +17,7 @@ const base = { test("compression spring roadmap example resolves defaults and custom dimensions", () => { const builder = mp.string( - "compressionspring_spec(custom)_od8mm_wire1mm_l20mm_turns8_active6_ends(closedground)_hand(right)_state(free)", + "compressionspring_custom_od8mm_wire1mm_l20mm_turns8_active6_closedground_righthanded_free", ) expect(builder.params()).toMatchObject({ od: "8mm", @@ -28,12 +28,12 @@ test("compression spring roadmap example resolves defaults and custom dimensions const model = builder.json() expect(model).toEqual({ fn: "compressionspring", - spec: "custom", + custom: true, ...base, activeTurns: 6, - ends: "closedground", - hand: "right", - state: "free", + closedGround: true, + leftHanded: false, + free: true, }) expect(modelDefinitionSchema.parse(model)).toEqual(model) expect(modelprinter.getModelNames()).toContain("compressionspring") @@ -58,9 +58,9 @@ test("compression spring roadmap example resolves defaults and custom dimensions test("compression spring units and mirrored winding preserve mounting datums", () => { expect( mp - .string("COMPRESSIONSPRING_OD0.8cm_WIRE0.1cm_LENGTH2cm_TURNS8_HAND(LEFT)") + .string("COMPRESSIONSPRING_OD0.8cm_WIRE0.1cm_LENGTH2cm_TURNS8_LEFTHANDED") .json(), - ).toMatchObject({ ...base, activeTurns: 6, hand: "left" }) + ).toMatchObject({ ...base, activeTurns: 6, leftHanded: true }) expect( compressionSpringModelPropsSchema.parse({ ...base, freeLength: "1in" }) .freeLength, @@ -78,7 +78,7 @@ test("compression spring units and mirrored winding preserve mounting datums", ( } const right = getCompressionSpringCenterlinePoint(base, 0.25) const left = getCompressionSpringCenterlinePoint( - { ...base, hand: "left" }, + { ...base, leftHanded: true }, 0.25, ) expect(right.y).toBeCloseTo(3.5) @@ -94,6 +94,22 @@ test("compression spring units and mirrored winding preserve mounting datums", ( test("compression spring validates closed-ground counts, bore and solid clearance", () => { const source = "compressionspring_od8mm_wire1mm_l20mm_turns8" for (const invalid of [ + ...[ + "spec(custom)", + "ends(closedground)", + "hand(right)", + "hand(left)", + "state(free)", + "custom_custom", + "free_free", + "closedground_closedground", + "lefthanded_righthanded", + "righthanded_lefthanded", + "lefthanded_lefthanded", + "righthanded_righthanded", + "lefthanded(true)", + "custom(true)", + ].map((token) => source + "_" + token), "compressionspring", "compressionspring_od8mm_wire1mm_l20mm", `${source}_active7`, @@ -118,6 +134,21 @@ test("compression spring validates closed-ground counts, bore and solid clearanc "compressionspring_od8mm_wire1mm_l20mmjunk_turns8", ]) expect(() => mp.string(invalid).json()).toThrow() + for (const legacy of [ + { spec: "custom" }, + { ends: "closedground" }, + { hand: "left" }, + { state: "free" }, + ]) + expect(() => + compressionSpringModelPropsSchema.parse({ ...base, ...legacy }), + ).toThrow() + expect(mp.string(source + "_righthanded").json()).toMatchObject({ + leftHanded: false, + }) + expect(mp.string(source + "_lefthanded").json()).toMatchObject({ + leftHanded: true, + }) for (const freeLength of [NaN, Infinity, 0, -1, 8, "20mm junk"]) expect(() => compressionSpringModelPropsSchema.parse({ ...base, freeLength }), diff --git a/tests/fixtures/assert-clamping-shaft-collar.ts b/tests/fixtures/assert-clamping-shaft-collar.ts index 6bd322b..112b709 100644 --- a/tests/fixtures/assert-clamping-shaft-collar.ts +++ b/tests/fixtures/assert-clamping-shaft-collar.ts @@ -9,7 +9,7 @@ import { } from "../../src" const example = - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4" + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4" const props = { boreDiameter: 8, outerDiameter: 18, @@ -28,9 +28,9 @@ export function assertClampingShaftCollarContract() { width: 9, splitWidth: 1, metricSize: "M4", - mount: "singleclamp", + singleClamp: true, threadPitch: 0.7, - threadHand: "right", + leftHanded: false, threadClass: "6H", screwZ: 4.5, clampX: 6.5, @@ -48,7 +48,7 @@ export function assertClampingShaftCollarContract() { expect( mp .string( - "CLAMPINGSHAFTCOLLAR_BORE0.8CM_OD1.8CM_W0.9CM_SPLIT0.1CM_MOUNT(SINGLECLAMP)_M4", + "CLAMPINGSHAFTCOLLAR_BORE0.8CM_OD1.8CM_W0.9CM_SPLIT0.1CM_singleclamp_M4", ) .json(), ).toEqual(model) @@ -59,10 +59,8 @@ export function assertClampingShaftCollarContract() { }).boreDiameter, ).toBeCloseTo(8) expect( - mp - .string(example + "_threadpitch0.5mm_threadhand(left)_threadclass(6H)") - .json(), - ).toMatchObject({ threadPitch: 0.5, threadHand: "left", threadClass: "6H" }) + mp.string(example + "_threadpitch0.5mm_lefthanded_threadclass(6H)").json(), + ).toMatchObject({ threadPitch: 0.5, leftHanded: true, threadClass: "6H" }) expect( clampingShaftCollarModelPropsSchema.parse({ ...props, metricSize: "M2.5" }) .threadPitch, @@ -88,7 +86,7 @@ export function assertClampingShaftCollarLayout() { direction: [0, 1, 0], diameter: 4, threadPitch: 0.7, - threadHand: "right", + leftHanded: false, threadGender: "female", }) expect( @@ -113,36 +111,61 @@ export function assertClampingShaftCollarLayout() { } export function assertClampingShaftCollarValidation() { + const source = "clampingshaftcollar_" + "bore8mm_od18mm_w9mm_split1mm_m4" + for (const token of [ + "mount(singleclamp)", + "threadhand(left)", + "threadhand(right)", + "singleclamp_singleclamp", + "lefthanded_righthanded", + "righthanded_lefthanded", + "lefthanded_lefthanded", + "righthanded_righthanded", + "singleclamp(true)", + "lefthanded(true)", + ]) + expect(() => mp.string(source + "_" + token).json()).toThrow() + expect(mp.string(source + "_righthanded").json()).toMatchObject({ + leftHanded: false, + }) + expect(mp.string(source + "_lefthanded").json()).toMatchObject({ + leftHanded: true, + }) + for (const legacy of [{ mount: "singleclamp" }, { threadHand: "left" }]) + expect(() => + clampingShaftCollarModelPropsSchema.parse({ ...props, ...legacy }), + ).toThrow() + for (const source of [ - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_typo1mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_bore9mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_m3", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_mount(unknown)", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_threadpitch4mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_threadhand(opposed)", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_threadclass(6g)", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_threadpitch0mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_threadpitch0.7oops", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_threadpitch1e-3mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_mountsetscrew", - "clampingshaftcollar_bore_od18mm_w9mm_split1mm_mount(singleclamp)_m4", - "clampingshaftcollar_bore0mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4", - "clampingshaftcollar_bore20mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4", - "clampingshaftcollar_bore8mmoops_od18mm_w9mm_split1mm_mount(singleclamp)_m4", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m7", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_threadpitch(0.7mm)", - "clampingshaftcollar(custom)_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_typo1mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_bore9mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_m3", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_mount(unknown)", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_threadpitch4mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_threadhand(opposed)", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_threadclass(6g)", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_threadpitch0mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_threadpitch0.7oops", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_threadpitch1e-3mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_mountsetscrew", + "clampingshaftcollar_bore_od18mm_w9mm_split1mm_singleclamp_m4", + "clampingshaftcollar_bore0mm_od18mm_w9mm_split1mm_singleclamp_m4", + "clampingshaftcollar_bore20mm_od18mm_w9mm_split1mm_singleclamp_m4", + "clampingshaftcollar_bore8mmoops_od18mm_w9mm_split1mm_singleclamp_m4", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m7", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_threadpitch(0.7mm)", + "clampingshaftcollar(custom)_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4", "clampingshaftcollar", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_width9mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_screwz1mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_clampx5mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_clampx8mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_clearance4mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_clearance6mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_mount(singleclamp)_m4_chamfer3mm", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split8mm_mount(singleclamp)_m4", - "clampingshaftcollar_bore8mm_od18mm_w9mm_split7mm_mount(singleclamp)_m4", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_width9mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_screwz1mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_clampx5mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_clampx8mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_clearance4mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_clearance6mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_singleclamp_m4_chamfer3mm", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split8mm_singleclamp_m4", + "clampingshaftcollar_bore8mm_od18mm_w9mm_split7mm_singleclamp_m4", ]) expect(() => mp.string(source).json()).toThrow() for (const boreDiameter of [ diff --git a/tests/fixtures/assert-flat-head-screw.ts b/tests/fixtures/assert-flat-head-screw.ts index 4ab9ac7..6224c98 100644 --- a/tests/fixtures/assert-flat-head-screw.ts +++ b/tests/fixtures/assert-flat-head-screw.ts @@ -10,7 +10,7 @@ import { import { parseFlatHeadScrewModelParams } from "../../src/models/flatheadscrew/parse-model-string" export const assertFlatHeadScrew = () => { - const builder = mp.string("flatheadscrew_m3_l10mm_drive(hexsocket)") + const builder = mp.string("flatheadscrew_m3_l10mm_hexsocket") const model = builder.json() if (model.fn !== "flatheadscrew") throw new Error("Unexpected model family") expect(model).toMatchObject({ @@ -31,11 +31,11 @@ export const assertFlatHeadScrew = () => { iso10642: true, metricSize: "M3", length: 10, - thread: "full", - drive: "hexsocket", - threadHand: "right", + fullThread: true, + hexSocket: true, + leftHanded: false, threadClass: "6g", - threadGender: "male", + male: true, showThreads: true, ...flatHeadScrewDimensions.M3, @@ -44,22 +44,22 @@ export const assertFlatHeadScrew = () => { expect(flatHeadScrewModelPropsSchema.parse(props)).toEqual(props) expect(model).not.toHaveProperty("standard") expect(model).not.toHaveProperty("headStandard") - expect( - mp.string("flatheadscrew_iso10642_m3_l10mm_drive(hexsocket)").json(), - ).toEqual(model) + expect(mp.string("flatheadscrew_iso10642_m3_l10mm_hexsocket").json()).toEqual( + model, + ) expect(modelDefinitionSchema.parse(model)).toEqual(model) expect(flatHeadScrewModelDefinitionSchema.parse(model)).toEqual(model) expect(modelprinter.getModelNames()).toContain("flatheadscrew") expect( - mp.string("FLATHEADSCREW_M3_length1.0cm_ISO10642_DRIVE(HEXSOCKET)").json(), + mp.string("FLATHEADSCREW_M3_length1.0cm_ISO10642_HEXSOCKET").json(), ).toEqual(model) expect( mp .string( - "flatheadscrew_metricsize(m3)_l10mm_threadpitch0.5mm_threadhand(left)_nothreads", + "flatheadscrew_metricsize(m3)_l10mm_threadpitch0.5mm_lefthanded_nothreads", ) .json(), - ).toMatchObject({ threadHand: "left", showThreads: false }) + ).toMatchObject({ leftHanded: true, showThreads: false }) for (const metricSize of ["M3", "M4", "M5", "M6"] as const) { const parsed = flatHeadScrewModelPropsSchema.parse({ diff --git a/tests/fixtures/assert-flex-screen-pitch.ts b/tests/fixtures/assert-flex-screen-pitch.ts index 7fba32f..9fd8677 100644 --- a/tests/fixtures/assert-flex-screen-pitch.ts +++ b/tests/fixtures/assert-flex-screen-pitch.ts @@ -19,6 +19,12 @@ export const assertFlexScreenPitch = () => { padLength: 1.25, tailLength: 3, taperLength: 2, + sitsFlat: true, + sitsFlatBelowBoard: false, + foldedToFaceAboveBoard: false, + foldedToFaceBelowBoard: false, + foldedToRightAngleAboveBoard: false, + foldedToRightAngleBelowBoard: false, }) expect(() => flexScreenModelPropsSchema.parse({ @@ -36,7 +42,12 @@ export const assertFlexScreenPitch = () => { width: 16, height: 10, flexCableLength: 5, - orientation: "sitsFlat", + sitsFlat: true, + sitsFlatBelowBoard: false, + foldedToFaceAboveBoard: false, + foldedToFaceBelowBoard: false, + foldedToRightAngleAboveBoard: false, + foldedToRightAngleBelowBoard: false, }) expect(mp.string("flexscreen_p0.02in_pincount30").json()).toMatchObject({ pinCount: 30, @@ -47,7 +58,17 @@ export const assertFlexScreenPitch = () => { ) expect( mp.string("flexscreen_conductorcount30_conductorpitch0.5").json(), - ).toEqual({ fn: "flexscreen", conductorCount: 30, conductorPitch: 0.5 }) + ).toEqual({ + fn: "flexscreen", + conductorCount: 30, + conductorPitch: 0.5, + sitsFlat: true, + sitsFlatBelowBoard: false, + foldedToFaceAboveBoard: false, + foldedToFaceBelowBoard: false, + foldedToRightAngleAboveBoard: false, + foldedToRightAngleBelowBoard: false, + }) expect( mp.string("flexscreen30_p0.5mm_pw0.3mm_pl1.25mm_tail3mm_taper2mm").json(), ).toEqual({ @@ -58,6 +79,12 @@ export const assertFlexScreenPitch = () => { padLength: 1.25, tailLength: 3, taperLength: 2, + sitsFlat: true, + sitsFlatBelowBoard: false, + foldedToFaceAboveBoard: false, + foldedToFaceBelowBoard: false, + foldedToRightAngleAboveBoard: false, + foldedToRightAngleBelowBoard: false, }) expect( mp @@ -71,7 +98,12 @@ export const assertFlexScreenPitch = () => { width: 16, height: 10, flexCableLength: 5, - orientation: "sitsFlat", + sitsFlat: true, + sitsFlatBelowBoard: false, + foldedToFaceAboveBoard: false, + foldedToFaceBelowBoard: false, + foldedToRightAngleAboveBoard: false, + foldedToRightAngleBelowBoard: false, }) for (const definition of [ "flexscreen0_p0.5", diff --git a/tests/fixtures/assert-gears.ts b/tests/fixtures/assert-gears.ts index 42758a6..fcc773a 100644 --- a/tests/fixtures/assert-gears.ts +++ b/tests/fixtures/assert-gears.ts @@ -154,7 +154,7 @@ export function assertWormGears() { backlash: 0, clearance: 0.25, boreDiameter: 0, - handedness: "right" as const, + leftHanded: false, phase: 0, radialSegments: 96, segmentsPerTurn: 32, @@ -173,7 +173,7 @@ export function assertWormGears() { fn: "wormgear", ...defaults, starts: 2, - handedness: "left", + leftHanded: true, boreDiameter: 3, backlash: 0.1, phase: 15, @@ -190,7 +190,7 @@ export function assertWormGears() { module: 1, pitchDiameter: 10, length: 12.7, - handedness: "right", + leftHanded: false, }) const d = getWormGearDimensions({ module: "2mm", diff --git a/tests/fixtures/assert-hex-bolt.ts b/tests/fixtures/assert-hex-bolt.ts index ad0387c..3c1b855 100644 --- a/tests/fixtures/assert-hex-bolt.ts +++ b/tests/fixtures/assert-hex-bolt.ts @@ -10,7 +10,7 @@ import { import { parseHexBoltModelParams } from "../../src/models/hexbolt/parse-model-string" export const assertHexBolt = () => { - const builder = mp.string("hexbolt_m6_l25mm_thread(full)_drive(hex)") + const builder = mp.string("hexbolt_m6_l25mm_fullthread_hex") const model = builder.json() if (model.fn !== "hexbolt") throw new Error("Unexpected model family") expect(model).toMatchObject({ @@ -26,11 +26,11 @@ export const assertHexBolt = () => { iso4017: true, metricSize: "M6", length: 25, - thread: "full", - drive: "hex", - threadHand: "right", + fullThread: true, + hex: true, + leftHanded: false, threadClass: "6g", - threadGender: "male", + male: true, showThreads: true, ...hexBoltDimensions.M6, @@ -43,14 +43,14 @@ export const assertHexBolt = () => { expect(modelDefinitionSchema.parse(model)).toEqual(model) expect(hexBoltModelDefinitionSchema.parse(model)).toEqual(model) expect(modelprinter.getModelNames()).toContain("hexbolt") - expect(mp.string("HEXBOLT_M6_length2.5cm_DRIVE(HEX)").json()).toEqual(model) + expect(mp.string("HEXBOLT_M6_length2.5cm_HEX").json()).toEqual(model) expect( mp .string( - "hexbolt_metricsize(m6)_l25mm_threadpitch1mm_threadhand(left)_nothreads", + "hexbolt_metricsize(m6)_l25mm_threadpitch1mm_lefthanded_nothreads", ) .json(), - ).toMatchObject({ threadHand: "left", showThreads: false }) + ).toMatchObject({ leftHanded: true, showThreads: false }) for (const metricSize of ["M3", "M4", "M5", "M6"] as const) { const parsed = hexBoltModelPropsSchema.parse({ diff --git a/tests/fixtures/assert-modelprinter.ts b/tests/fixtures/assert-modelprinter.ts index 1bb5e5b..7d950fb 100644 --- a/tests/fixtures/assert-modelprinter.ts +++ b/tests/fixtures/assert-modelprinter.ts @@ -77,7 +77,12 @@ export function assertModelprinter() { width: 40, height: 22.5, flexCableLength: 60, - orientation: "foldedToFaceAboveBoard", + sitsFlat: false, + sitsFlatBelowBoard: false, + foldedToFaceAboveBoard: true, + foldedToFaceBelowBoard: false, + foldedToRightAngleAboveBoard: false, + foldedToRightAngleBelowBoard: false, distanceAboveBoard: 20, foldDistanceFromConnector: 9, foldOutset: 6, diff --git a/tests/fixtures/assert-nema-motor.ts b/tests/fixtures/assert-nema-motor.ts index 167a78f..e11b664 100644 --- a/tests/fixtures/assert-nema-motor.ts +++ b/tests/fixtures/assert-nema-motor.ts @@ -20,18 +20,20 @@ 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(size === 34 ? "plain" : "screws") + expect(props.backFacePlain).toBe(size === 34) + expect(props.backFaceHoles).toBe(false) + expect(props.backFaceScrews).toBe(size !== 34) expect(props.backFaceHoleSpacing).toBe(size === 34 ? 40 : expected.pitch) expect(props.backFaceScrewSize).toBe( ({ 8: "M2", 17: "M3", 23: "M4", 34: "M5" } as const)[size], ) for (const [token, mode] of [ - ["backfaceholes", "holes"], - ["backfacescrews", "screws"], - ["plainbackface", "plain"], - ]) + ["backfaceholes", "backFaceHoles"], + ["backfacescrews", "backFaceScrews"], + ["plainbackface", "backFacePlain"], + ] as const) expect(mp.string(`nema${size}_${token}`).json()).toMatchObject({ - backFace: mode, + [mode]: true, }) expect( mp @@ -40,7 +42,7 @@ export function assertNemaMotor(size: NemaSize) { ) .json(), ).toMatchObject({ - backFace: "holes", + backFaceHoles: true, backFaceHoleSpacing: 12, backFaceHoleDiameter: 2, backFaceHoleDepth: 1, @@ -48,7 +50,7 @@ export function assertNemaMotor(size: NemaSize) { }) expect( modelDefinitionSchema.parse(mp.string(`nema${size}_backfaceholes`).json()), - ).toMatchObject({ backFace: "holes" }) + ).toMatchObject({ backFaceHoles: true }) expect(props.bodyLength).toBe(expected.length) expect(props.shaftLength).toBe(expected.shaft) expect(props.shaftFlatLength).toBe(size === 8 ? 10 : 15) @@ -57,14 +59,14 @@ export function assertNemaMotor(size: NemaSize) { .string(`nema${size}_dshaft_flatdepth0.5mm_flatlength8mm_flatangle90deg`) .json(), ).toMatchObject({ - shaftShape: "d", + dShaft: true, shaftFlatDepth: 0.5, shaftFlatLength: 8, shaftFlatAngle: 90, }) expect(mp.string(`nema${size}_l6cm_round`).json()).toMatchObject({ bodyLength: 60, - shaftShape: "round", + roundShaft: true, }) expect( mp.string("nema8_holespacing15.4mm_pilotdiameter16mm").json(), @@ -101,7 +103,7 @@ export function assertNemaMotor(size: NemaSize) { { bodyLength: 8 }, { bodyWidth: 20 }, { pilotDiameter: 4 }, - { shaftShape: "d", shaftFlatDepth: 0 }, + { dShaft: true, shaftFlatDepth: 0 }, { mountingHoleSpacing: 1 }, { mystery: true }, { backFace: "threads" }, diff --git a/tests/fixtures/assert-pan-screw.ts b/tests/fixtures/assert-pan-screw.ts index 123d9d1..eb60a33 100644 --- a/tests/fixtures/assert-pan-screw.ts +++ b/tests/fixtures/assert-pan-screw.ts @@ -10,7 +10,7 @@ import { import { parsePanScrewModelParams } from "../../src/models/panscrew/parse-model-string" export const assertPanScrew = () => { - const builder = mp.string("panscrew_m3_l10mm_drive(phillips)") + const builder = mp.string("panscrew_m3_l10mm_phillips") const model = builder.json() if (model.fn !== "panscrew") throw new Error("Unexpected model family") expect(model).toMatchObject({ @@ -29,11 +29,11 @@ export const assertPanScrew = () => { iso7045: true, metricSize: "M3", length: 10, - thread: "full", - drive: "phillips", - threadHand: "right", + fullThread: true, + phillips: true, + leftHanded: false, threadClass: "6g", - threadGender: "male", + male: true, showThreads: true, iso4757: true, recessType: "H", @@ -52,16 +52,14 @@ export const assertPanScrew = () => { expect(modelDefinitionSchema.parse(model)).toEqual(model) expect(panScrewModelDefinitionSchema.parse(model)).toEqual(model) expect(modelprinter.getModelNames()).toContain("panscrew") - expect(mp.string("PANSCREW_M3_length1.0cm_DRIVE(PHILLIPS)").json()).toEqual( - model, - ) + expect(mp.string("PANSCREW_M3_length1.0cm_PHILLIPS").json()).toEqual(model) expect( mp .string( - "panscrew_metricsize(m3)_l10mm_threadpitch0.5mm_threadhand(left)_nothreads", + "panscrew_metricsize(m3)_l10mm_threadpitch0.5mm_lefthanded_nothreads", ) .json(), - ).toMatchObject({ threadHand: "left", showThreads: false }) + ).toMatchObject({ leftHanded: true, showThreads: false }) for (const metricSize of ["M3", "M4", "M5", "M6"] as const) { const parsed = panScrewModelPropsSchema.parse({ diff --git a/tests/fixtures/assert-rigid-coupler.ts b/tests/fixtures/assert-rigid-coupler.ts index 9862e0e..8ae9507 100644 --- a/tests/fixtures/assert-rigid-coupler.ts +++ b/tests/fixtures/assert-rigid-coupler.ts @@ -8,8 +8,7 @@ import { mp, } from "../../src" -const example = - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4" +const example = "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4" const props = { boreDiameter: 8, outerDiameter: 20, @@ -28,9 +27,9 @@ export function assertRigidCouplerContract() { length: 25, screwCount: 4, metricSize: "M4", - mount: "setscrew", + setScrew: true, threadPitch: 0.7, - threadHand: "right", + leftHanded: false, threadClass: "6H", screwEndOffset: 6.25, screwAngle: 0, @@ -46,9 +45,7 @@ export function assertRigidCouplerContract() { expect(modelDefinitionSchema.parse(model)).toEqual(model) expect( mp - .string( - "RIGIDCOUPLER_BORE0.8CM_OD2CM_L2.5CM_MOUNT(SETSCREW)_SCREWCOUNT4_M4", - ) + .string("RIGIDCOUPLER_BORE0.8CM_OD2CM_L2.5CM_setscrew_SCREWCOUNT4_M4") .json(), ).toEqual(model) expect( @@ -58,10 +55,8 @@ export function assertRigidCouplerContract() { }).boreDiameter, ).toBeCloseTo(8) expect( - mp - .string(example + "_threadpitch0.5mm_threadhand(left)_threadclass(6H)") - .json(), - ).toMatchObject({ threadPitch: 0.5, threadHand: "left", threadClass: "6H" }) + mp.string(example + "_threadpitch0.5mm_lefthanded_threadclass(6H)").json(), + ).toMatchObject({ threadPitch: 0.5, leftHanded: true, threadClass: "6H" }) expect( rigidCouplerModelPropsSchema.parse({ ...props, metricSize: "M2.5" }) .threadPitch, @@ -79,7 +74,7 @@ export function assertRigidCouplerLayout() { direction: [-1, -0, 0], diameter: 4, threadPitch: 0.7, - threadHand: "right", + leftHanded: false, threadGender: "female", }) expect(b0?.start).toEqual([10, 0, 18.75]) @@ -115,6 +110,31 @@ export function assertRigidCouplerLayout() { } export function assertRigidCouplerValidation() { + const source = "rigidcoupler_" + "bore8mm_od20mm_l25mm_m4" + for (const token of [ + "mount(setscrew)", + "threadhand(left)", + "threadhand(right)", + "setscrew_setscrew", + "lefthanded_righthanded", + "righthanded_lefthanded", + "lefthanded_lefthanded", + "righthanded_righthanded", + "setscrew(true)", + "lefthanded(true)", + ]) + expect(() => mp.string(source + "_" + token).json()).toThrow() + expect(mp.string(source + "_righthanded").json()).toMatchObject({ + leftHanded: false, + }) + expect(mp.string(source + "_lefthanded").json()).toMatchObject({ + leftHanded: true, + }) + for (const legacy of [{ mount: "setscrew" }, { threadHand: "left" }]) + expect(() => + rigidCouplerModelPropsSchema.parse({ ...props, ...legacy }), + ).toThrow() + for (const boreDiameter of [4.1, 5.6, Math.SQRT2 * 4]) { expect(() => rigidCouplerModelPropsSchema.parse({ ...props, boreDiameter }), @@ -135,37 +155,37 @@ export function assertRigidCouplerValidation() { ).not.toThrow() for (const source of [ - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_typo1mm", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_bore9mm", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_m3", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_mount(unknown)", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_threadpitch4mm", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_threadhand(opposed)", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_threadclass(6g)", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_threadpitch0mm", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_threadpitch0.7oops", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_threadpitch1e-3mm", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_mountsetscrew", - "rigidcoupler_bore_od20mm_l25mm_mount(setscrew)_screwcount4_m4", - "rigidcoupler_bore0mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4", - "rigidcoupler_bore20mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4", - "rigidcoupler_bore8mmoops_od20mm_l25mm_mount(setscrew)_screwcount4_m4", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m7", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4mm", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_threadpitch(0.7mm)", - "rigidcoupler(custom)_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_typo1mm", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_bore9mm", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_m3", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_mount(unknown)", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_threadpitch4mm", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_threadhand(opposed)", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_threadclass(6g)", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_threadpitch0mm", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_threadpitch0.7oops", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_threadpitch1e-3mm", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_mountsetscrew", + "rigidcoupler_bore_od20mm_l25mm_setscrew_screwcount4_m4", + "rigidcoupler_bore0mm_od20mm_l25mm_setscrew_screwcount4_m4", + "rigidcoupler_bore20mm_od20mm_l25mm_setscrew_screwcount4_m4", + "rigidcoupler_bore8mmoops_od20mm_l25mm_setscrew_screwcount4_m4", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m7", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4mm", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_threadpitch(0.7mm)", + "rigidcoupler(custom)_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4", "rigidcoupler", - "rigidcoupler_bore4.1mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4", - "rigidcoupler_bore8mm_boreb5.6mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_length30mm", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_screwcount2", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_screwendoffset1mm", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_screwendoffset12mm", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_boreb20mm", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_boreb4mm", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4_m4_screwangle90rad", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount3_m4", - "rigidcoupler_bore8mm_od20mm_l25mm_mount(setscrew)_screwcount4mm_m4", + "rigidcoupler_bore4.1mm_od20mm_l25mm_setscrew_screwcount4_m4", + "rigidcoupler_bore8mm_boreb5.6mm_od20mm_l25mm_setscrew_screwcount4_m4", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_length30mm", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_screwcount2", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_screwendoffset1mm", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_screwendoffset12mm", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_boreb20mm", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_boreb4mm", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4_m4_screwangle90rad", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount3_m4", + "rigidcoupler_bore8mm_od20mm_l25mm_setscrew_screwcount4mm_m4", ]) expect(() => mp.string(source).json()).toThrow() for (const boreDiameter of [ diff --git a/tests/fixtures/assert-shaft-collar.ts b/tests/fixtures/assert-shaft-collar.ts index 816805b..e8db68b 100644 --- a/tests/fixtures/assert-shaft-collar.ts +++ b/tests/fixtures/assert-shaft-collar.ts @@ -8,7 +8,7 @@ import { mp, } from "../../src" -const example = "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4" +const example = "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4" const props = { boreDiameter: 8, outerDiameter: 16, @@ -25,9 +25,9 @@ export function assertShaftCollarContract() { outerDiameter: 16, width: 8, metricSize: "M4", - mount: "setscrew", + setScrew: true, threadPitch: 0.7, - threadHand: "right", + leftHanded: false, threadClass: "6H", screwZ: 4, screwAngle: 0, @@ -42,7 +42,7 @@ export function assertShaftCollarContract() { expect(shaftCollarModelDefinitionSchema.parse(model)).toEqual(model) expect(modelDefinitionSchema.parse(model)).toEqual(model) expect( - mp.string("SHAFTCOLLAR_BORE0.8CM_OD1.6CM_W0.8CM_MOUNT(SETSCREW)_M4").json(), + mp.string("SHAFTCOLLAR_BORE0.8CM_OD1.6CM_W0.8CM_setscrew_M4").json(), ).toEqual(model) expect( shaftCollarModelPropsSchema.parse({ @@ -51,10 +51,8 @@ export function assertShaftCollarContract() { }).boreDiameter, ).toBeCloseTo(8) expect( - mp - .string(example + "_threadpitch0.5mm_threadhand(left)_threadclass(6H)") - .json(), - ).toMatchObject({ threadPitch: 0.5, threadHand: "left", threadClass: "6H" }) + mp.string(example + "_threadpitch0.5mm_lefthanded_threadclass(6H)").json(), + ).toMatchObject({ threadPitch: 0.5, leftHanded: true, threadClass: "6H" }) expect( shaftCollarModelPropsSchema.parse({ ...props, metricSize: "M2.5" }) .threadPitch, @@ -69,7 +67,7 @@ export function assertShaftCollarLayout() { direction: [-1, -0, 0], diameter: 4, threadPitch: 0.7, - threadHand: "right", + leftHanded: false, threadGender: "female", threadClass: "6H", }) @@ -94,33 +92,58 @@ export function assertShaftCollarLayout() { } export function assertShaftCollarValidation() { + const source = "shaftcollar_" + "bore8mm_od16mm_w8mm_m4" + for (const token of [ + "mount(setscrew)", + "threadhand(left)", + "threadhand(right)", + "setscrew_setscrew", + "lefthanded_righthanded", + "righthanded_lefthanded", + "lefthanded_lefthanded", + "righthanded_righthanded", + "setscrew(true)", + "lefthanded(true)", + ]) + expect(() => mp.string(source + "_" + token).json()).toThrow() + expect(mp.string(source + "_righthanded").json()).toMatchObject({ + leftHanded: false, + }) + expect(mp.string(source + "_lefthanded").json()).toMatchObject({ + leftHanded: true, + }) + for (const legacy of [{ mount: "setscrew" }, { threadHand: "left" }]) + expect(() => + shaftCollarModelPropsSchema.parse({ ...props, ...legacy }), + ).toThrow() + for (const source of [ - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_typo1mm", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_bore9mm", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_m3", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_mount(unknown)", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_threadpitch4mm", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_threadhand(opposed)", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_threadclass(6g)", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_threadpitch0mm", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_threadpitch0.7oops", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_threadpitch1e-3mm", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_mountsetscrew", - "shaftcollar_bore_od16mm_w8mm_mount(setscrew)_m4", - "shaftcollar_bore0mm_od16mm_w8mm_mount(setscrew)_m4", - "shaftcollar_bore20mm_od16mm_w8mm_mount(setscrew)_m4", - "shaftcollar_bore8mmoops_od16mm_w8mm_mount(setscrew)_m4", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m7", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4mm", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_threadpitch(0.7mm)", - "shaftcollar(custom)_bore8mm_od16mm_w8mm_mount(setscrew)_m4", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_typo1mm", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_bore9mm", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_m3", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_mount(unknown)", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_threadpitch4mm", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_threadhand(opposed)", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_threadclass(6g)", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_threadpitch0mm", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_threadpitch0.7oops", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_threadpitch1e-3mm", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_mountsetscrew", + "shaftcollar_bore_od16mm_w8mm_setscrew_m4", + "shaftcollar_bore0mm_od16mm_w8mm_setscrew_m4", + "shaftcollar_bore20mm_od16mm_w8mm_setscrew_m4", + "shaftcollar_bore8mmoops_od16mm_w8mm_setscrew_m4", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m7", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4mm", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_threadpitch(0.7mm)", + "shaftcollar(custom)_bore8mm_od16mm_w8mm_setscrew_m4", "shaftcollar", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_width9mm", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_screwz1mm", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_screwz7mm", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_chamfer2mm", - "shaftcollar_bore8mm_od16mm_w8mm_mount(setscrew)_m4_screwangle90rad", - "shaftcollar_bore8mm_od16mm_w4mm_mount(setscrew)_m4", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_width9mm", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_screwz1mm", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_screwz7mm", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_chamfer2mm", + "shaftcollar_bore8mm_od16mm_w8mm_setscrew_m4_screwangle90rad", + "shaftcollar_bore8mm_od16mm_w4mm_setscrew_m4", ]) expect(() => mp.string(source).json()).toThrow() for (const boreDiameter of [ diff --git a/tests/fixtures/assert-sheet-metal.ts b/tests/fixtures/assert-sheet-metal.ts index 0af63c6..aad43dc 100644 --- a/tests/fixtures/assert-sheet-metal.ts +++ b/tests/fixtures/assert-sheet-metal.ts @@ -6,7 +6,9 @@ export function assertSheetMetal() { mp.string("sheetmetal_channel_w28mm_l24mm_h14mm_t1mm_r2mm").json(), ).toMatchObject({ fn: "sheetmetal", - profile: "channel", + plate: false, + angle: false, + channel: true, width: 28, baseLength: 24, }) @@ -15,7 +17,9 @@ export function assertSheetMetal() { .string("sheetmetal_plate_w20_l20_hole1(r3mm_bottomface_leftofcenter3mm)") .json(), ).toMatchObject({ - holes: [{ panel: "base", shape: "round", diameter: 6, u: -3, v: 0 }], + holes: [ + { panel: "base", round: true, slot: false, diameter: 6, u: -3, v: 0 }, + ], }) expect( mp @@ -27,7 +31,8 @@ export function assertSheetMetal() { holes: [ { panel: "left", - shape: "slot", + round: false, + slot: true, length: 10, width: 2, u: 2, diff --git a/tests/fixtures/model-string-flag-cases.ts b/tests/fixtures/model-string-flag-cases.ts index dd9f152..44b58ed 100644 --- a/tests/fixtures/model-string-flag-cases.ts +++ b/tests/fixtures/model-string-flag-cases.ts @@ -1,89 +1,185 @@ -// Public syntax pairs also serve as package-consumer compatibility fixtures. +/** Bare selectors and the boolean contract expected from each model. */ export const modelStringFlagCases = [ [ "compressionspring_od8mm_wire1mm_l20mm_turns8", "closedground", "ends(closedground)", + "closedGround", + true, + ], + [ + "compressionspring_od8mm_wire1mm_l20mm_turns8", + "lefthanded", + "hand(left)", + "leftHanded", + true, + ], + [ + "shaftcollar_bore8mm_od16mm_w8mm_m4", + "setscrew", + "mount(setscrew)", + "setScrew", + true, + ], + [ + "shaftcollar_bore8mm_od16mm_w8mm_m4", + "lefthanded", + "threadhand(left)", + "leftHanded", + true, ], - ["compressionspring_od8mm_wire1mm_l20mm_turns8", "lefthanded", "hand(left)"], - ["shaftcollar_bore8mm_od16mm_w8mm_m4", "setscrew", "mount(setscrew)"], - ["shaftcollar_bore8mm_od16mm_w8mm_m4", "lefthanded", "threadhand(left)"], [ "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_m4", "singleclamp", "mount(singleclamp)", + "singleClamp", + true, ], [ "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_m4", "lefthanded", "threadhand(left)", + "leftHanded", + true, ], [ "rigidcoupler_bore8mm_od20mm_l25mm_screwcount4_m4", "setscrew", "mount(setscrew)", + "setScrew", + true, ], [ "rigidcoupler_bore8mm_od20mm_l25mm_screwcount4_m4", "lefthanded", "threadhand(left)", + "leftHanded", + true, + ], + [ + "plainbushing_id8mm_od12mm_l20mm", + "plainclosed", + "style(plainclosed)", + "plainClosed", + true, ], - ["plainbushing_id8mm_od12mm_l20mm", "plainclosed", "style(plainclosed)"], [ "flangedbushing_id8mm_od12mm_flangeod18mm_l15mm_flangethickness2mm", "plainclosed", "style(plainclosed)", + "plainClosed", + true, + ], + ["hexbolt_m4_l20mm", "hex", "drive(hex)", "hex", true], + ["hexbolt_m4_l20mm", "lefthanded", "threadhand(left)", "leftHanded", true], + [ + "flatheadscrew_m4_l20mm", + "hexsocket", + "drive(hexsocket)", + "hexSocket", + true, + ], + [ + "flatheadscrew_m4_l20mm", + "lefthanded", + "threadhand(left)", + "leftHanded", + true, ], - ["hexbolt_m4_l20mm", "hex", "drive(hex)"], - ["hexbolt_m4_l20mm", "lefthanded", "threadhand(left)"], - ["flatheadscrew_m4_l20mm", "hexsocket", "drive(hexsocket)"], - ["flatheadscrew_m4_l20mm", "lefthanded", "threadhand(left)"], - ["panscrew_m4_l20mm", "phillips", "drive(phillips)"], - ["panscrew_m4_l20mm", "lefthanded", "threadhand(left)"], - ["buttonscrew_m4_l20mm", "hexsocket", "drive(hexsocket)"], - ["hexnut_m6", "righthanded", "threadhand(right)"], + ["panscrew_m4_l20mm", "phillips", "drive(phillips)", "phillips", true], + ["panscrew_m4_l20mm", "lefthanded", "threadhand(left)", "leftHanded", true], + ["buttonscrew_m4_l20mm", "hexsocket", "drive(hexsocket)", "hexSocket", true], + ["hexnut_m6", "righthanded", "threadhand(right)", "rightHanded", true], [ "cablegrommet_panelhole20mm_id10mm_od24mm_h8mm_groovew3mm_grooved2mm", "symmetricring", "shape(symmetricring)", + "symmetricRing", + true, ], [ "tslotextrusion_w20mm_h20mm_l100mm_slot6mm_pocket10mm_pocketd2mm_lip2mm_bore4mm_corner1mm", "fourtsolid", "profile(fourtsolid)", + "fourTSolid", + true, ], [ "tslotgusset_w40mm_h40mm_t4mm_slots2_slot(5mm,12mm)_centers(12mm,28mm)", "righttriangle", "shape(righttriangle)", + "rightTriangle", + true, ], - ["flatheadscrew_m4_l20mm", "fullthread", "thread(full)"], - ["flatheadscrew_m4_l20mm", "righthanded", "threadhand(right)"], - ["flatheadscrew_m4_l20mm", "male", "threadgender(male)"], - ["hexbolt_m4_l20mm", "fullthread", "thread(full)"], - ["hexbolt_m4_l20mm", "righthanded", "threadhand(right)"], - ["hexbolt_m4_l20mm", "male", "threadgender(male)"], + ["flatheadscrew_m4_l20mm", "fullthread", "thread(full)", "fullThread", true], + [ + "flatheadscrew_m4_l20mm", + "righthanded", + "threadhand(right)", + "leftHanded", + false, + ], + ["flatheadscrew_m4_l20mm", "male", "threadgender(male)", "male", true], + ["hexbolt_m4_l20mm", "fullthread", "thread(full)", "fullThread", true], + ["hexbolt_m4_l20mm", "righthanded", "threadhand(right)", "leftHanded", false], + ["hexbolt_m4_l20mm", "male", "threadgender(male)", "male", true], [ "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_m4", "righthanded", "threadhand(right)", + "leftHanded", + false, ], [ "rigidcoupler_bore8mm_od20mm_l25mm_screwcount4_m4", "righthanded", "threadhand(right)", + "leftHanded", + false, + ], + ["buttonscrew_m4_l20mm", "fullthread", "thread(full)", "fullThread", true], + [ + "buttonscrew_m4_l20mm", + "righthanded", + "threadhand(right)", + "leftHanded", + false, ], - ["buttonscrew_m4_l20mm", "fullthread", "thread(full)"], - ["buttonscrew_m4_l20mm", "righthanded", "threadhand(right)"], [ "compressionspring_od8mm_wire1mm_l20mm_turns8", "righthanded", "hand(right)", + "leftHanded", + false, + ], + [ + "compressionspring_od8mm_wire1mm_l20mm_turns8", + "custom", + "spec(custom)", + "custom", + true, + ], + [ + "compressionspring_od8mm_wire1mm_l20mm_turns8", + "free", + "state(free)", + "free", + true, + ], + ["panscrew_m4_l20mm", "fullthread", "thread(full)", "fullThread", true], + [ + "panscrew_m4_l20mm", + "righthanded", + "threadhand(right)", + "leftHanded", + false, + ], + ["panscrew_m4_l20mm", "male", "threadgender(male)", "male", true], + [ + "shaftcollar_bore8mm_od16mm_w8mm_m4", + "righthanded", + "threadhand(right)", + "leftHanded", + false, ], - ["compressionspring_od8mm_wire1mm_l20mm_turns8", "custom", "spec(custom)"], - ["compressionspring_od8mm_wire1mm_l20mm_turns8", "free", "state(free)"], - ["panscrew_m4_l20mm", "fullthread", "thread(full)"], - ["panscrew_m4_l20mm", "righthanded", "threadhand(right)"], - ["panscrew_m4_l20mm", "male", "threadgender(male)"], - ["shaftcollar_bore8mm_od16mm_w8mm_m4", "righthanded", "threadhand(right)"], ] as const diff --git a/tests/fixtures/registration-baselines.json b/tests/fixtures/registration-baselines.json index c7a8b50..d1518ac 100644 --- a/tests/fixtures/registration-baselines.json +++ b/tests/fixtures/registration-baselines.json @@ -1,16 +1,12 @@ [ { - "source": "hexbolt_m6_l25mm_drive(hex)", + "source": "hexbolt_m6_l25mm_hex", "json": { "fn": "hexbolt", "iso4017": true, "metricSize": "M6", "length": 25, - "thread": "full", - "drive": "hex", - "threadHand": "right", "threadClass": "6g", - "threadGender": "male", "showThreads": true, "diameter": 6, "threadPitch": 1, @@ -24,7 +20,11 @@ "threadRootDiameter": 4.773130677972045, "threadPitchDiameter": 5.350480947161671, "headCornerDiameter": 11.547005383792516, - "headChamferDiameter": 10 + "headChamferDiameter": 10, + "fullThread": true, + "hex": true, + "leftHanded": false, + "male": true } }, { @@ -43,8 +43,8 @@ "phase": 0, "segmentsPerTooth": 12, "helixAngle": 20, - "handedness": "right", - "segmentsPerTurn": 32 + "segmentsPerTurn": 32, + "leftHanded": false } }, { @@ -52,12 +52,10 @@ "json": { "fn": "nema", "nemaSize": 17, - "wireConnection": "stubs", "wireSideAngle": 0, "wireLength": 6, "wireDiameter": 1.2, "wireCount": 4, - "backFace": "screws", "backFaceHoleSpacing": 31, "backFaceHoleDiameter": 3, "backFaceHoleDepth": 4.5, @@ -72,27 +70,37 @@ "pilotLength": 2, "shaftDiameter": 5, "shaftLength": 24, - "shaftShape": "round", "shaftFlatDepth": 0.5, "shaftFlatLength": 15, "shaftFlatAngle": 0, "frontCapLength": 5, "rearCapLength": 5, "faceCornerChamfer": 3, - "bodyCornerChamfer": 6 + "bodyCornerChamfer": 6, + "noWires": false, + "wireStubs": true, + "backFacePlain": false, + "backFaceHoles": false, + "backFaceScrews": true, + "roundShaft": true, + "dShaft": false, + "chamfered": true, + "stepped": false } }, { "source": "sheetmetal_channel_w28mm_l24mm_h14mm_t1mm_r2mm", "json": { "fn": "sheetmetal", - "profile": "channel", "width": 28, "baseLength": 24, "flangeHeight": 14, "thickness": 1, "insideBendRadius": 2, - "holes": [] + "holes": [], + "plate": false, + "angle": false, + "channel": true } }, { @@ -101,10 +109,15 @@ "fn": "flexscreen", "width": 16, "height": 10, - "orientation": "sitsFlat", "flexCableLength": 5, "pinCount": 30, - "pitch": 0.5 + "pitch": 0.5, + "sitsFlat": true, + "sitsFlatBelowBoard": false, + "foldedToFaceAboveBoard": false, + "foldedToFaceBelowBoard": false, + "foldedToRightAngleAboveBoard": false, + "foldedToRightAngleBelowBoard": false } }, { @@ -145,10 +158,10 @@ "backlash": 0, "clearance": 0.25, "boreDiameter": 0, - "handedness": "right", "phase": 0, "radialSegments": 96, - "segmentsPerTurn": 32 + "segmentsPerTurn": 32, + "leftHanded": false } } ] diff --git a/tests/flanged-bushing.test.ts b/tests/flanged-bushing.test.ts index 5bbc521..971d2e6 100644 --- a/tests/flanged-bushing.test.ts +++ b/tests/flanged-bushing.test.ts @@ -11,13 +11,13 @@ import { test("flanged bushing roadmap example, public registry and overall length convention", () => { const source = - "flangedbushing_id8mm_od12mm_flangeod18mm_l15mm_flangethickness2mm_style(plainclosed)" + "flangedbushing_id8mm_od12mm_flangeod18mm_l15mm_flangethickness2mm_plainclosed" const builder = mp.string(source) expect(builder.params()).toMatchObject({ id: "8mm", flangeod: "18mm", flangethickness: "2mm", - style: "(plainclosed)", + plainclosed: true, }) const expected = { fn: "flangedbushing", @@ -26,7 +26,7 @@ test("flanged bushing roadmap example, public registry and overall length conven flangeDiameter: 18, length: 15, flangeThickness: 2, - style: "plainclosed", + plainClosed: true, } as const expect(builder.json()).toEqual(expected) expect(parseModelString(source)).toEqual(expected) @@ -69,12 +69,12 @@ test("flanged bushing default style and normalized length aliases", () => { flangeDiameter: 18, length: 15, flangeThickness: 2, - style: "plainclosed", + plainClosed: true, }) expect( mp .string( - "FLANGEDBUSHING_innerdiameter8MM_outerdiameter1.2CM_flangediameter18mm_length15mm_flangethickness0.2cm_STYLE(PLAINCLOSED)", + "FLANGEDBUSHING_innerdiameter8MM_outerdiameter1.2CM_flangediameter18mm_length15mm_flangethickness0.2cm_PLAINCLOSED", ) .json(), ).toMatchObject({ @@ -83,7 +83,7 @@ test("flanged bushing default style and normalized length aliases", () => { flangeDiameter: 18, length: 15, flangeThickness: 2, - style: "plainclosed", + plainClosed: true, }) expect( getFlangedBushingDimensions({ @@ -109,13 +109,13 @@ test("flanged bushing rejects duplicate aliases, malformed tokens and unsupporte "l15mm", "length15mm", "flangethickness2mm", - "style(plainclosed)_style(plainclosed)", + "plainclosed_plainclosed", "style(open)", "style(ball)", "style", "stylePLAINCLOSED", - "style(plainclosed)junk", - "style(plainclosed)(plainclosed)", + "plainclosedjunk", + "plainclosed(plainclosed)", "edgechamfer0.5mm", "flangethickness(2mm)", "flangethickness2mmjunk", @@ -140,7 +140,7 @@ test("flanged bushing rejects duplicate aliases, malformed tokens and unsupporte "flangedbushing_id8mmjunk_od12mm_flangeod18mm_l15mm_flangethickness2mm", "flangedbushing_id8mm_od12mm_flangeod18mm_l1e2_flangethickness2mm", `${base}_style(plainclosed`, - `${base}__style(plainclosed)`, + `${base}__plainclosed`, ]) expect(() => mp.string(source).json()).toThrow() }) @@ -189,3 +189,37 @@ test("flanged bushing direct schema compatibility and complete finite lengths", getFlangedBushingDimensions({ ...base, flangeThickness: 15 }), ).toThrow() }) + +test("flangedbushing uses a direct boolean flag and rejects legacy selectors", () => { + const base = + "flangedbushing_id8mm_od12mm_flangeod18mm_l15mm_flangethickness2mm" + const omitted = mp.string(base).json() + if (omitted.fn !== "flangedbushing") throw new Error("Unexpected model") + expect(omitted.plainClosed).toBe(true) + expect(omitted).not.toHaveProperty("style") + expect(mp.string(base + "_plainclosed").json()).toEqual(omitted) + expect(mp.string((base + "_plainclosed").toUpperCase()).json()).toEqual( + omitted, + ) + expect(mp.string(base + "_plainclosed").params()).toMatchObject({ + plainclosed: true, + string: base + "_plainclosed", + }) + for (const suffix of [ + "plainclosed_plainclosed", + "plainclosed1", + "plainclosed(true)", + "plainclosed(false)", + "style(plainclosed)", + "style(unsupported)", + ]) + expect(() => mp.string(base + "_" + suffix).json()).toThrow() + const { fn, ...props } = omitted + for (const value of [false, "true", 1, null]) + expect(() => + flangedBushingModelPropsSchema.parse({ ...props, plainClosed: value }), + ).toThrow() + expect(() => + flangedBushingModelPropsSchema.parse({ ...props, style: "plainclosed" }), + ).toThrow() +}) diff --git a/tests/flat-head-screw-boolean-flags.test.ts b/tests/flat-head-screw-boolean-flags.test.ts new file mode 100644 index 0000000..1db295e --- /dev/null +++ b/tests/flat-head-screw-boolean-flags.test.ts @@ -0,0 +1,86 @@ +import { expect, test } from "bun:test" +import { mp, flatHeadScrewModelPropsSchema } from "../src" +const source = "flatheadscrew_m3_l10mm" + +test("flatheadscrew directly exposes boolean selectors", () => { + const complete = source + "_hexsocket_fullthread_righthanded_male" + const builder = mp.string(complete) + expect(builder.params().string).toBe(complete) + const model = builder.json() + expect(model).toMatchObject({ + hexSocket: true, + fullThread: true, + leftHanded: false, + male: true, + }) + for (const property of ["drive", "thread", "threadHand", "threadGender"]) + expect(model).not.toHaveProperty(property) + expect( + flatHeadScrewModelPropsSchema.parse({ + metricSize: "M3", + length: 10, + hexSocket: true, + fullThread: true, + leftHanded: false, + }), + ).toMatchObject({ hexSocket: true, fullThread: true, leftHanded: false }) +}) +test("flatheadscrew boolean flags accept no values or repetitions", () => { + for (const tail of [ + "hexsocket_hexsocket", + "hexsocket(true)", + "hexsocket1", + "fullthread_fullthread", + "fullthread(true)", + "righthanded_righthanded", + "righthanded(false)", + "righthanded_lefthanded", + "lefthanded_righthanded", + "lefthanded_lefthanded", + "male_male", + "male(true)", + ]) + expect(() => mp.string(source + "_" + tail).json()).toThrow() +}) +test("flatheadscrew rejects deprecated string selectors and schema properties", () => { + for (const tail of [ + "drive(hexsocket)", + "thread(full)", + "threadhand(right)", + "threadhand(left)", + "threadgender(male)", + ]) + expect(() => mp.string(source + "_" + tail).json()).toThrow() + for (const invalid of [ + { drive: "hexsocket" }, + { thread: "full" }, + { threadHand: "right" }, + { threadGender: "male" }, + { hexSocket: false }, + { fullThread: false }, + { leftHanded: "left" }, + { male: false }, + ]) + expect(() => + flatHeadScrewModelPropsSchema.parse({ + metricSize: "M3", + length: 10, + ...invalid, + }), + ).toThrow() +}) +test("flatheadscrew both hand flags map to one boolean property", () => { + expect(mp.string(source + "_lefthanded").json()).toMatchObject({ + leftHanded: true, + }) + expect(mp.string(source + "_righthanded").json()).toMatchObject({ + leftHanded: false, + }) + expect( + flatHeadScrewModelPropsSchema.parse({ + metricSize: "M3", + length: 10, + leftHanded: true, + }), + ).toMatchObject({ leftHanded: true }) +}) diff --git a/tests/flatgasket-contract-01.test.ts b/tests/flatgasket-contract-01.test.ts index bf1ca71..9892c31 100644 --- a/tests/flatgasket-contract-01.test.ts +++ b/tests/flatgasket-contract-01.test.ts @@ -10,9 +10,10 @@ test("flatgasket complete strings and public dimensions agree", () => { expect(mp.string("flatgasket_id20mm_od35mm_t2mm_flatannulus").json()).toEqual( { fn: "flatgasket", ...props }, ) - expect( - mp.string("flatgasket_id20mm_od35mm_t2mm_profile(flatannulus)").json(), - ).toEqual({ fn: "flatgasket", ...props }) + expect(mp.string("flatgasket_id20mm_od35mm_t2mm").json()).toEqual({ + fn: "flatgasket", + ...props, + }) expect(flatGasketModelPropsSchema.parse(props)).toEqual(props) expect(getFlatGasketDimensions(props).size).toEqual([35, 35, 2]) }) diff --git a/tests/flatgasket-contract-04-flags.test.ts b/tests/flatgasket-contract-04-flags.test.ts new file mode 100644 index 0000000..89c5712 --- /dev/null +++ b/tests/flatgasket-contract-04-flags.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from "bun:test" +import { mp } from "../src" + +test("flatgasket accepts value-free flags and rejects standalone enum selectors", () => { + const source = "flatgasket_id20mm_od35mm_t2mm" + expect(mp.string(`${source}_flatannulus`).json()).toEqual( + mp.string(source).json(), + ) + for (const token of [ + "profile(flatannulus)", + "style(flatannulus)", + "flatannulus(true)", + "flatannulus(false)", + "flatannulus_flatannulus", + ]) { + expect(() => mp.string(`${source}_${token}`).json(), token).toThrow() + } +}) diff --git a/tests/flex-screen-orientation.test.ts b/tests/flex-screen-orientation.test.ts new file mode 100644 index 0000000..42a08c7 --- /dev/null +++ b/tests/flex-screen-orientation.test.ts @@ -0,0 +1,157 @@ +import { expect, test } from "bun:test" +import * as publicApi from "../src" +import { flexScreenModelPropsSchema, mp } from "../src" + +const flat = { + sitsFlat: true, + sitsFlatBelowBoard: false, + foldedToFaceAboveBoard: false, + foldedToFaceBelowBoard: false, + foldedToRightAngleAboveBoard: false, + foldedToRightAngleBelowBoard: false, +} as const + +const placements = [ + ["sitsFlat", ["sitsflat"]], + ["sitsFlatBelowBoard", ["sitsflatbelow", "sitsflatbelowboard"]], + [ + "foldedToFaceAboveBoard", + ["foldsabove", "foldsaboveboard", "foldedtofaceaboveboard"], + ], + [ + "foldedToFaceBelowBoard", + ["foldsbelow", "foldsbelowboard", "foldedtofacebelowboard"], + ], + [ + "foldedToRightAngleAboveBoard", + ["rightangleabove", "rightangleaboveboard", "foldedtorightangleaboveboard"], + ], + [ + "foldedToRightAngleBelowBoard", + ["rightanglebelow", "rightanglebelowboard", "foldedtorightanglebelowboard"], + ], +] as const + +test("FlexScreen exposes boolean placements and resolves a default and aliases", () => { + expect(flexScreenModelPropsSchema.parse({})).toEqual(flat) + expect(mp.string("flexscreen30_p0.5").json()).toEqual({ + fn: "flexscreen", + pinCount: 30, + pitch: 0.5, + ...flat, + }) + expect("flexScreenOrientationSchema" in publicApi).toBe(false) + for (const [flag, aliases] of placements) { + const expected = Object.fromEntries( + placements.map(([key]) => [key, key === flag]), + ) as Record + expect(flexScreenModelPropsSchema.parse({ [flag]: true })).toEqual(expected) + expect(flexScreenModelPropsSchema.parse(expected)).toEqual(expected) + for (const alias of aliases) { + const source = `flexscreen_w2cm_${alias.toUpperCase()}` + expect(mp.string(source).params()).toMatchObject({ + [alias]: true, + string: source, + }) + expect(mp.string(source).json()).toEqual({ + fn: "flexscreen", + width: 20, + ...expected, + }) + } + } + for (const [alias, flag] of [ + ["foldsAboveBoard", "foldedToFaceAboveBoard"], + ["foldsBelowBoard", "foldedToFaceBelowBoard"], + ] as const) { + const normalized = flexScreenModelPropsSchema.parse({ [alias]: true }) + expect(normalized[flag]).toBe(true) + expect(alias in normalized).toBe(false) + } +}) + +test("FlexScreen rejects enum APIs, invalid boolean values, missing selections and repeated aliases", () => { + for (const value of ["sitsFlat", "foldedToFaceAboveBoard", null, false]) { + expect(() => + flexScreenModelPropsSchema.parse({ orientation: value }), + ).toThrow() + } + for (const [flag] of placements) { + for (const value of ["true", 1, null]) { + expect(() => + flexScreenModelPropsSchema.parse({ [flag]: value }), + ).toThrow() + } + expect(() => flexScreenModelPropsSchema.parse({ [flag]: false })).toThrow( + "must be true", + ) + } + expect(() => + flexScreenModelPropsSchema.parse( + Object.fromEntries(placements.map(([flag]) => [flag, false])), + ), + ).toThrow("must be true") + expect(() => + flexScreenModelPropsSchema.parse({ foldsAboveBoard: false }), + ).toThrow("must be true") + expect(() => + flexScreenModelPropsSchema.parse({ + foldedToFaceAboveBoard: true, + foldsAboveBoard: true, + }), + ).toThrow("Only one") + for (const source of [ + "flexscreen_sitsflat_sitsflat", + "flexscreen_sitsflatbelow_sitsflatbelowboard", + "flexscreen_foldsabove_foldsaboveboard", + "flexscreen_foldsbelow_foldedtofacebelowboard", + "flexscreen_rightangleabove_foldedtorightangleaboveboard", + "flexscreen_rightanglebelow_rightanglebelowboard", + "flexscreen_sitsflat_foldsabove", + "flexscreen_foldsabove_foldsbelow", + "flexscreen_sitsflat(true)", + "flexscreen_sitsflatfalse", + "flexscreen_sitsflat1", + "flexscreen_orientation(sitsFlat)", + ]) + expect(() => mp.string(source).json()).toThrow() +}) + +test("FlexScreen bare folds keep relative distance and value-bearing ratio and color data", () => { + expect( + mp + .string( + "flexscreen30_w2cm_foldsabove_distance1cm_ratio4x3_screencolor(#123456)", + ) + .json(), + ).toMatchObject({ + pinCount: 30, + width: 20, + foldedToFaceAboveBoard: true, + sitsFlat: false, + distanceAboveBoard: 10, + aspectRatio: "4:3", + screenColor: "#123456", + }) + expect( + mp.string("flexscreen_foldedtofacebelowboard_distance0.5cm").json(), + ).toMatchObject({ + foldedToFaceBelowBoard: true, + distanceBelowBoard: 5, + }) + expect(() => mp.string("flexscreen_sitsflat_distance10").json()).toThrow( + "requires foldsabove or foldsbelow", + ) + expect( + flexScreenModelPropsSchema.parse({ + aspectRatio: [4, 3], + screenRotation: [0, "90deg", 0], + screenColor: "#123456", + }), + ).toMatchObject({ + ...flat, + aspectRatio: [4, 3], + screenRotation: [0, "90deg", 0], + screenColor: "#123456", + }) +}) diff --git a/tests/gear-boolean-flags.test.ts b/tests/gear-boolean-flags.test.ts new file mode 100644 index 0000000..fae8788 --- /dev/null +++ b/tests/gear-boolean-flags.test.ts @@ -0,0 +1,70 @@ +import { expect, test } from "bun:test" +import { + helicalGearModelDefinitionSchema, + helicalGearModelPropsSchema, + mp, + wormGearModelDefinitionSchema, + wormGearModelPropsSchema, +} from "../src" + +const families = [ + { + name: "helicalgear", + propsSchema: helicalGearModelPropsSchema, + definitionSchema: helicalGearModelDefinitionSchema, + }, + { + name: "wormgear", + propsSchema: wormGearModelPropsSchema, + definitionSchema: wormGearModelDefinitionSchema, + }, +] as const + +for (const { name, propsSchema, definitionSchema } of families) { + test(`${name} uses a boolean hand property without rewriting its string`, () => { + expect(propsSchema.parse({}).leftHanded).toBe(false) + for (const [flag, leftHanded] of [ + ["left", true], + ["right", false], + ] as const) { + const source = `${name}_${flag}` + const builder = mp.string(source) + expect(builder.params().string).toBe(source) + expect(builder.json()).toEqual({ + fn: name, + ...propsSchema.parse({ leftHanded }), + }) + expect(definitionSchema.parse(builder.json())).toEqual( + builder.json(), + ) + expect(builder.json()).not.toHaveProperty("handedness") + } + }) + + test(`${name} rejects hand repeats, conflicts, payloads and deprecated selectors`, () => { + for (const suffix of [ + "left_left", + "right_right", + "left_right", + "right_left", + "left(true)", + "right(false)", + "left1", + "right0", + "hand(left)", + "handedness(right)", + "leftHanded(true)", + ]) { + expect(() => mp.string(`${name}_${suffix}`).json()).toThrow() + } + for (const props of [ + { handedness: "left" }, + { handedness: "right" }, + { leftHanded: "left" }, + { leftHanded: 1 }, + ]) { + expect(() => propsSchema.parse(props)).toThrow() + expect(() => definitionSchema.parse({ fn: name, ...props })).toThrow() + } + }) +} diff --git a/tests/helical-gears.test.ts b/tests/helical-gears.test.ts index 02398e7..581e3dd 100644 --- a/tests/helical-gears.test.ts +++ b/tests/helical-gears.test.ts @@ -14,7 +14,7 @@ test("helical gear defaults, aliases, units, and model union", () => { const defaults = { ...spurGearModelPropsSchema.parse({}), helixAngle: 20, - handedness: "right" as const, + leftHanded: false, segmentsPerTurn: 32, } expect(helicalGearModelPropsSchema.parse({})).toEqual(defaults) @@ -36,7 +36,7 @@ test("helical gear defaults, aliases, units, and model union", () => { module: 1, faceWidth: 6.35, helixAngle: 30, - handedness: "left", + leftHanded: true, boreDiameter: 2, backlash: 0.1, clearance: 0.3, @@ -56,7 +56,7 @@ test("helical gear defaults, aliases, units, and model union", () => { faceWidth: 4, pressureAngle: 20, helixAngle: 25, - handedness: "right", + leftHanded: false, boreDiameter: 3, }) expect(mp.string("helicalgear_width2mm").json()).toMatchObject({ @@ -78,7 +78,7 @@ test("helical dimensions preserve transverse spur geometry and signed pitch-cyli (((8 * Math.tan(Math.PI / 6)) / 30) * 180) / Math.PI, ) expect( - getHelicalGearDimensions({ ...props, helixAngle: 30, handedness: "left" }) + getHelicalGearDimensions({ ...props, helixAngle: 30, leftHanded: true }) .twistAngle, ).toBe(-d.twistAngle) const zero = getHelicalGearDimensions({ ...props, helixAngle: 0 }) diff --git a/tests/hex-bolt-boolean-flags.test.ts b/tests/hex-bolt-boolean-flags.test.ts new file mode 100644 index 0000000..16f52a2 --- /dev/null +++ b/tests/hex-bolt-boolean-flags.test.ts @@ -0,0 +1,86 @@ +import { expect, test } from "bun:test" +import { mp, hexBoltModelPropsSchema } from "../src" +const source = "hexbolt_m3_l25mm" + +test("hexbolt directly exposes boolean selectors", () => { + const complete = source + "_hex_fullthread_righthanded_male" + const builder = mp.string(complete) + expect(builder.params().string).toBe(complete) + const model = builder.json() + expect(model).toMatchObject({ + hex: true, + fullThread: true, + leftHanded: false, + male: true, + }) + for (const property of ["drive", "thread", "threadHand", "threadGender"]) + expect(model).not.toHaveProperty(property) + expect( + hexBoltModelPropsSchema.parse({ + metricSize: "M3", + length: 25, + hex: true, + fullThread: true, + leftHanded: false, + }), + ).toMatchObject({ hex: true, fullThread: true, leftHanded: false }) +}) +test("hexbolt boolean flags accept no values or repetitions", () => { + for (const tail of [ + "hex_hex", + "hex(true)", + "hex1", + "fullthread_fullthread", + "fullthread(true)", + "righthanded_righthanded", + "righthanded(false)", + "righthanded_lefthanded", + "lefthanded_righthanded", + "lefthanded_lefthanded", + "male_male", + "male(true)", + ]) + expect(() => mp.string(source + "_" + tail).json()).toThrow() +}) +test("hexbolt rejects deprecated string selectors and schema properties", () => { + for (const tail of [ + "drive(hex)", + "thread(full)", + "threadhand(right)", + "threadhand(left)", + "threadgender(male)", + ]) + expect(() => mp.string(source + "_" + tail).json()).toThrow() + for (const invalid of [ + { drive: "hex" }, + { thread: "full" }, + { threadHand: "right" }, + { threadGender: "male" }, + { hex: false }, + { fullThread: false }, + { leftHanded: "left" }, + { male: false }, + ]) + expect(() => + hexBoltModelPropsSchema.parse({ + metricSize: "M3", + length: 25, + ...invalid, + }), + ).toThrow() +}) +test("hexbolt both hand flags map to one boolean property", () => { + expect(mp.string(source + "_lefthanded").json()).toMatchObject({ + leftHanded: true, + }) + expect(mp.string(source + "_righthanded").json()).toMatchObject({ + leftHanded: false, + }) + expect( + hexBoltModelPropsSchema.parse({ + metricSize: "M3", + length: 25, + leftHanded: true, + }), + ).toMatchObject({ leftHanded: true }) +}) diff --git a/tests/hex-nut-boolean-flags.test.ts b/tests/hex-nut-boolean-flags.test.ts new file mode 100644 index 0000000..5e47f61 --- /dev/null +++ b/tests/hex-nut-boolean-flags.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from "bun:test" +import { hexNutModelPropsSchema, mp } from "../src" + +test("hex nut handedness is a fixed boolean with no enum selector", () => { + const expected = mp.string("hexnut_m6").json() + expect(expected).toHaveProperty("rightHanded", true) + expect(expected).not.toHaveProperty("threadHand") + expect(mp.string("hexnut_m6_righthanded").json()).toEqual(expected) + expect(() => + hexNutModelPropsSchema.parse({ metricSize: "M6", threadHand: "right" }), + ).toThrow() + expect(() => + hexNutModelPropsSchema.parse({ metricSize: "M6", rightHanded: false }), + ).toThrow() +}) diff --git a/tests/hex-nut.test.ts b/tests/hex-nut.test.ts index 7a09a2c..fc2cdc3 100644 --- a/tests/hex-nut.test.ts +++ b/tests/hex-nut.test.ts @@ -24,7 +24,7 @@ test("hex nut example selects pinned ISO dimensions and through-thread defaults" asmeb1822: false, metricSize: "M6", threadPitch: 1, - threadHand: "right", + rightHanded: true, threadClass: "6H", showThreads: true, }) diff --git a/tests/model-string-flags.test.ts b/tests/model-string-flags.test.ts index 87bcc6e..70fc156 100644 --- a/tests/model-string-flags.test.ts +++ b/tests/model-string-flags.test.ts @@ -11,24 +11,27 @@ import { modelStringFlagCases } from "./fixtures/model-string-flag-cases" const registry = new ModelRegistry() registerAllModels(registry) -for (const [base, flag, selector] of modelStringFlagCases) { - test(`${base.split("_")[0]} ${flag} preserves the released JSON contract`, () => { - const legacy = `${base}_${selector}` +for (const [base, flag, deprecated, property, value] of modelStringFlagCases) { + test(`${base.split("_")[0]} ${flag} resolves to a boolean without enum expansion`, () => { const source = `${base}_${flag}` - const expected = mp.string(legacy).json() + const expected = { ...mp.string(base).json(), [property]: value } const builder = mp.string(source) expect(builder.json()).toEqual(expected) expect(registry.parse(parseModelStringParams(source))).toEqual(expected) expect(modelDefinitionSchema.parse(expected)).toEqual(expected) expect(mp.string(source.toUpperCase()).json()).toEqual(expected) - const canonical = builder.params().string - expect(mp.string(canonical).params().string).toBe(canonical) - expect(mp.string(canonical).json()).toEqual(expected) - expect(mp.string(legacy).params()).toEqual(parseModelStringParams(legacy)) + expect(builder.params()).toEqual(parseModelStringParams(source)) + expect(mp.string(builder.params().string).json()).toEqual(expected) + const parsed = builder.json() as unknown as Record + expect(typeof parsed[property]).toBe("boolean") + // Check the old selector alone so duplicate-flag rejection cannot hide it. + const retired = mp.string(`${base}_${deprecated}`) + expect(retired.params()).toEqual( + parseModelStringParams(`${base}_${deprecated}`), + ) + expect(() => retired.json()).toThrow() for (const invalid of [ `${source}_${flag}`, - `${source}_${selector}`, - `${base}_${selector}_${flag}`, `${base}_${flag}(true)`, `${base}_${flag}1`, ]) @@ -36,66 +39,30 @@ for (const [base, flag, selector] of modelStringFlagCases) { }) } -test("new flags remove redundant defaults while retaining dimensional and identifier tokens", () => { - const source = - "compressionspring_spec(custom)_od0.8cm_wire1mm_l20mm_turns8_ends(closedground)_lefthanded_state(free)" - expect(mp.string(source).params().string).toBe( - "compressionspring_od0.8cm_wire1mm_l20mm_turns8_closedground_lefthanded", - ) - expect( - mp.string("plainbushing_id8mm_od12mm_l20mm_plainclosed").params().string, - ).toBe("plainbushing_id8mm_od12mm_l20mm") - expect( - mp - .string("hexbolt_iso4017_m4_l20mm_hex_male_righthanded_fullthread") - .params().string, - ).toBe("hexbolt_iso4017_m4_l20mm_hex_fullthread") -}) - -test("mixed legacy selectors and flags reject hand conflicts and duplicate defaults", () => { +test("handedness alternatives cannot be repeated or combined", () => { for (const base of [ "shaftcollar_bore8mm_od16mm_w8mm_m4", "hexbolt_m4_l20mm", - ]) { - for (const suffix of [ + "compressionspring_od8mm_wire1mm_l20mm_turns8", + ]) + for (const flags of [ "lefthanded_righthanded", - "lefthanded_threadhand(right)", - "threadhand(left)_righthanded", - "righthanded_threadhand(right)", - ]) { - expect(() => mp.string(`${base}_${suffix}`).json()).toThrow() + "righthanded_lefthanded", + "lefthanded_lefthanded", + "righthanded_righthanded", + ]) expect(() => - registry.parse(parseModelStringParams(`${base}_${suffix}`)), + registry.parse(parseModelStringParams(`${base}_${flags}`)), ).toThrow() - } - } - expect(() => - mp - .string( - "compressionspring_od8mm_wire1mm_l20mm_turns8_custom_spec(custom)", - ) - .json(), - ).toThrow() - expect(() => - mp - .string("plainbushing_id8mm_od12mm_l20mm_plainclosed_style(plainclosed)") - .json(), - ).toThrow() }) -test("flags remain model-local and preserve unsupported option validation", () => { - for (const value of [ +test("geometry flags remain model-local and retain unsupported-option validation", () => { + for (const source of [ "plainbushing_id8mm_od12mm_l20mm_setscrew", "shaftcollar_bore8mm_od16mm_w8mm_m4_singleclamp", "hexbolt_m4_l20mm_phillips", "buttonscrew_m4_l20mm_lefthanded", "hexnut_m6_lefthanded", ]) - expect(() => mp.string(value).json()).toThrow() - // Legacy invalid strings retain deferred validation and untouched raw params. - const invalidLegacy = "shaftcollar_bore8mm_od16mm_w8mm_m4_threadhand(opposed)" - expect(mp.string(invalidLegacy).params()).toEqual( - parseModelStringParams(invalidLegacy), - ) - expect(() => mp.string(invalidLegacy).json()).toThrow() + expect(() => mp.string(source).json()).toThrow() }) diff --git a/tests/mount-spring-direct-flags.test.ts b/tests/mount-spring-direct-flags.test.ts new file mode 100644 index 0000000..a9586d6 --- /dev/null +++ b/tests/mount-spring-direct-flags.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from "bun:test" +import { parseShaftCollarModelParams } from "../src/models/shaftcollar/parse-model-string" +import { parseClampingShaftCollarModelParams } from "../src/models/clampingshaftcollar/parse-model-string" +import { parseRigidCouplerModelParams } from "../src/models/rigidcoupler/parse-model-string" +import { parseCompressionSpringModelParams } from "../src/models/compressionspring/parse-model-string" + +const cases = [ + { + fn: "shaftcollar", + base: "shaftcollar_bore8mm_od16mm_w8mm_m4", + parse: parseShaftCollarModelParams, + flags: "setscrew_lefthanded", + legacy: ["mount(setscrew)", "threadhand(left)"], + }, + { + fn: "clampingshaftcollar", + base: "clampingshaftcollar_bore8mm_od18mm_w9mm_split1mm_m4", + parse: parseClampingShaftCollarModelParams, + flags: "singleclamp_lefthanded", + legacy: ["mount(singleclamp)", "threadhand(left)"], + }, + { + fn: "rigidcoupler", + base: "rigidcoupler_bore8mm_od20mm_l25mm_m4", + parse: parseRigidCouplerModelParams, + flags: "setscrew_lefthanded", + legacy: ["mount(setscrew)", "threadhand(left)"], + }, + { + fn: "compressionspring", + base: "compressionspring_od8mm_wire1mm_l20mm_turns8", + parse: parseCompressionSpringModelParams, + flags: "custom_closedground_free_lefthanded", + legacy: ["spec(custom)", "ends(closedground)", "hand(left)", "state(free)"], + }, +] + +for (const entry of cases) { + test(`${entry.fn} direct parser accepts flags and rejects legacy selectors`, () => { + const direct = (suffix: string) => + entry.parse({ fn: entry.fn, string: `${entry.base}_${suffix}` }) + expect(direct(entry.flags).leftHanded).toBe(true) + expect(direct("righthanded").leftHanded).toBe(false) + for (const suffix of [ + ...entry.legacy, + "lefthanded_righthanded", + "lefthanded(true)", + ]) + expect(() => direct(suffix), suffix).toThrow() + }) +} diff --git a/tests/nema-motor-boolean-flags.test.ts b/tests/nema-motor-boolean-flags.test.ts new file mode 100644 index 0000000..cb4239c --- /dev/null +++ b/tests/nema-motor-boolean-flags.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from "bun:test" +import { mp, nemaMotorModelPropsSchema } from "../src" + +test("NEMA geometry choices resolve to exclusive booleans with unchanged frame defaults", () => { + for (const size of [8, 17, 23, 34] as const) { + const definition = mp.string(`nema${size}`).json() + if (definition.fn !== "nema") throw new Error("Expected NEMA") + const { fn, ...props } = definition + expect(nemaMotorModelPropsSchema.parse(props)).toEqual(props) + expect(nemaMotorModelPropsSchema.parse({ nemaSize: size })).toEqual(props) + expect(props.roundShaft).toBe(size === 8 || size === 34) + expect(props.dShaft).toBe(size === 17 || size === 23) + expect(props.chamfered).toBe(size !== 34) + expect(props.stepped).toBe(size === 34) + expect(props.backFacePlain).toBe(size === 34) + expect(props.backFaceHoles).toBe(false) + expect(props.backFaceScrews).toBe(size !== 34) + expect(props.noWires).toBe(false) + expect(props.wireStubs).toBe(true) + for (const key of [ + "backFace", + "shaftShape", + "cornerProfile", + "wireConnection", + ]) + expect(props).not.toHaveProperty(key) + for (const [token, flag] of [ + ["round", "roundShaft"], + ["roundshaft", "roundShaft"], + ["dshaft", "dShaft"], + ["plainbackface", "backFacePlain"], + ["backfaceplain", "backFacePlain"], + ["backfaceholes", "backFaceHoles"], + ["backfacescrews", "backFaceScrews"], + ["stepped", "stepped"], + ["chamfered", "chamfered"], + ["nowires", "noWires"], + ["wirestubs", "wireStubs"], + ] as const) { + const flagged = mp + .string(`nema${size}_${token}_backholespacing12mm`) + .json() + expect(flagged).toMatchObject({ [flag]: true }) + expect(flagged).toEqual({ + fn: "nema", + ...nemaMotorModelPropsSchema.parse({ + nemaSize: size, + backFaceHoleSpacing: 12, + [flag]: true, + }), + }) + } + } + const jst = nemaMotorModelPropsSchema.parse({ + nemaSize: 17, + wireConnection: "jst-sh-4", + }) + expect(jst).toMatchObject({ + noWires: false, + wireStubs: false, + wireConnection: "jst-sh-4", + }) + expect(nemaMotorModelPropsSchema.parse(jst)).toEqual(jst) +}) diff --git a/tests/nema-motor-boolean-validation.test.ts b/tests/nema-motor-boolean-validation.test.ts new file mode 100644 index 0000000..0c37f6e --- /dev/null +++ b/tests/nema-motor-boolean-validation.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from "bun:test" +import { mp, nemaMotorModelPropsSchema } from "../src" + +test("NEMA rejects enum selectors, flag values, repeated and conflicting choices", () => { + for (const input of [ + { backFace: "holes" }, + { shaftShape: "round" }, + { cornerProfile: "stepped" }, + { wireConnection: "none" }, + { wireConnection: "stubs" }, + { backFaceHoles: true, backFaceScrews: true }, + { backFacePlain: true, backFaceHoles: true }, + { roundShaft: true, dShaft: true }, + { chamfered: true, stepped: true }, + { noWires: true, wireStubs: true }, + { wireConnection: "jst-ph-4", noWires: true }, + { wireConnection: "jst-sh-4", wireStubs: true }, + { backFacePlain: false }, + { roundShaft: false, dShaft: false }, + { chamfered: false }, + { noWires: false, wireStubs: false }, + { dShaft: "true" }, + { noWires: 1 }, + ]) + expect(() => + nemaMotorModelPropsSchema.parse({ nemaSize: 17, ...input }), + ).toThrow() + for (const token of [ + "backface(holes)", + "shaftshape(round)", + "cornerprofile(stepped)", + "wireconnection(none)", + "style(stepped)", + "profile(stepped)", + "backfaceholes(true)", + "roundshaft(false)", + "stepped(true)", + "nowires(false)", + "backfaceholes_backfaceholes", + "plainbackface_backfaceplain", + "backfaceholes_backfacescrews", + "round_dshaft", + "dshaft_round", + "round_roundshaft", + "chamfered_stepped", + "stepped_chamfered", + "nowires_wirestubs", + "wirestubs_nowires", + "jstph4_wirestubs", + "wirestubs_jstph4", + "jstsh4_nowires", + "nowires_jstsh4", + ]) + expect(() => mp.string(`nema17_${token}`).json()).toThrow() +}) diff --git a/tests/nema-motor-wire-references.test.ts b/tests/nema-motor-wire-references.test.ts index 162384f..67d3fc6 100644 --- a/tests/nema-motor-wire-references.test.ts +++ b/tests/nema-motor-wire-references.test.ts @@ -6,13 +6,17 @@ import { } from "../src" test("NEMA references follow dimensions and wire angles independently of visibility", () => { - for (const wireConnection of ["none", "stubs", "jst-ph-6"] as const) { + for (const termination of [ + { noWires: true }, + { wireStubs: true }, + { wireConnection: "jst-ph-6" }, + ] as const) { const p = nemaMotorModelPropsSchema.parse({ nemaSize: 17, bodyLength: 48, shaftLength: 30, wireSideAngle: 90, - wireConnection, + ...termination, }) const refs = getNemaMotorReferencePoints(p) expect(refs.backface.position).toEqual({ x: 0, y: 0, z: -48 }) @@ -40,13 +44,15 @@ test("wire model strings parse, normalize and reject conflicting/invalid paramet "nema17_wirestubs_wirecount6_wirelength8mm_wirediameter1mm", ), ).toMatchObject({ - wireConnection: "stubs", + wireStubs: true, + noWires: false, wireCount: 6, wireLength: 8, wireDiameter: 1, }) expect(parseModelString("nema17_nowires")).toMatchObject({ - wireConnection: "none", + noWires: true, + wireStubs: false, }) for (const model of [ "nema17_jstph1", diff --git a/tests/nema34.test.ts b/tests/nema34.test.ts index d510f8e..3c0453c 100644 --- a/tests/nema34.test.ts +++ b/tests/nema34.test.ts @@ -20,7 +20,8 @@ test("NEMA34 resolves its envelope and assembly reference points", () => { pilotLength: 1.6, shaftDiameter: 14, shaftLength: 32, - shaftShape: "round", + roundShaft: true, + dShaft: false, }) const { fn, ...props } = definition expect(nemaMotorModelPropsSchema.parse({ nemaSize: 34 })).toEqual(props) @@ -64,8 +65,11 @@ test("NEMA34 rear profile and fasteners are independent of the front flange", () rearCornerChamfer: 24, mountingHoleSpacing: 69.6, faceCornerChamfer: 2, - cornerProfile: "stepped", - backFace: "plain", + stepped: true, + chamfered: false, + backFacePlain: true, + backFaceHoles: false, + backFaceScrews: false, wireSideAngle: 90, wireLength: 25, }) @@ -95,7 +99,7 @@ test("NEMA34 wire exit follows the stepped body and stays consistent when hidden 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) + expect(getNemaMotorReferencePoints({ ...p, noWires: true }).wireside).toEqual( + visible, + ) }) diff --git a/tests/pan-screw-boolean-flags.test.ts b/tests/pan-screw-boolean-flags.test.ts new file mode 100644 index 0000000..50cc347 --- /dev/null +++ b/tests/pan-screw-boolean-flags.test.ts @@ -0,0 +1,86 @@ +import { expect, test } from "bun:test" +import { mp, panScrewModelPropsSchema } from "../src" +const source = "panscrew_m3_l10mm" + +test("panscrew directly exposes boolean selectors", () => { + const complete = source + "_phillips_fullthread_righthanded_male" + const builder = mp.string(complete) + expect(builder.params().string).toBe(complete) + const model = builder.json() + expect(model).toMatchObject({ + phillips: true, + fullThread: true, + leftHanded: false, + male: true, + }) + for (const property of ["drive", "thread", "threadHand", "threadGender"]) + expect(model).not.toHaveProperty(property) + expect( + panScrewModelPropsSchema.parse({ + metricSize: "M3", + length: 10, + phillips: true, + fullThread: true, + leftHanded: false, + }), + ).toMatchObject({ phillips: true, fullThread: true, leftHanded: false }) +}) +test("panscrew boolean flags accept no values or repetitions", () => { + for (const tail of [ + "phillips_phillips", + "phillips(true)", + "phillips1", + "fullthread_fullthread", + "fullthread(true)", + "righthanded_righthanded", + "righthanded(false)", + "righthanded_lefthanded", + "lefthanded_righthanded", + "lefthanded_lefthanded", + "male_male", + "male(true)", + ]) + expect(() => mp.string(source + "_" + tail).json()).toThrow() +}) +test("panscrew rejects deprecated string selectors and schema properties", () => { + for (const tail of [ + "drive(phillips)", + "thread(full)", + "threadhand(right)", + "threadhand(left)", + "threadgender(male)", + ]) + expect(() => mp.string(source + "_" + tail).json()).toThrow() + for (const invalid of [ + { drive: "phillips" }, + { thread: "full" }, + { threadHand: "right" }, + { threadGender: "male" }, + { phillips: false }, + { fullThread: false }, + { leftHanded: "left" }, + { male: false }, + ]) + expect(() => + panScrewModelPropsSchema.parse({ + metricSize: "M3", + length: 10, + ...invalid, + }), + ).toThrow() +}) +test("panscrew both hand flags map to one boolean property", () => { + expect(mp.string(source + "_lefthanded").json()).toMatchObject({ + leftHanded: true, + }) + expect(mp.string(source + "_righthanded").json()).toMatchObject({ + leftHanded: false, + }) + expect( + panScrewModelPropsSchema.parse({ + metricSize: "M3", + length: 10, + leftHanded: true, + }), + ).toMatchObject({ leftHanded: true }) +}) diff --git a/tests/plain-bushing.test.ts b/tests/plain-bushing.test.ts index 2f65f48..6571771 100644 --- a/tests/plain-bushing.test.ts +++ b/tests/plain-bushing.test.ts @@ -10,20 +10,19 @@ import { } from "../src" test("plain bushing roadmap example, dimensions and public registry", () => { - const source = - "plainbushing_id8mm_od12mm_l20mm_style(plainclosed)_edgechamfer0.5mm" + const source = "plainbushing_id8mm_od12mm_l20mm_plainclosed_edgechamfer0.5mm" const builder = mp.string(source) expect(builder.params()).toMatchObject({ id: "8mm", od: "12mm", - style: "(plainclosed)", + plainclosed: true, }) const expected = { fn: "plainbushing", innerDiameter: 8, outerDiameter: 12, length: 20, - style: "plainclosed", + plainClosed: true, edgeChamfer: 0.5, } as const expect(builder.json()).toEqual(expected) @@ -60,13 +59,13 @@ test("plain bushing defaults, length units and uppercase aliases", () => { innerDiameter: 8, outerDiameter: 12, length: 20, - style: "plainclosed", + plainClosed: true, edgeChamfer: 0, }) expect( mp .string( - "PLAINBUSHING_innerdiameter0.8CM_outerdiameter1.2cm_length20MM_STYLE(PLAINCLOSED)", + "PLAINBUSHING_innerdiameter0.8CM_outerdiameter1.2cm_length20MM_PLAINCLOSED", ) .json(), ).toEqual({ @@ -74,7 +73,7 @@ test("plain bushing defaults, length units and uppercase aliases", () => { innerDiameter: 8, outerDiameter: 12, length: 20, - style: "plainclosed", + plainClosed: true, edgeChamfer: 0, }) expect( @@ -96,13 +95,13 @@ test("plain bushing rejects malformed tokens, duplicates and unknown styles", () "l20mm", "length20mm", "edgechamfer0.1mm_edgechamfer0.1mm", - "style(plainclosed)_style(plainclosed)", + "plainclosed_plainclosed", "style(open)", "style(ball)", "style", "stylePLAINCLOSED", - "style(plainclosed)junk", - "style(plainclosed)(plainclosed)", + "plainclosedjunk", + "plainclosed(plainclosed)", "edgechamfer1mm", "edgechamfer-1mm", "edgechamfer", @@ -129,7 +128,7 @@ test("plain bushing rejects malformed tokens, duplicates and unknown styles", () "plainbushing_id8mm_od12mm_l1e2", "plainbushing_id(8mm)_od12mm_l20mm", `${base}_style(plainclosed`, - `${base}__style(plainclosed)`, + `${base}__plainclosed`, ]) expect(() => mp.string(source).json()).toThrow() }) @@ -165,3 +164,36 @@ test("plain bushing direct schemas reject invalid dimensions and incomplete leng } expect(() => getPlainBushingDimensions({ ...base, edgeChamfer: 1 })).toThrow() }) + +test("plainbushing uses a direct boolean flag and rejects legacy selectors", () => { + const base = "plainbushing_id8mm_od12mm_l20mm" + const omitted = mp.string(base).json() + if (omitted.fn !== "plainbushing") throw new Error("Unexpected model") + expect(omitted.plainClosed).toBe(true) + expect(omitted).not.toHaveProperty("style") + expect(mp.string(base + "_plainclosed").json()).toEqual(omitted) + expect(mp.string((base + "_plainclosed").toUpperCase()).json()).toEqual( + omitted, + ) + expect(mp.string(base + "_plainclosed").params()).toMatchObject({ + plainclosed: true, + string: base + "_plainclosed", + }) + for (const suffix of [ + "plainclosed_plainclosed", + "plainclosed1", + "plainclosed(true)", + "plainclosed(false)", + "style(plainclosed)", + "style(unsupported)", + ]) + expect(() => mp.string(base + "_" + suffix).json()).toThrow() + const { fn, ...props } = omitted + for (const value of [false, "true", 1, null]) + expect(() => + plainBushingModelPropsSchema.parse({ ...props, plainClosed: value }), + ).toThrow() + expect(() => + plainBushingModelPropsSchema.parse({ ...props, style: "plainclosed" }), + ).toThrow() +}) diff --git a/tests/rectangulargasket-contract-01.test.ts b/tests/rectangulargasket-contract-01.test.ts index 8d59568..8db1d42 100644 --- a/tests/rectangulargasket-contract-01.test.ts +++ b/tests/rectangulargasket-contract-01.test.ts @@ -21,11 +21,7 @@ test("rectangulargasket complete strings and public dimensions agree", () => { .json(), ).toEqual({ fn: "rectangulargasket", ...props }) expect( - mp - .string( - "rectangulargasket_w80mm_h50mm_border5mm_t2mm_profile(flatframe)_cornerr5mm", - ) - .json(), + mp.string("rectangulargasket_w80mm_h50mm_border5mm_t2mm_cornerr5mm").json(), ).toEqual({ fn: "rectangulargasket", ...props }) expect(rectangularGasketModelPropsSchema.parse(props)).toEqual(props) expect(getRectangularGasketDimensions(props).size).toEqual([80, 50, 2]) diff --git a/tests/rectangulargasket-contract-04-flags.test.ts b/tests/rectangulargasket-contract-04-flags.test.ts new file mode 100644 index 0000000..bf34072 --- /dev/null +++ b/tests/rectangulargasket-contract-04-flags.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from "bun:test" +import { mp } from "../src" + +test("rectangulargasket accepts value-free flags and rejects standalone enum selectors", () => { + const source = "rectangulargasket_w80mm_h50mm_border5mm_t2mm_cornerr5mm" + expect(mp.string(`${source}_flatframe`).json()).toEqual( + mp.string(source).json(), + ) + for (const token of [ + "profile(flatframe)", + "style(flatframe)", + "flatframe(true)", + "flatframe(false)", + "flatframe_flatframe", + ]) { + expect(() => mp.string(`${source}_${token}`).json(), token).toThrow() + } +}) diff --git a/tests/sheet-metal-flags.test.ts b/tests/sheet-metal-flags.test.ts new file mode 100644 index 0000000..92a84cc --- /dev/null +++ b/tests/sheet-metal-flags.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from "bun:test" +import { mp, sheetMetalHoleSchema, sheetMetalModelPropsSchema } from "../src" + +test("sheetmetal form flags default to plate and normalize individual choices", () => { + expect(mp.string("sheetmetal_w20_l20").json()).toMatchObject({ + plate: true, + angle: false, + channel: false, + }) + for (const flag of ["plate", "angle", "channel"] as const) { + expect(mp.string(`sheetmetal_${flag}_w20_l20`).json()).toMatchObject({ + plate: flag === "plate", + angle: flag === "angle", + channel: flag === "channel", + }) + expect( + sheetMetalModelPropsSchema.parse({ + width: 20, + baseLength: 20, + [flag]: true, + }), + ).toMatchObject({ + plate: flag === "plate", + angle: flag === "angle", + channel: flag === "channel", + }) + } + expect( + sheetMetalModelPropsSchema.parse({ + width: 20, + baseLength: 20, + plate: false, + }), + ).toMatchObject({ plate: true }) +}) + +test("sheetmetal rejects conflicting flags and legacy selector inputs", () => { + for (const suffix of [ + "plate_plate", + "angle_channel", + "plate_angle", + "profile(channel)", + "angle(true)", + ]) + expect( + () => mp.string(`sheetmetal_${suffix}_w20_l20`).json(), + suffix, + ).toThrow() + for (const extra of [ + { profile: "angle" }, + { angle: "true" }, + { plate: true, angle: true }, + { angle: true, channel: true }, + ]) + expect(() => + sheetMetalModelPropsSchema.parse({ width: 20, baseLength: 20, ...extra }), + ).toThrow() +}) + +test("sheetmetal cutout flags preserve panel coordinates and reject legacy shapes", () => { + const position = { panel: "base", u: 2, v: -3 } as const + expect( + sheetMetalHoleSchema.parse({ ...position, round: true, diameter: 4 }), + ).toEqual({ + ...position, + round: true, + slot: false, + diameter: 4, + }) + expect( + sheetMetalHoleSchema.parse({ + ...position, + slot: true, + length: 8, + width: 2, + }), + ).toEqual({ + ...position, + round: false, + slot: true, + length: 8, + width: 2, + axis: "v", + }) + for (const extra of [ + { shape: "round", diameter: 4 }, + { shape: "slot", length: 8, width: 2 }, + { round: true, slot: true, diameter: 4 }, + { round: false, slot: false, diameter: 4 }, + { round: "true", diameter: 4 }, + ]) + expect(() => + sheetMetalHoleSchema.parse({ ...position, ...extra }), + ).toThrow() +}) diff --git a/tests/tslot-extrusion.test.ts b/tests/tslot-extrusion.test.ts index 58ec742..eb784e9 100644 --- a/tests/tslot-extrusion.test.ts +++ b/tests/tslot-extrusion.test.ts @@ -10,7 +10,7 @@ import { } from "../src" const example = - "tslotextrusion_w20mm_h20mm_l100mm_profile(fourtsolid)_slot6mm_pocket10mm_pocketd2mm_lip2mm_bore4mm_corner1mm" + "tslotextrusion_w20mm_h20mm_l100mm_fourtsolid_slot6mm_pocket10mm_pocketd2mm_lip2mm_bore4mm_corner1mm" test("T-slot extrusion roadmap example resolves every profile dimension", () => { const model = mp.string(example).json() @@ -20,7 +20,7 @@ test("T-slot extrusion roadmap example resolves every profile dimension", () => width: 20, height: 20, length: 100, - profile: "fourtsolid", + fourTSolid: true, slotWidth: 6, pocketWidth: 10, pocketDepth: 2, @@ -104,10 +104,43 @@ test("T-slot extrusion parser rejects repeated, incomplete and unknown tokens", "tslotextrusion_profile2020", "tslotextrusion_profile(fourtsolid,fourtsolid)", "tslotextrusion_profile(2020)", - "tslotextrusion_profile(fourtsolid)_profile(fourtsolid)", + "tslotextrusion_fourtsolid_fourtsolid", "tslotextrusion_profile((fourtsolid))", "tslotextrusion_unknown1mm", "tslotextrusion__l100mm", ]) expect(() => parseModelString(value)).toThrow() }) + +test("tslotextrusion uses a direct boolean flag and rejects legacy selectors", () => { + const base = "tslotextrusion" + const omitted = mp.string(base).json() + if (omitted.fn !== "tslotextrusion") throw new Error("Unexpected model") + expect(omitted.fourTSolid).toBe(true) + expect(omitted).not.toHaveProperty("profile") + expect(mp.string(base + "_fourtsolid").json()).toEqual(omitted) + expect(mp.string((base + "_fourtsolid").toUpperCase()).json()).toEqual( + omitted, + ) + expect(mp.string(base + "_fourtsolid").params()).toMatchObject({ + fourtsolid: true, + string: base + "_fourtsolid", + }) + for (const suffix of [ + "fourtsolid_fourtsolid", + "fourtsolid1", + "fourtsolid(true)", + "fourtsolid(false)", + "profile(fourtsolid)", + "profile(unsupported)", + ]) + expect(() => mp.string(base + "_" + suffix).json()).toThrow() + const { fn, ...props } = omitted + for (const value of [false, "true", 1, null]) + expect(() => + tSlotExtrusionModelPropsSchema.parse({ ...props, fourTSolid: value }), + ).toThrow() + expect(() => + tSlotExtrusionModelPropsSchema.parse({ ...props, profile: "fourtsolid" }), + ).toThrow() +}) diff --git a/tests/tslot-gusset.test.ts b/tests/tslot-gusset.test.ts index 49eaa2f..def10c8 100644 --- a/tests/tslot-gusset.test.ts +++ b/tests/tslot-gusset.test.ts @@ -10,7 +10,7 @@ import { } from "../src" const example = - "tslotgusset_w40mm_h40mm_t4mm_shape(righttriangle)_slots2_slot(5mm,12mm)_centers(12mm,28mm)" + "tslotgusset_w40mm_h40mm_t4mm_righttriangle_slots2_slot(5mm,12mm)_centers(12mm,28mm)" test("T-slot gusset roadmap example resolves every shape and layout token", () => { const model = mp.string(example).json() @@ -20,7 +20,7 @@ test("T-slot gusset roadmap example resolves every shape and layout token", () = width: 40, height: 40, thickness: 4, - shape: "righttriangle", + rightTriangle: true, slotCount: 2, slot: [5, 12], centers: [12, 28], @@ -125,7 +125,7 @@ test("T-slot gusset parser rejects duplicates, tuple mistakes and unknown tokens "tslotgusset_slot(5mm,)", "tslotgusset_centers((12mm,28mm))", "tslotgusset_shape(righttriangle,righttriangle)", - "tslotgusset_shape(righttriangle)_shape(righttriangle)", + "tslotgusset_righttriangle_righttriangle", "tslotgusset_centers(12mm,28mm", "tslotgusset_t", "tslotgusset_unknown1mm", @@ -133,3 +133,36 @@ test("T-slot gusset parser rejects duplicates, tuple mistakes and unknown tokens ]) expect(() => parseModelString(value)).toThrow() }) + +test("tslotgusset uses a direct boolean flag and rejects legacy selectors", () => { + const base = "tslotgusset" + const omitted = mp.string(base).json() + if (omitted.fn !== "tslotgusset") throw new Error("Unexpected model") + expect(omitted.rightTriangle).toBe(true) + expect(omitted).not.toHaveProperty("shape") + expect(mp.string(base + "_righttriangle").json()).toEqual(omitted) + expect(mp.string((base + "_righttriangle").toUpperCase()).json()).toEqual( + omitted, + ) + expect(mp.string(base + "_righttriangle").params()).toMatchObject({ + righttriangle: true, + string: base + "_righttriangle", + }) + for (const suffix of [ + "righttriangle_righttriangle", + "righttriangle1", + "righttriangle(true)", + "righttriangle(false)", + "shape(righttriangle)", + "shape(unsupported)", + ]) + expect(() => mp.string(base + "_" + suffix).json()).toThrow() + const { fn, ...props } = omitted + for (const value of [false, "true", 1, null]) + expect(() => + tSlotGussetModelPropsSchema.parse({ ...props, rightTriangle: value }), + ).toThrow() + expect(() => + tSlotGussetModelPropsSchema.parse({ ...props, shape: "righttriangle" }), + ).toThrow() +}) diff --git a/tests/tslotcoverstrip-01-contract.test.ts b/tests/tslotcoverstrip-01-contract.test.ts index ac98772..5ba85f5 100644 --- a/tests/tslotcoverstrip-01-contract.test.ts +++ b/tests/tslotcoverstrip-01-contract.test.ts @@ -6,7 +6,7 @@ import { getTSlotCoverStripDimensions, } from "../src" const source = - "tslotcoverstrip_l100mm_w8mm_t1mm_stemw5.8mm_stemh2mm_barbw6.2mm_profile(tee)" + "tslotcoverstrip_l100mm_w8mm_t1mm_stemw5.8mm_stemh2mm_barbw6.2mm_tee" const props = { length: 100, width: 8, @@ -17,7 +17,7 @@ const props = { barbHeight: 0.5, tee: true, } as const -test("tslotcoverstrip complete roadmap contract and public schema roundtrip", () => { +test("tslotcoverstrip complete flag contract and public schema roundtrip", () => { const expected = { fn: "tslotcoverstrip" as const, ...props } expect(mp.string(source).json()).toEqual(expected) expect(modelDefinitionSchema.parse(expected)).toEqual(expected) diff --git a/tests/tslotcoverstrip-02-units.test.ts b/tests/tslotcoverstrip-02-units.test.ts index f6b9835..721553f 100644 --- a/tests/tslotcoverstrip-02-units.test.ts +++ b/tests/tslotcoverstrip-02-units.test.ts @@ -6,7 +6,7 @@ import { getTSlotCoverStripDimensions, } from "../src" const source = - "tslotcoverstrip_l100mm_w8mm_t1mm_stemw5.8mm_stemh2mm_barbw6.2mm_profile(tee)" + "tslotcoverstrip_l100mm_w8mm_t1mm_stemw5.8mm_stemh2mm_barbw6.2mm_tee" const props = { length: 100, width: 8, diff --git a/tests/tslotcoverstrip-03-invalid-tokens.test.ts b/tests/tslotcoverstrip-03-invalid-tokens.test.ts index c20567a..04264db 100644 --- a/tests/tslotcoverstrip-03-invalid-tokens.test.ts +++ b/tests/tslotcoverstrip-03-invalid-tokens.test.ts @@ -6,7 +6,7 @@ import { getTSlotCoverStripDimensions, } from "../src" const source = - "tslotcoverstrip_l100mm_w8mm_t1mm_stemw5.8mm_stemh2mm_barbw6.2mm_profile(tee)" + "tslotcoverstrip_l100mm_w8mm_t1mm_stemw5.8mm_stemh2mm_barbw6.2mm_tee" const props = { length: 100, width: 8, diff --git a/tests/tslotcoverstrip-04-fit-validation.test.ts b/tests/tslotcoverstrip-04-fit-validation.test.ts index 7865ecc..ecc0c3f 100644 --- a/tests/tslotcoverstrip-04-fit-validation.test.ts +++ b/tests/tslotcoverstrip-04-fit-validation.test.ts @@ -6,7 +6,7 @@ import { getTSlotCoverStripDimensions, } from "../src" const source = - "tslotcoverstrip_l100mm_w8mm_t1mm_stemw5.8mm_stemh2mm_barbw6.2mm_profile(tee)" + "tslotcoverstrip_l100mm_w8mm_t1mm_stemw5.8mm_stemh2mm_barbw6.2mm_tee" const props = { length: 100, width: 8, diff --git a/tests/tslotcoverstrip-05-flags.test.ts b/tests/tslotcoverstrip-05-flags.test.ts index e89a211..ddb3952 100644 --- a/tests/tslotcoverstrip-05-flags.test.ts +++ b/tests/tslotcoverstrip-05-flags.test.ts @@ -6,7 +6,7 @@ import { getTSlotCoverStripDimensions, } from "../src" const source = - "tslotcoverstrip_l100mm_w8mm_t1mm_stemw5.8mm_stemh2mm_barbw6.2mm_profile(tee)" + "tslotcoverstrip_l100mm_w8mm_t1mm_stemw5.8mm_stemh2mm_barbw6.2mm_tee" const props = { length: 100, width: 8, @@ -17,7 +17,7 @@ const props = { barbHeight: 0.5, tee: true, } as const -test("tslotcoverstrip value-free flags reject repeated and conflicting selectors", () => { +test("tslotcoverstrip value-free flags reject repeated flags and values", () => { expect( mp .string( diff --git a/tests/tslotcoverstrip-06-legacy-selectors.test.ts b/tests/tslotcoverstrip-06-legacy-selectors.test.ts new file mode 100644 index 0000000..0591498 --- /dev/null +++ b/tests/tslotcoverstrip-06-legacy-selectors.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from "bun:test" +import { mp } from "../src" + +test("tslotcoverstrip accepts value-free flags and rejects standalone enum selectors", () => { + const source = + "tslotcoverstrip_l100mm_w8mm_t1mm_stemw5.8mm_stemh2mm_barbw6.2mm" + expect(mp.string(`${source}_tee`).json()).toEqual(mp.string(source).json()) + for (const token of [ + "profile(tee)", + "style(tee)", + "tee(true)", + "tee(false)", + "tee_tee", + ]) { + expect(() => mp.string(`${source}_${token}`).json(), token).toThrow() + } +})