diff --git a/docs/coupling-models.md b/docs/coupling-models.md new file mode 100644 index 0000000..5bbcb7a --- /dev/null +++ b/docs/coupling-models.md @@ -0,0 +1,34 @@ +# Couplings and straight bevel gear + +These are generic industrial component families. They use explicit dimensions rather than a manufacturer part number. Length fields accept finite numbers in millimeters or decimal strings with `mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, or `feet`. Counts and the bridge angle accept unitless numbers. Unknown properties, unknown string tokens, duplicate aliases, malformed values, and impossible geometric relationships fail validation. + +All models are centered on the Z shaft axis. Bottom datum is Z=0. Hubs and shaft bores are real geometry; the universal joint is in its straight pose. The following complete strings also give the defaults. + +| Model | Full default model string | +| --- | --- | +| `jawcoupling` | `jawcoupling_od30mm_l40mm_bore8mm_hubl12mm_jawd12mm_jawcount3` | +| `beamcoupling` | `beamcoupling_od25mm_l30mm_bore8mm_hubl7mm_slotw1mm_slots6_bridgeangle60` | +| `oldhamcoupling` | `oldhamcoupling_od30mm_l36mm_bore8mm_hubl12mm_slotw7mm_slotd3mm_clearance0.2mm` | +| `universaljoint` | `universaljoint_od30mm_l60mm_bore8mm_hubl14mm_armt5mm_crossd6mm_clearance0.2mm` | +| `bevelgear` | `bevelgear_od40mm_td24mm_h14mm_bore8mm_depth3mm_hubd20mm_hubh8mm_teeth16` | + +| Family | Normalized properties and aliases | Features and constraints | +| --- | --- | --- | +| Jaw coupling | `outerDiameter` (`od`), `length` (`l`), `boreDiameter` (`bore`), `hubLength` (`hubl`), `jawDepth` (`jawd`), `jawCount` (`jawcount`) | Two interleaved jaw hubs and a separate radial spider; `jawCount` is 3–12 per hub. Jaw depth must exceed half the central gap and cannot reach the opposite hub. Bore diameter must be less than 70% of the outside diameter. | +| Beam coupling | `outerDiameter` (`od`), `length` (`l`), `boreDiameter` (`bore`), `hubLength` (`hubl`), `slotWidth` (`slotw`), `slotCount` (`slots`, `slotcount`), `bridgeAngle` (`bridgeangle`) | 2–20 staggered transverse slots, rotated 90 degrees successively, leave a 20–120 degree intact bridge at each station. The slots must leave axial material between cuts and solid end hubs. This models a slotted flexure, not a continuous helical cut. | +| Oldham coupling | `outerDiameter` (`od`), `length` (`l`), `boreDiameter` (`bore`), `hubLength` (`hubl`), `slotWidth` (`slotw`), `slotDepth` (`slotd`), `clearance` (`clearance`) | Orthogonal hub face slots and matching center-disk tongues. Disk diameter is 88% of the outside diameter; disk thickness is the central gap minus twice the clearance. Slots leave a hub floor and side walls. Clearance can be zero. | +| Universal joint | `outerDiameter` (`od`), `length` (`l`), `boreDiameter` (`bore`), `hubLength` (`hubl`), `armThickness` (`armt`), `crossDiameter` (`crossd`), `clearance` (`clearance`) | Orthogonal forks, transverse journal holes, and a solid four-arm cross centered at Z=`length/2`. Fork ear width is 1.8 times cross diameter. Constraints ensure the forks clear one another and leave material around shaft and journal bores. Clearance can be zero. | +| Straight bevel gear | `outerDiameter` (`od`), `topDiameter` (`td`), `height` (`h`), `boreDiameter` (`bore`), `toothDepth` (`depth`), `hubDiameter` (`hubd`), `hubHeight` (`hubh`), `teeth` (`teeth`) | 6–96 teeth taper toward the small face; a bored hub extends from Z=`height` to Z=`height+hubHeight`. Tooth roots leave positive material around the bore and the hub fits on the small face. The tooth shape is a nominal trapezoidal envelope for assembly visualization, not a conjugate manufacturing profile. | + +Full lowercase property names are accepted as aliases for the length fields, for example `outerdiameter30mm` and `hubheight8mm`. + +The public API exports each family's `...ModelPropsInput`, normalized `...ModelProps`, discriminated `...ModelDefinition`, `...ModelPropsSchema`, `...ModelDefinitionSchema`, and `get...Dimensions`. The dimension helpers include normalized properties, nominal `size`, bottom/top Z datums, and the central gap, slot pitch, disk/tongue dimensions, cross dimensions, or gear root diameters as appropriate. + +```ts +import { mp, getOldhamCouplingDimensions } from "@tscircuit/modelprinter" + +mp.string("oldhamcoupling_od3cm_l36mm_bore8mm").json() +getOldhamCouplingDimensions({ outerDiameter: "3cm", length: 36 }) +``` + +Geometry and visual regressions are maintained in the paired jscad-electronics renderer. diff --git a/docs/industrial-automation-models.md b/docs/industrial-automation-models.md new file mode 100644 index 0000000..610e2db --- /dev/null +++ b/docs/industrial-automation-models.md @@ -0,0 +1,306 @@ +# Industrial automation model families + +These are fundamental nominal placement models. They use millimeters after normalization and accept explicit mm, cm, m, in, mil and ft units. Omitted parameters use the defaults below. Unknown parameters, duplicate aliases and incompatible geometry dimensions fail validation. Smooth bores and decorative ribs do not assert functional thread forms or supplier-specific dimensions. + +## PneumaticCylinder + +Air cylinder with square end caps. The body is along +Z; extension is the visible rod displacement and cannot exceed stroke. End-cap screw holes and cross-body pneumatic ports are geometric openings. Bore and stroke describe nominal assembly sizing; piston, seals, threads and concealed internals are omitted. + +`pneumaticcylinder_borediameter32mm_bodylength80mm_roddiameter12mm_stroke50mm_extension25mm_endcaplength10mm_portdiameter6mm` + +| Parameter | Default, mm | +| --- | ---: | +| boreDiameter | 32 | +| bodyLength | 80 | +| rodDiameter | 12 | +| stroke | 50 | +| extension | 25 | +| endCapLength | 10 | +| portDiameter | 6 | + +## CylinderMount + +L-shaped rod-end or cylinder mounting bracket with one horizontal clearance bore in the upright and two mounting holes in the foot. Width is X, depth is Y, height is Z; all bores are nominal clearance openings. + +`cylindermount_width50mm_depth25mm_height45mm_thickness5mm_borediameter20mm_holediameter5mm` + +| Parameter | Default, mm | +| --- | ---: | +| width | 50 | +| depth | 25 | +| height | 45 | +| thickness | 5 | +| boreDiameter | 20 | +| holeDiameter | 5 | + +## AirFilter + +Pneumatic filter with a bowl guard, bottom drain and through ports in the head. Bowl and guard are a solid visual envelope; the filter media and transparent bowl interior are omitted. The drain is bored; there are no supplier-specific threads. + +`airfilter_bodywidth40mm_bodydepth40mm_bodyheight30mm_bowldiameter28mm_bowllength50mm_portdiameter8mm_drainlength8mm` + +| Parameter | Default, mm | +| --- | ---: | +| bodyWidth | 40 | +| bodyDepth | 40 | +| bodyHeight | 30 | +| bowlDiameter | 28 | +| bowlLength | 50 | +| portDiameter | 8 | +| drainLength | 8 | + +## AirRegulator + +Air pressure regulator with a fluted adjustment knob, front gauge bezel and lateral through ports. The head and gauge are nominal visual envelopes; concealed diaphragm and calibrated gauge markings are omitted. + +`airregulator_bodywidth40mm_bodydepth36mm_bodyheight30mm_knobdiameter28mm_knobheight20mm_portdiameter8mm_gaugediameter20mm` + +| Parameter | Default, mm | +| --- | ---: | +| bodyWidth | 40 | +| bodyDepth | 36 | +| bodyHeight | 30 | +| knobDiameter | 28 | +| knobHeight | 20 | +| portDiameter | 8 | +| gaugeDiameter | 20 | + +## AirLubricator + +Air-line lubricator with cylindrical reservoir, sight recess, retaining collar and top fill cap. The solid reservoir is an external envelope and does not represent oil volume or the metering internals. + +`airlubricator_bodywidth40mm_bodydepth40mm_bodyheight30mm_bowldiameter28mm_bowllength50mm_portdiameter8mm_filldiameter12mm_fillheight8mm` + +| Parameter | Default, mm | +| --- | ---: | +| bodyWidth | 40 | +| bodyDepth | 40 | +| bodyHeight | 30 | +| bowlDiameter | 28 | +| bowlLength | 50 | +| portDiameter | 8 | +| fillDiameter | 12 | +| fillHeight | 8 | + +## SolenoidValve + +Generic directional air solenoid valve: long valve body, three cross ports, two mounting holes, coil and electrical connector envelope. The internal spool and port switching paths are omitted; the model is not a pneumatic schematic. + +`solenoidvalve_length70mm_width28mm_height25mm_coilwidth24mm_coilheight30mm_portdiameter6mm` + +| Parameter | Default, mm | +| --- | ---: | +| length | 70 | +| width | 28 | +| height | 25 | +| coilWidth | 24 | +| coilHeight | 30 | +| portDiameter | 6 | + +## PressureSwitch + +Mechanical pressure switch enclosure with a bottom pressure port, top adjustment screw and bored cable gland on +X. The switch mechanism and pressure calibration are omitted. + +`pressureswitch_width32mm_depth28mm_height38mm_portdiameter8mm_portlength10mm` + +| Parameter | Default, mm | +| --- | ---: | +| width | 32 | +| depth | 28 | +| height | 38 | +| portDiameter | 8 | +| portLength | 10 | + +## PressureGauge + +Round pressure gauge facing -Y with a recessed dial, eleven major tick marks and a fixed nominal needle. The bottom stem is bored. Tick marks are decorative and intentionally have no numbers or calibration. + +`pressuregauge_diameter60mm_depth22mm_stemdiameter10mm_stemlength15mm` + +| Parameter | Default, mm | +| --- | ---: | +| diameter | 60 | +| depth | 22 | +| stemDiameter | 10 | +| stemLength | 15 | + +## FlowMeter + +Vertical variable-area flowmeter with hollow end fittings, a rear tube wall, visible float and scale ticks. The front wall is intentionally cut away to show the nominal internal float through the inspection window; this is an explanatory envelope model rather than a closed pressure vessel. + +`flowmeter_length120mm_diameter24mm_borediameter12mm_enddiameter32mm_endlength12mm_floatdiameter8mm` + +| Parameter | Default, mm | +| --- | ---: | +| length | 120 | +| diameter | 24 | +| boreDiameter | 12 | +| endDiameter | 32 | +| endLength | 12 | +| floatDiameter | 8 | + +## VacuumCup + +Tapered vacuum suction cup with a hollow bell, sealing lip and through-bored connection neck. Cup lip rests on Z=0; no flexible deformation, thread form or vacuum rating is modeled. + +`vacuumcup_diameter35mm_height22mm_neckdiameter12mm_necklength12mm_wallthickness2mm_borediameter6mm` + +| Parameter | Default, mm | +| --- | ---: | +| diameter | 35 | +| height | 22 | +| neckDiameter | 12 | +| neckLength | 12 | +| wallThickness | 2 | +| boreDiameter | 6 | + +## InductiveProximitySensor + +Cylindrical inductive proximity sensor with two hex mounting nuts, a distinct sensing face on +Z and short cable tail on -Z. Circumferential ribs suggest nominal barrel threading; they are not a functional helical thread. + +`inductiveproximitysensor_barreldiameter18mm_barrellength55mm_facethickness2mm_cablediameter5mm_cablelength15mm_nutwidth22mm_nutthickness4mm` + +| Parameter | Default, mm | +| --- | ---: | +| barrelDiameter | 18 | +| barrelLength | 55 | +| faceThickness | 2 | +| cableDiameter | 5 | +| cableLength | 15 | +| nutWidth | 22 | +| nutThickness | 4 | + +## PhotoelectricSensor + +Rectangular photoelectric sensor with two projecting optical lenses facing -Y, a top status indicator, cable tail and two transverse mounting holes. Optical axes are nominal and no sensing range is implied. + +`photoelectricsensor_width20mm_depth35mm_height55mm_lensdiameter12mm_cablediameter5mm_mountholediameter4mm` + +| Parameter | Default, mm | +| --- | ---: | +| width | 20 | +| depth | 35 | +| height | 55 | +| lensDiameter | 12 | +| cableDiameter | 5 | +| mountHoleDiameter | 4 | + +## LimitSwitch + +Industrial roller-lever limit switch with a transverse pivot, straight lever, end roller and two mounting bores. The lever is fixed in its nominal horizontal position; electrical contacts and motion are omitted. + +`limitswitch_width30mm_depth20mm_height55mm_actuatorlength30mm_rollerdiameter12mm_leverthickness3mm_holediameter4mm` + +| Parameter | Default, mm | +| --- | ---: | +| width | 30 | +| depth | 20 | +| height | 55 | +| actuatorLength | 30 | +| rollerDiameter | 12 | +| leverThickness | 3 | +| holeDiameter | 4 | + +## PushbuttonOperator + +Panel pushbutton operator with nominal mounting barrel, bezel, circular plunger and two rear terminal tabs. The panel is at Z=bodyLength, the actuator faces +Z, and terminals project below Z=0. No contact rating is implied. + +`pushbuttonoperator_paneldiameter22mm_bezeldiameter30mm_buttondiameter22mm_bodylength35mm_buttonheight8mm` + +| Parameter | Default, mm | +| --- | ---: | +| panelDiameter | 22 | +| bezelDiameter | 30 | +| buttonDiameter | 22 | +| bodyLength | 35 | +| buttonHeight | 8 | + +## EmergencyStopSwitch + +Mushroom-head emergency stop operator with a panel collar, tapered head, rear contact block and terminal tabs. This is a mechanical placement model and does not assert contact configuration, latching performance or safety certification. + +`emergencystopswitch_paneldiameter22mm_headdiameter40mm_bodylength45mm_headheight18mm_collardiameter30mm` + +| Parameter | Default, mm | +| --- | ---: | +| panelDiameter | 22 | +| headDiameter | 40 | +| bodyLength | 45 | +| headHeight | 18 | +| collarDiameter | 30 | + +## SelectorOperator + +Panel selector switch operator with mounting barrel, bezel, raised bar grip and two rear terminals. The grip is aligned with X at its nominal position; detents and switching contacts are omitted. + +`selectoroperator_paneldiameter22mm_bezeldiameter30mm_bodylength35mm_knobwidth28mm_knobdepth10mm_knobheight15mm` + +| Parameter | Default, mm | +| --- | ---: | +| panelDiameter | 22 | +| bezelDiameter | 30 | +| bodyLength | 35 | +| knobWidth | 28 | +| knobDepth | 10 | +| knobHeight | 15 | + +## PilotLight + +Panel indicator light with raised lens, surrounding bezel, mounting barrel and two rear terminal tabs. Optical diffusion and illumination are not simulated; no electrical ratings or mounting thread are asserted. + +`pilotlight_paneldiameter22mm_lensdiameter28mm_bodylength28mm_lensheight8mm` + +| Parameter | Default, mm | +| --- | ---: | +| panelDiameter | 22 | +| lensDiameter | 28 | +| bodyLength | 28 | +| lensHeight | 8 | + +## DinRail + +Generic top-hat DIN rail with a central floor, upright webs, outturned flanges and repeated oblong mounting holes. Rail length runs along X and the section width is Y. Dimensions are nominal; this model does not assert compliance with a rail standard. + +`dinrail_width35mm_height7.5mm_thickness1mm_length100mm_holediameter5mm_slotlength10mm_slotpitch25mm` + +| Parameter | Default, mm | +| --- | ---: | +| width | 35 | +| height | 7.5 | +| thickness | 1 | +| length | 100 | +| holeDiameter | 5 | +| slotLength | 10 | +| slotPitch | 25 | + +## TerminalBlock + +Single DIN terminal block with two wire-entry bores, paired slotted screw heads, center separator recess and underside rail clip recess. The insulating body is an envelope; screw thread, conductive cage and rail latching mechanics are omitted. + +`terminalblock_width6mm_depth40mm_height35mm_wirediameter3mm_screwdiameter4mm` + +| Parameter | Default, mm | +| --- | ---: | +| width | 6 | +| depth | 40 | +| height | 35 | +| wireDiameter | 3 | +| screwDiameter | 4 | + +## WireDuct + +Open slotted wire duct with a continuous floor, two comb sidewalls and repeated bottom mounting holes. Length runs along X. The removable lid and flexible sidewall deformation are omitted. + +`wireduct_width40mm_height40mm_length100mm_thickness2mm_slotwidth4mm_slotpitch8mm_holediameter5mm` + +| Parameter | Default, mm | +| --- | ---: | +| width | 40 | +| height | 40 | +| length | 100 | +| thickness | 2 | +| slotWidth | 4 | +| slotPitch | 8 | +| holeDiameter | 5 | + +Repeated DIN rail and wire duct sidewall features are capped at 512. Wire duct length is limited to 12,800 mm to bound the fixed mounting-hole pattern. diff --git a/docs/industrial-catalog.md b/docs/industrial-catalog.md new file mode 100644 index 0000000..36d7fb0 --- /dev/null +++ b/docs/industrial-catalog.md @@ -0,0 +1,237 @@ +# Industrial component catalog + +100 fundamental component families for mechanical assemblies and industrial automation. Each family has a distinct model name with configurable dimensions normalized to millimeters. The catalog uses nominal custom envelopes; threaded surfaces, bearings, gears and valve internals use the simplifications documented in the group guides. + +The duplicate audit reviewed all 274 historical PRs authored by seveibar (104 modelprinter, 170 jscad-electronics), including closed and unmerged proposals, as well as both main registries. See [audit](industrial-model-pr-audit.json). + +## Fasteners And Retainers (20) + +| Model | Distinguishing geometry | Example and four-view snapshot | +| --- | --- | --- | +| `flatwasher` | Flat load-spreading annulus | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/FlatWasher.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/flatwasher.snap.png) | +| `toothedwasher` | Annulus with radial locking teeth | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/ToothedWasher.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/toothedwasher.snap.png) | +| `wavewasher` | Axially undulating annulus | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/WaveWasher.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/wavewasher.snap.png) | +| `bellevillewasher` | Conical annulus | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/BellevilleWasher.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/bellevillewasher.snap.png) | +| `shoulderbolt` | Large shoulder, small shank and socket head | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/ShoulderBolt.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/shoulderbolt.snap.png) | +| `ubolt` | Bent U rod with two parallel legs | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/UBolt.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/ubolt.snap.png) | +| `eyebolt` | Axial shank with ring eye | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/EyeBolt.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/eyebolt.snap.png) | +| `carriagebolt` | Domed head and square neck | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/CarriageBolt.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/carriagebolt.snap.png) | +| `wingnut` | Central bore and two wings | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/WingNut.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/wingnut.snap.png) | +| `capnut` | Hex base with closed dome | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/CapNut.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/capnut.snap.png) | +| `couplingnut` | Long hex sleeve with through bore | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/CouplingNut.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/couplingnut.snap.png) | +| `tslotnut` | T-section threaded slug | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/TSlotNut.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/tslotnut.snap.png) | +| `rivetnut` | Flanged cylindrical insert and blind bore | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/RivetNut.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/rivetnut.snap.png) | +| `solidrivet` | Domed head and solid shank | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/SolidRivet.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/solidrivet.snap.png) | +| `blindrivet` | Flanged sleeve and projecting mandrel | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/BlindRivet.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/blindrivet.snap.png) | +| `clevispin` | Headed shaft with transverse retention bore | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/ClevisPin.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/clevispin.snap.png) | +| `springpin` | Slit cylindrical tube | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/SpringPin.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/springpin.snap.png) | +| `taperpin` | Tapered solid rod | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/TaperPin.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/taperpin.snap.png) | +| `retainingring` | Split C-ring with lug holes | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/RetainingRing.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/retainingring.snap.png) | +| `hairpincotter` | Bent hairpin wire with curved retention leg | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/HairpinCotter.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/hairpincotter.snap.png) | + +## Motion Transmission And Bearings (20) + +| Model | Distinguishing geometry | Example and four-view snapshot | +| --- | --- | --- | +| `pillowblockbearing` | Bearing in foot-mounted housing | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PillowBlockBearing.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/pillowblockbearing.snap.png) | +| `flangebearing` | Bearing in pierced flange housing | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/FlangeBearing.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/flangebearing.snap.png) | +| `needlebearing` | Races and slender roller cage | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/NeedleBearing.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/needlebearing.snap.png) | +| `cylindricalrollerbearing` | Races and large cylindrical rollers | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/CylindricalRollerBearing.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/cylindricalrollerbearing.snap.png) | +| `camfollower` | Bearing wheel with projecting stud | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/CamFollower.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/camfollower.snap.png) | +| `vgroovewheel` | Wheel with V running groove | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/VGrooveWheel.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/vgroovewheel.snap.png) | +| `rodend` | Eye with spherical bearing and stem | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/RodEnd.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/rodend.snap.png) | +| `sphericalbearing` | Bored spherical inner member in outer race | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/SphericalBearing.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/sphericalbearing.snap.png) | +| `clevis` | Fork with two cross-pierced ears | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/Clevis.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/clevis.snap.png) | +| `universaljoint` | Two yokes connected by cross | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/UniversalJoint.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/universaljoint.snap.png) | +| `jawcoupling` | Opposed jaw hubs and center spider | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/JawCoupling.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/jawcoupling.snap.png) | +| `beamcoupling` | Sleeve with helical flexure slots | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/BeamCoupling.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/beamcoupling.snap.png) | +| `oldhamcoupling` | Two hubs and orthogonally grooved center disk | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/OldhamCoupling.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/oldhamcoupling.snap.png) | +| `gearrack` | Linear tooth row on base | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/GearRack.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/gearrack.snap.png) | +| `sprocket` | Chain-toothed wheel and bore | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/Sprocket.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/sprocket.snap.png) | +| `rollerchain` | Repeated paired plate, pin and roller links | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/RollerChain.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/rollerchain.snap.png) | +| `vbeltsheave` | Pulley with V belt groove | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/VBeltSheave.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/vbeltsheave.snap.png) | +| `conveyorroller` | Drum with end bearings and axle | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/ConveyorRoller.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/conveyorroller.snap.png) | +| `shaftsupport` | Split bore pedestal on pierced feet | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/ShaftSupport.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/shaftsupport.snap.png) | +| `bevelgear` | Conical tooth wheel and hub | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/BevelGear.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/bevelgear.snap.png) | + +## Pipes Fittings And Valves (20) + +| Model | Distinguishing geometry | Example and four-view snapshot | +| --- | --- | --- | +| `rigidpipe` | Annular rigid pipe with cut ends | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/RigidPipe.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-rigidpipe.snap.png) | +| `pipeelbow` | Right-angle curved bore | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PipeElbow.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-pipeelbow.snap.png) | +| `pipetee` | Three intersecting ports | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PipeTee.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-pipetee.snap.png) | +| `pipecross` | Four intersecting ports | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PipeCross.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-pipecross.snap.png) | +| `pipecoupling` | Thick annular coupling sleeve | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PipeCoupling.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-pipecoupling.snap.png) | +| `pipeunion` | Two pipe ends with central hex union | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PipeUnion.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-pipeunion.snap.png) | +| `pipenipple` | Short pipe with raised threaded end envelopes | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PipeNipple.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-pipenipple.snap.png) | +| `pipecap` | Closed end with blind bore | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PipeCap.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-pipecap.snap.png) | +| `pipeplug` | Solid hex-headed plug | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PipePlug.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-pipeplug.snap.png) | +| `reducingbushing` | Hex head and stepped bore | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/ReducingBushing.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-reducingbushing.snap.png) | +| `hosebarb` | Raised conical barb rings and bore | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/HoseBarb.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-hosebarb.snap.png) | +| `bulkheadfitting` | Tube with flange and locknut | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/BulkheadFitting.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-bulkheadfitting.snap.png) | +| `pushconnectfitting` | Fitting body with collet and bore | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PushConnectFitting.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-pushconnectfitting.snap.png) | +| `compressionfitting` | Fitting body with compression nuts | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/CompressionFitting.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-compressionfitting.snap.png) | +| `hoseclamp` | Clamping band and screw housing | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/HoseClamp.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-hoseclamp.snap.png) | +| `pipeflange` | Annulus with bolt circle | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PipeFlange.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-pipeflange.snap.png) | +| `ballvalve` | Inline valve body, ports and lever | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/BallValve.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-ballvalve.snap.png) | +| `checkvalve` | Inline valve with identifiable direction feature | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/CheckValve.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-checkvalve.snap.png) | +| `needlevalve` | Valve body with stem and handwheel | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/NeedleValve.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-needlevalve.snap.png) | +| `manifold` | Rectangular bar with intersecting port array | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/Manifold.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/industrial-manifold.snap.png) | + +## Industrial Automation (20) + +| Model | Distinguishing geometry | Example and four-view snapshot | +| --- | --- | --- | +| `pneumaticcylinder` | Barrel, end caps and extending rod | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PneumaticCylinder.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/pneumaticcylinder.snap.png) | +| `cylindermount` | Cylinder bracket with mounting or pivot bores | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/CylinderMount.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/cylindermount.snap.png) | +| `airfilter` | Ported head and filter bowl | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/AirFilter.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/airfilter.snap.png) | +| `airregulator` | Ported body and adjustment knob | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/AirRegulator.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/airregulator.snap.png) | +| `airlubricator` | Ported head, oil bowl and sight dome | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/AirLubricator.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/airlubricator.snap.png) | +| `solenoidvalve` | Multiport block with coil | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/SolenoidValve.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/solenoidvalve.snap.png) | +| `pressureswitch` | Pressure connection, body and electrical head | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PressureSwitch.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/pressureswitch.snap.png) | +| `pressuregauge` | Round case, readable dial features and stem | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PressureGauge.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/pressuregauge.snap.png) | +| `flowmeter` | Transparent measuring tube with float and end fittings | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/FlowMeter.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/flowmeter.snap.png) | +| `vacuumcup` | Bell-shaped suction cup and neck | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/VacuumCup.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/vacuumcup.snap.png) | +| `inductiveproximitysensor` | Cylindrical sensing face, locknuts and cable | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/InductiveProximitySensor.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/inductiveproximitysensor.snap.png) | +| `photoelectricsensor` | Rectangular housing, lenses and mount bores | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PhotoelectricSensor.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/photoelectricsensor.snap.png) | +| `limitswitch` | Housing with lever and roller | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/LimitSwitch.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/limitswitch.snap.png) | +| `pushbuttonoperator` | Panel barrel, bezel and plunger | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PushbuttonOperator.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/pushbuttonoperator.snap.png) | +| `emergencystopswitch` | Mushroom button and panel barrel | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/EmergencyStopSwitch.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/emergencystopswitch.snap.png) | +| `selectoroperator` | Panel barrel and rotary lever | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/SelectorOperator.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/selectoroperator.snap.png) | +| `pilotlight` | Bezel, lens and panel barrel | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/PilotLight.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/pilotlight.snap.png) | +| `dinrail` | Top-hat section with slotted web | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/DinRail.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/dinrail.snap.png) | +| `terminalblock` | DIN foot, insulated body and screw wells | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/TerminalBlock.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/terminalblock.snap.png) | +| `wireduct` | Slotted channel sidewalls and removable cover | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/WireDuct.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/wireduct.snap.png) | + +## Machine Hardware And Structural (20) + +| Model | Distinguishing geometry | Example and four-view snapshot | +| --- | --- | --- | +| `hinge` | Leaves, alternating knuckles and pin | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/Hinge.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/hinge.snap.png) | +| `barrelhinge` | Two barrels and connecting pin | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/BarrelHinge.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/barrelhinge.snap.png) | +| `drawlatch` | Keeper, hook and lever base | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/DrawLatch.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/drawlatch.snap.png) | +| `toggleclamp` | Mount base, pivoting links and clamp foot | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/ToggleClamp.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/toggleclamp.snap.png) | +| `levelingfoot` | Base pad and adjusting stud | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/LevelingFoot.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/levelingfoot.snap.png) | +| `caster` | Fork, wheel, top plate and swivel | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/Caster.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/caster.snap.png) | +| `rubberbumper` | Resilient dome, flat bottom and mounting bore | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/RubberBumper.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/rubberbumper.snap.png) | +| `bridgehandle` | Two mounting feet and raised grip | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/BridgeHandle.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/bridgehandle.snap.png) | +| `handwheel` | Rim, spokes and hub | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/Handwheel.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/handwheel.snap.png) | +| `starknob` | Lobed grip and mounting bore | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/StarKnob.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/starknob.snap.png) | +| `leverhandle` | Mounting hub and offset lever | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/LeverHandle.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/leverhandle.snap.png) | +| `squarekey` | Rectangular shaft key with end chamfers | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/SquareKey.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/squarekey.snap.png) | +| `woodruffkey` | Half-disk shaft key | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/WoodruffKey.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/woodruffkey.snap.png) | +| `ibeam` | I-shaped structural section | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/IBeam.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/ibeam.snap.png) | +| `teebar` | T-shaped structural section | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/TeeBar.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/teebar.snap.png) | +| `roundbar` | Solid round stock with cut ends | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/RoundBar.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/roundbar.snap.png) | +| `splitringhanger` | Split pipe ring with top mounting boss | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/SplitRingHanger.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/splitringhanger.snap.png) | +| `detentpin` | Grip, smooth shaft and tip detent ball | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/DetentPin.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/detentpin.snap.png) | +| `expandedmetal` | Diamond mesh of connected strands | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/ExpandedMetal.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/expandedmetal.snap.png) | +| `coolingfan` | Square frame, circular shroud, hub and blades | [example](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/examples/CoolingFan.example.tsx) / [snapshot](https://github.com/tscircuit/jscad-electronics/blob/codex/industrial-models-100/tests/snapshots/__snapshots__/coolingfan.snap.png) | + +## Example strings + +Use these through `mp.string(source).json()`, `Footprinter3d`, or the synchronous vanilla model renderer. Full parameter contracts live in `src/models//schema.ts` in modelprinter. + +| Model | Example string | +| --- | --- | +| `flatwasher` | `flatwasher_id6.4mm_od12mm_t1.6mm` | +| `toothedwasher` | `toothedwasher_id6.4mm_od13mm_t0.8mm_td1.5mm` | +| `wavewasher` | `wavewasher_id10mm_od16mm_t0.6mm_wh1.5mm` | +| `bellevillewasher` | `bellevillewasher_id6.4mm_od14mm_t0.8mm_ch1.5mm` | +| `shoulderbolt` | `shoulderbolt_sd8mm_sl20mm_td6mm_tl10mm_hd13mm_hh5mm` | +| `ubolt` | `ubolt_d6mm_s30mm_l35mm` | +| `eyebolt` | `eyebolt_d6mm_l24mm_id14mm_wd5mm` | +| `carriagebolt` | `carriagebolt_d6mm_l25mm_nw6.5mm_nh3mm_hd14mm_hh4mm` | +| `wingnut` | `wingnut_id6mm_af10mm_h5mm_s28mm_wh10mm_wt3mm` | +| `capnut` | `capnut_id6mm_af10mm_h5mm_ch5mm_wt1mm` | +| `couplingnut` | `couplingnut_id6mm_af10mm_h18mm` | +| `tslotnut` | `tslotnut_id6mm_w16mm_l20mm_bh4mm_nw10mm_nh4mm` | +| `rivetnut` | `rivetnut_id6mm_d9mm_l14mm_fd12mm_ft1.5mm` | +| `solidrivet` | `solidrivet_d4mm_l12mm_hd8mm_hh2.5mm` | +| `blindrivet` | `blindrivet_d4mm_l10mm_fd8mm_ft1.2mm_md2mm_ml20mm` | +| `clevispin` | `clevispin_d6mm_l25mm_hd10mm_hh2mm_hol2mm_ho3mm` | +| `springpin` | `springpin_od6mm_l20mm_wt1mm_g1mm` | +| `taperpin` | `taperpin_ld6mm_sd5mm_l30mm` | +| `retainingring` | `retainingring_id16mm_od20mm_t1.2mm_g5mm_ld4mm_hol1.5mm` | +| `hairpincotter` | `hairpincotter_d2mm_l40mm_ld10mm` | +| `pillowblockbearing` | `pillowblockbearing_bore20mm_bw80mm_bd30mm_bt8mm_hd46mm_ch30mm_ms60mm_mh6mm` | +| `flangebearing` | `flangebearing_bore20mm_fw64mm_ft6mm_hd44mm_hh20mm_ms46mm_mh6mm` | +| `needlebearing` | `needlebearing_id20mm_od30mm_w20mm_race1.5mm_rollers18` | +| `cylindricalrollerbearing` | `cylindricalrollerbearing_id20mm_od42mm_w16mm_race3mm_rollers12` | +| `camfollower` | `camfollower_od32mm_w14mm_stud10mm_sl26mm_socket5mm_rollers10` | +| `vgroovewheel` | `vgroovewheel_od40mm_root28mm_bore8mm_w12mm` | +| `rodend` | `rodend_hd28mm_bore12mm_w12mm_stem10mm_sl30mm` | +| `sphericalbearing` | `sphericalbearing_od30mm_bore12mm_w14mm_ball22mm` | +| `clevis` | `clevis_w30mm_l45mm_h20mm_gap14mm_back10mm_pin8mm_mount10mm` | +| `universaljoint` | `universaljoint_od30mm_l60mm_bore8mm_hubl14mm_armt5mm_crossd6mm_clearance0.2mm` | +| `jawcoupling` | `jawcoupling_od30mm_l40mm_bore8mm_hubl12mm_jawd12mm_jawcount3` | +| `beamcoupling` | `beamcoupling_od25mm_l30mm_bore8mm_hubl7mm_slotw1mm_slots6_bridgeangle60` | +| `oldhamcoupling` | `oldhamcoupling_od30mm_l36mm_bore8mm_hubl12mm_slotw7mm_slotd3mm_clearance0.2mm` | +| `gearrack` | `gearrack_pitch8mm_teeth12_w12mm_h8mm_th4mm_mh3.5mm` | +| `sprocket` | `sprocket_od60mm_root48mm_w8mm_bore12mm_teeth16_hub26mm_hh10mm` | +| `rollerchain` | `rollerchain_pitch12.7mm_links6_roller7.9mm_w8mm_pin4mm_plate1.2mm` | +| `vbeltsheave` | `vbeltsheave_od60mm_bore12mm_w24mm_grooves2_depth8mm_gw9mm_hub26mm_hh8mm` | +| `conveyorroller` | `conveyorroller_od50mm_l150mm_wall3mm_shaft10mm_ext15mm_end4mm` | +| `shaftsupport` | `shaftsupport_bore16mm_bw55mm_bd32mm_bt7mm_hw26mm_hd20mm_hh34mm_ch26mm_ms40mm_mh5mm_slit1.5mm_clamp4mm` | +| `bevelgear` | `bevelgear_od40mm_td24mm_h14mm_bore8mm_depth3mm_hubd20mm_hubh8mm_teeth16` | +| `rigidpipe` | `rigidpipe_od20mm_bore16mm_l100mm` | +| `pipeelbow` | `pipeelbow_od24mm_bore16mm_leg36mm_socket28mm_socketl10mm` | +| `pipetee` | `pipetee_od24mm_bore16mm_l70mm_branch35mm_socket28mm_socketl10mm` | +| `pipecross` | `pipecross_od24mm_bore16mm_span70mm_socket28mm_socketl10mm` | +| `pipecoupling` | `pipecoupling_od30mm_bore16mm_socket20mm_l40mm_depth14mm` | +| `pipeunion` | `pipeunion_od30mm_bore16mm_l55mm_af36mm_nutl20mm_endod24mm` | +| `pipenipple` | `pipenipple_od20mm_bore16mm_l50mm_threadl12mm_shoulder22mm` | +| `pipecap` | `pipecap_od30mm_socket20mm_l25mm_floor4mm_af32mm` | +| `pipeplug` | `pipeplug_d20mm_l20mm_af26mm_head8mm` | +| `reducingbushing` | `reducingbushing_od26mm_largebore18mm_smallbore10mm_l30mm_af30mm_hexl10mm` | +| `hosebarb` | `hosebarb_od20mm_bore10mm_l45mm_barbod16mm_barbl25mm_pitch6mm_af24mm` | +| `bulkheadfitting` | `bulkheadfitting_od20mm_bore12mm_l55mm_flange32mm_flanget4mm_af28mm_nutt8mm_gap10mm` | +| `pushconnectfitting` | `pushconnectfitting_od26mm_bore8mm_l40mm_tube12mm_collet20mm_colletl6mm_af28mm` | +| `compressionfitting` | `compressionfitting_od22mm_bore8mm_l40mm_tube12mm_af26mm_nutl10mm` | +| `hoseclamp` | `hoseclamp_id30mm_bandt1.5mm_bandw8mm_housingl14mm_housingw8mm_housingh7mm` | +| `pipeflange` | `pipeflange_flange70mm_bore16mm_hub30mm_flanget8mm_hubl20mm_bc50mm_hole6mm_bolts4` | +| `ballvalve` | `ballvalve_od30mm_bore16mm_l65mm_endod24mm_handlel55mm_handlew10mm_stemh14mm_handlet4mm` | +| `checkvalve` | `checkvalve_od28mm_bore12mm_l60mm_endod20mm_af30mm_nutl12mm` | +| `needlevalve` | `needlevalve_bodyl38mm_bodyw24mm_bodyh26mm_bore8mm_port16mm_portl10mm_stemh24mm_wheel26mm_wheelt5mm` | +| `manifold` | `manifold_l100mm_w30mm_h26mm_bore12mm_port8mm_pitch25mm_ports3_hole5mm` | +| `pneumaticcylinder` | `pneumaticcylinder_borediameter32mm_bodylength80mm_roddiameter12mm_stroke50mm_extension25mm_endcaplength10mm_portdiameter6mm` | +| `cylindermount` | `cylindermount_width50mm_depth25mm_height45mm_thickness5mm_borediameter20mm_holediameter5mm` | +| `airfilter` | `airfilter_bodywidth40mm_bodydepth40mm_bodyheight30mm_bowldiameter28mm_bowllength50mm_portdiameter8mm_drainlength8mm` | +| `airregulator` | `airregulator_bodywidth40mm_bodydepth36mm_bodyheight30mm_knobdiameter28mm_knobheight20mm_portdiameter8mm_gaugediameter20mm` | +| `airlubricator` | `airlubricator_bodywidth40mm_bodydepth40mm_bodyheight30mm_bowldiameter28mm_bowllength50mm_portdiameter8mm_filldiameter12mm_fillheight8mm` | +| `solenoidvalve` | `solenoidvalve_length70mm_width28mm_height25mm_coilwidth24mm_coilheight30mm_portdiameter6mm` | +| `pressureswitch` | `pressureswitch_width32mm_depth28mm_height38mm_portdiameter8mm_portlength10mm` | +| `pressuregauge` | `pressuregauge_diameter60mm_depth22mm_stemdiameter10mm_stemlength15mm` | +| `flowmeter` | `flowmeter_length120mm_diameter24mm_borediameter12mm_enddiameter32mm_endlength12mm_floatdiameter8mm` | +| `vacuumcup` | `vacuumcup_diameter35mm_height22mm_neckdiameter12mm_necklength12mm_wallthickness2mm_borediameter6mm` | +| `inductiveproximitysensor` | `inductiveproximitysensor_barreldiameter18mm_barrellength55mm_facethickness2mm_cablediameter5mm_cablelength15mm_nutwidth22mm_nutthickness4mm` | +| `photoelectricsensor` | `photoelectricsensor_width20mm_depth35mm_height55mm_lensdiameter12mm_cablediameter5mm_mountholediameter4mm` | +| `limitswitch` | `limitswitch_width30mm_depth20mm_height55mm_actuatorlength30mm_rollerdiameter12mm_leverthickness3mm_holediameter4mm` | +| `pushbuttonoperator` | `pushbuttonoperator_paneldiameter22mm_bezeldiameter30mm_buttondiameter22mm_bodylength35mm_buttonheight8mm` | +| `emergencystopswitch` | `emergencystopswitch_paneldiameter22mm_headdiameter40mm_bodylength45mm_headheight18mm_collardiameter30mm` | +| `selectoroperator` | `selectoroperator_paneldiameter22mm_bezeldiameter30mm_bodylength35mm_knobwidth28mm_knobdepth10mm_knobheight15mm` | +| `pilotlight` | `pilotlight_paneldiameter22mm_lensdiameter28mm_bodylength28mm_lensheight8mm` | +| `dinrail` | `dinrail_width35mm_height7.5mm_thickness1mm_length100mm_holediameter5mm_slotlength10mm_slotpitch25mm` | +| `terminalblock` | `terminalblock_width6mm_depth40mm_height35mm_wirediameter3mm_screwdiameter4mm` | +| `wireduct` | `wireduct_width40mm_height40mm_length100mm_thickness2mm_slotwidth4mm_slotpitch8mm_holediameter5mm` | +| `hinge` | `hinge_width60mm_length60mm_leafthickness3mm_barreldiameter10mm_pindiameter4mm_holediameter5mm` | +| `barrelhinge` | `barrelhinge_height60mm_barreldiameter14mm_pindiameter6mm_leafwidth20mm_leafthickness4mm_holediameter5mm` | +| `drawlatch` | `drawlatch_length100mm_width25mm_basethickness4mm_handleheight18mm_wirediameter4mm_holediameter4mm` | +| `toggleclamp` | `toggleclamp_length110mm_width35mm_basethickness5mm_handleheight55mm_armlength75mm_paddiameter14mm_holediameter5mm` | +| `levelingfoot` | `levelingfoot_basediameter50mm_baseheight12mm_stemdiameter12mm_stemlength55mm_hexdiameter24mm_hexheight8mm` | +| `caster` | `caster_wheeldiameter75mm_wheelwidth24mm_axlediameter10mm_forkthickness5mm_platewidth50mm_platelength65mm_platethickness5mm_overallheight110mm_holediameter5mm` | +| `rubberbumper` | `rubberbumper_diameter30mm_height15mm_holediameter6mm_counterborediameter13mm_counterboredepth5mm` | +| `bridgehandle` | `bridgehandle_length120mm_gripdiameter15mm_height40mm_mountingdiameter24mm_mountingheight5mm_holediameter5mm` | +| `handwheel` | `handwheel_outerdiameter100mm_rimwidth12mm_hubdiameter28mm_hubheight25mm_borediameter12mm_spokes4` | +| `starknob` | `starknob_diameter40mm_height16mm_borediameter6mm_lobes5_hubdiameter18mm_hubheight10mm` | +| `leverhandle` | `leverhandle_length90mm_width18mm_height16mm_hubdiameter28mm_hubheight25mm_borediameter8mm` | +| `squarekey` | `squarekey_width8mm_height8mm_length40mm_chamfer1mm` | +| `woodruffkey` | `woodruffkey_diameter30mm_thickness6mm_height12mm` | +| `ibeam` | `ibeam_width40mm_height60mm_webthickness4mm_flangethickness6mm_length100mm` | +| `teebar` | `teebar_width40mm_height40mm_webthickness4mm_flangethickness6mm_length100mm` | +| `roundbar` | `roundbar_diameter20mm_length100mm_chamfer1mm` | +| `splitringhanger` | `splitringhanger_pipediameter32mm_bandthickness3mm_bandwidth15mm_gap2mm_lugwidth14mm_holediameter5mm` | +| `detentpin` | `detentpin_shaftdiameter8mm_shaftlength50mm_headdiameter18mm_headheight6mm_balldiameter3mm_balloffset4mm_ringdiameter24mm_ringwire2mm` | +| `expandedmetal` | `expandedmetal_width100mm_length100mm_thickness2mm_diamondwidth15mm_diamondlength30mm_strandwidth2mm_framewidth3mm` | +| `coolingfan` | `coolingfan_size80mm_thickness25mm_hubdiameter28mm_bladecount7_bladewidth12mm_bladethickness3mm_mountingholediameter4mm_framethickness6mm` | diff --git a/docs/industrial-fasteners.md b/docs/industrial-fasteners.md new file mode 100644 index 0000000..559c4fa --- /dev/null +++ b/docs/industrial-fasteners.md @@ -0,0 +1,53 @@ +# Industrial fastener families + +These models describe nominal visual assembly envelopes, not manufacturing standards or load ratings. Threaded bolt/nut/insert surfaces are smooth cylinders; no helical thread geometry, fit class, or ISO compliance is claimed. Dimensions use millimeters; explicit mm, cm, m, in, inch, mil, ft, and feet tokens are accepted. + +All dimensions must be finite, positive, and within 0.0001–10000 mm. Every feature dimension must also be at least 0.0001 times the largest dimension to avoid unstable, near-degenerate geometry. Minimum material and clearance gaps also use that relative bound. Model-specific fit checks reject bores larger than bodies, intersecting features, invalid taper direction, and insufficient material. Unknown/duplicate tokens and inline first-token values are rejected. Long property names and the short aliases below are equivalent. + +Unless specified otherwise, a model is centered around the Z axis with its lower end at Z=0. U bolts lie in the XZ plane with wire thickness along Y, and eye bolts have their eye in XZ. Hairpin cotters lie in XY with their wire above Z=0. + +| Family | Geometry | Aliases and defaults (mm) | +| --- | --- | --- | +| `flatwasher` | Flat annulus with an open through bore. | `id` / `innerDiameter` = 6.4, `od` / `outerDiameter` = 12, `t` / `thickness` = 1.6 | +| `toothedwasher` | External 12-tooth washer with relief between teeth; flat uninstalled envelope. | `id` / `innerDiameter` = 6.4, `od` / `outerDiameter` = 13, `t` / `thickness` = 0.8, `td` / `toothDepth` = 1.5 | +| `wavewasher` | Three-wave annular spring washer; waveHeight is peak-to-trough rise in addition to thickness. | `id` / `innerDiameter` = 10, `od` / `outerDiameter` = 16, `t` / `thickness` = 0.6, `wh` / `waveHeight` = 1.5 | +| `bellevillewasher` | Conical annulus; coneHeight is inner-edge rise above the outer edge, in addition to axial thickness. | `id` / `innerDiameter` = 6.4, `od` / `outerDiameter` = 14, `t` / `thickness` = 0.8, `ch` / `coneHeight` = 1.5 | +| `shoulderbolt` | Reduced thread envelope, larger precision shoulder, and cylindrical head with recessed hex socket. | `sd` / `shoulderDiameter` = 8, `sl` / `shoulderLength` = 20, `td` / `threadDiameter` = 6, `tl` / `threadLength` = 10, `hd` / `headDiameter` = 13, `hh` / `headHeight` = 5 | +| `ubolt` | Round-wire U bend with two parallel legs; legSpacing is center-to-center and legLength is the straight portion. | `d` / `rodDiameter` = 6, `s` / `legSpacing` = 30, `l` / `legLength` = 35 | +| `eyebolt` | Smooth shank and closed round-wire eye in the XZ plane; eyeInnerDiameter is the clear eye diameter. | `d` / `shaftDiameter` = 6, `l` / `shaftLength` = 24, `id` / `eyeInnerDiameter` = 14, `wd` / `eyeWireDiameter` = 5 | +| `carriagebolt` | Round shank, square antirotation neck, and spherical-cap head. | `d` / `shaftDiameter` = 6, `l` / `shaftLength` = 25, `nw` / `neckWidth` = 6.5, `nh` / `neckHeight` = 3, `hd` / `headDiameter` = 14, `hh` / `headHeight` = 4 | +| `wingnut` | Hexagonal nut body with two swept-profile wings and an open cylindrical thread envelope. | `id` / `boreDiameter` = 6, `af` / `acrossFlats` = 10, `h` / `bodyHeight` = 5, `s` / `wingSpan` = 28, `wh` / `wingHeight` = 10, `wt` / `wingThickness` = 3 | +| `capnut` | Hexagonal base and ellipsoidal closed dome; blind bore terminates wallThickness below the base-to-dome plane. | `id` / `boreDiameter` = 6, `af` / `acrossFlats` = 10, `h` / `bodyHeight` = 5, `ch` / `capHeight` = 5, `wt` / `wallThickness` = 1 | +| `couplingnut` | Elongated hexagonal body with an open cylindrical thread envelope. | `id` / `boreDiameter` = 6, `af` / `acrossFlats` = 10, `h` / `height` = 18 | +| `tslotnut` | Rectangular wider base, narrower raised neck, and central open thread envelope. | `id` / `boreDiameter` = 6, `w` / `baseWidth` = 16, `l` / `length` = 20, `bh` / `baseHeight` = 4, `nw` / `neckWidth` = 10, `nh` / `neckHeight` = 4 | +| `rivetnut` | Flanged tubular insert with 24 shallow axial grip grooves and open thread envelope; uninstalled state. | `id` / `boreDiameter` = 6, `d` / `bodyDiameter` = 9, `l` / `bodyLength` = 14, `fd` / `flangeDiameter` = 12, `ft` / `flangeThickness` = 1.5 | +| `solidrivet` | Solid shank and spherical-cap factory head; uninstalled state. | `d` / `shaftDiameter` = 4, `l` / `shaftLength` = 12, `hd` / `headDiameter` = 8, `hh` / `headHeight` = 2.5 | +| `blindrivet` | Tubular flanged body, protruding mandrel, and mandrel upset head; uninstalled state. | `d` / `bodyDiameter` = 4, `l` / `bodyLength` = 10, `fd` / `flangeDiameter` = 8, `ft` / `flangeThickness` = 1.2, `md` / `mandrelDiameter` = 2, `ml` / `mandrelLength` = 20 | +| `clevispin` | Headed pin with a real transverse through hole; holeOffset is measured from the unheaded end. | `d` / `shaftDiameter` = 6, `l` / `shaftLength` = 25, `hd` / `headDiameter` = 10, `hh` / `headHeight` = 2, `hol` / `holeDiameter` = 2, `ho` / `holeOffset` = 3 | +| `springpin` | Slotted hollow cylindrical pin; gapWidth is the chord width of the straight radial slot. | `od` / `outerDiameter` = 6, `l` / `length` = 20, `wt` / `wallThickness` = 1, `g` / `gapWidth` = 1 | +| `taperpin` | Smooth conical pin between independently specified smallDiameter and largeDiameter. | `ld` / `largeDiameter` = 6, `sd` / `smallDiameter` = 5, `l` / `length` = 30 | +| `retainingring` | External C ring with two eyelet lugs and open plier holes; ring opening points toward +X. | `id` / `innerDiameter` = 16, `od` / `outerDiameter` = 20, `t` / `thickness` = 1.2, `g` / `gapWidth` = 5, `ld` / `lugDiameter` = 4, `hol` / `holeDiameter` = 1.5 | +| `hairpincotter` | One continuous round wire with a return loop, straight leg, and three waved retention bends in the XY plane. | `d` / `wireDiameter` = 2, `l` / `length` = 40, `ld` / `loopDiameter` = 10 | + +## Examples + +`flatwasher_id6.4mm_od12mm_t1.6mm` +`toothedwasher_id6.4mm_od13mm_t0.8mm_td1.5mm` +`wavewasher_id10mm_od16mm_t0.6mm_wh1.5mm` +`bellevillewasher_id6.4mm_od14mm_t0.8mm_ch1.5mm` +`shoulderbolt_sd8mm_sl20mm_td6mm_tl10mm_hd13mm_hh5mm` +`ubolt_d6mm_s30mm_l35mm` +`eyebolt_d6mm_l24mm_id14mm_wd5mm` +`carriagebolt_d6mm_l25mm_nw6.5mm_nh3mm_hd14mm_hh4mm` +`wingnut_id6mm_af10mm_h5mm_s28mm_wh10mm_wt3mm` +`capnut_id6mm_af10mm_h5mm_ch5mm_wt1mm` +`couplingnut_id6mm_af10mm_h18mm` +`tslotnut_id6mm_w16mm_l20mm_bh4mm_nw10mm_nh4mm` +`rivetnut_id6mm_d9mm_l14mm_fd12mm_ft1.5mm` +`solidrivet_d4mm_l12mm_hd8mm_hh2.5mm` +`blindrivet_d4mm_l10mm_fd8mm_ft1.2mm_md2mm_ml20mm` +`clevispin_d6mm_l25mm_hd10mm_hh2mm_hol2mm_ho3mm` +`springpin_od6mm_l20mm_wt1mm_g1mm` +`taperpin_ld6mm_sd5mm_l30mm` +`retainingring_id16mm_od20mm_t1.2mm_g5mm_ld4mm_hol1.5mm` +`hairpincotter_d2mm_l40mm_ld10mm` diff --git a/docs/industrial-model-pr-audit.json b/docs/industrial-model-pr-audit.json new file mode 100644 index 0000000..59aabf7 --- /dev/null +++ b/docs/industrial-model-pr-audit.json @@ -0,0 +1,3023 @@ +{ + "auditDate": "2026-10-13", + "author": "seveibar", + "scope": "All historical authored PRs, including open, merged, closed without merge; current main trees; main Footprinter3d route; source-index searches for alternate names.", + "retrieval": { + "method": "GitHub issue-search collection through github_fetch", + "query": "repo: is:pr author:seveibar", + "perPage": 100, + "pagesFetched": [1, 2], + "allStates": true, + "incompleteResults": false + }, + "repositories": [ + { + "name": "tscircuit/modelprinter", + "totalHistoricalAuthoredPullRequests": 104, + "mainModelFamilies": [ + "adhesivemountelectricalcomponentheatsink", + "anglebar", + "ballbearing", + "balltransferunit", + "buttonscrew", + "cablegrommet", + "channelbar", + "clampingshaftcollar", + "compressionspring", + "dowelpin", + "femalestandoff", + "flangebolt", + "flangedbushing", + "flangenut", + "flatcableclip", + "flatgasket", + "flatheadscrew", + "flexscreen", + "hatsection", + "heatsetinsert", + "helicalgear", + "hexbolt", + "hexnut", + "hexsocketbolt", + "hollowpositioningarmtube", + "malefemalestandoff", + "nema", + "nylonlocknut", + "panscrew", + "perforatedangle", + "perforatedsheet", + "plainbushing", + "pushfitplug", + "rectangularbar", + "rectangulargasket", + "rectangulartube", + "rigidcoupler", + "setscrew", + "shaftcollar", + "sheetmetal", + "slottedchannel", + "spacer", + "splitgrommet", + "splitwasher", + "spurgear", + "threadedrod", + "torsionspring", + "tslotcoverstrip", + "tslotextrusion", + "tslotgusset", + "tslotinsidecorner", + "ufl", + "wormgear", + "zeebar" + ], + "pullRequests": [ + { + "number": 1, + "title": "Add metric hex socket bolt models and M3 \u00d7 6 mm visual snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/1", + "createdAt": "2026-09-21T23:32:30Z", + "mergedAt": "2026-09-21T23:49:29Z" + }, + { + "number": 2, + "title": "Configure modelprinter npm publishing and stop tracking generated files", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/2", + "createdAt": "2026-09-21T23:54:26Z", + "mergedAt": "2026-09-22T06:00:07Z" + }, + { + "number": 3, + "title": "Add parameterized sheet metal plates, brackets and channels", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/3", + "createdAt": "2026-09-23T19:34:22Z", + "mergedAt": "2026-09-23T19:49:45Z" + }, + { + "number": 4, + "title": "Publish modelprinter through GitHub Packages for jscdn", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/4", + "createdAt": "2026-09-23T19:52:34Z", + "mergedAt": "2026-09-23T20:31:17Z" + }, + { + "number": 5, + "title": "Use consumer-provided Zod as a wildcard peer dependency", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/5", + "createdAt": "2026-09-24T15:51:43Z", + "mergedAt": "2026-09-24T19:45:22Z" + }, + { + "number": 6, + "title": "Release modelprinter 0.0.4 with the Zod peer fix", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/6", + "createdAt": "2026-09-24T19:46:21Z", + "mergedAt": null + }, + { + "number": 7, + "title": "Add NEMA motor specs and move geometry to jscad-electronics", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/7", + "createdAt": "2026-10-02T06:38:56Z", + "mergedAt": "2026-10-02T07:53:40Z" + }, + { + "number": 8, + "title": "Publish modelprinter to npm using trusted publishing", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/8", + "createdAt": "2026-10-02T08:08:03Z", + "mergedAt": "2026-10-02T15:52:29Z" + }, + { + "number": 9, + "title": "Add NEMA rear-face holes and screw specifications", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/9", + "createdAt": "2026-10-02T18:58:32Z", + "mergedAt": "2026-10-02T20:43:22Z" + }, + { + "number": 10, + "title": "Add NEMA wire termination specs and named reference points", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/10", + "createdAt": "2026-10-03T18:44:05Z", + "mergedAt": "2026-10-03T19:24:16Z" + }, + { + "number": 11, + "title": "Add ModelPrinter specs for spur gears and worm screws", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/11", + "createdAt": "2026-10-03T23:00:49Z", + "mergedAt": "2026-10-03T23:10:44Z" + }, + { + "number": 12, + "title": "Add helical gear schemas, dimensions, and model strings", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/12", + "createdAt": "2026-10-04T00:32:46Z", + "mergedAt": "2026-10-04T00:47:30Z" + }, + { + "number": 19, + "title": "Add PCB card guide model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/19", + "createdAt": "2026-10-05T17:27:32Z", + "mergedAt": null + }, + { + "number": 20, + "title": "Add threaded rod model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/20", + "createdAt": "2026-10-05T17:28:39Z", + "mergedAt": "2026-10-06T23:53:07Z" + }, + { + "number": 21, + "title": "Add male pin header model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/21", + "createdAt": "2026-10-05T17:29:10Z", + "mergedAt": null + }, + { + "number": 22, + "title": "Add plain bushing model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/22", + "createdAt": "2026-10-05T17:31:29Z", + "mergedAt": "2026-10-06T23:52:38Z" + }, + { + "number": 23, + "title": "Add flanged bushing model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/23", + "createdAt": "2026-10-05T17:31:59Z", + "mergedAt": "2026-10-06T23:52:53Z" + }, + { + "number": 24, + "title": "Add ISO button screw model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/24", + "createdAt": "2026-10-05T17:33:30Z", + "mergedAt": "2026-10-06T23:53:01Z" + }, + { + "number": 25, + "title": "Add ISO hex nut model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/25", + "createdAt": "2026-10-05T17:33:58Z", + "mergedAt": "2026-10-06T23:52:47Z" + }, + { + "number": 26, + "title": "Add compression spring model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/26", + "createdAt": "2026-10-05T17:34:26Z", + "mergedAt": "2026-10-06T23:53:21Z" + }, + { + "number": 27, + "title": "Add shaft collar model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/27", + "createdAt": "2026-10-05T17:35:51Z", + "mergedAt": "2026-10-06T23:53:56Z" + }, + { + "number": 28, + "title": "Add clamping shaft collar model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/28", + "createdAt": "2026-10-05T17:36:21Z", + "mergedAt": "2026-10-06T23:53:48Z" + }, + { + "number": 29, + "title": "Add rigid shaft coupler model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/29", + "createdAt": "2026-10-05T17:36:49Z", + "mergedAt": "2026-10-06T23:54:03Z" + }, + { + "number": 30, + "title": "Add cable tie model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/30", + "createdAt": "2026-10-05T17:37:58Z", + "mergedAt": null + }, + { + "number": 31, + "title": "Add cable grommet model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/31", + "createdAt": "2026-10-05T17:38:26Z", + "mergedAt": "2026-10-06T23:53:14Z" + }, + { + "number": 32, + "title": "Add straight flexible hose model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/32", + "createdAt": "2026-10-05T17:38:55Z", + "mergedAt": null + }, + { + "number": 33, + "title": "Add T-slot extrusion model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/33", + "createdAt": "2026-10-05T17:39:55Z", + "mergedAt": "2026-10-06T23:53:34Z" + }, + { + "number": 34, + "title": "Add T-slot inside corner model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/34", + "createdAt": "2026-10-05T17:40:31Z", + "mergedAt": "2026-10-06T23:53:27Z" + }, + { + "number": 35, + "title": "Add T-slot triangular gusset model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/35", + "createdAt": "2026-10-05T17:41:03Z", + "mergedAt": "2026-10-06T23:53:40Z" + }, + { + "number": 36, + "title": "Add ISO hex bolt model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/36", + "createdAt": "2026-10-05T17:41:49Z", + "mergedAt": "2026-10-05T22:47:40Z" + }, + { + "number": 37, + "title": "Add ISO countersunk socket screw model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/37", + "createdAt": "2026-10-05T17:42:47Z", + "mergedAt": "2026-10-06T23:54:13Z" + }, + { + "number": 38, + "title": "Add ISO Phillips pan screw model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/38", + "createdAt": "2026-10-05T17:43:16Z", + "mergedAt": "2026-10-06T23:54:19Z" + }, + { + "number": 39, + "title": "Support FPC-style flexscreen contacts and tail dimensions", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/39", + "createdAt": "2026-10-05T18:09:30Z", + "mergedAt": "2026-10-05T22:47:42Z" + }, + { + "number": 40, + "title": "Discover models from per-model registrations", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/40", + "createdAt": "2026-10-06T01:22:28Z", + "mergedAt": "2026-10-06T20:02:37Z" + }, + { + "number": 41, + "title": "Preview combined model contracts for renderer integration", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/41", + "createdAt": "2026-10-06T20:23:39Z", + "mergedAt": null + }, + { + "number": 42, + "title": "Support generalized JST PH and SH motor wire connection aliases", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/42", + "createdAt": "2026-10-06T20:35:41Z", + "mergedAt": "2026-10-06T20:50:47Z" + }, + { + "number": 43, + "title": "Fix NEMA variant test typechecking to unblock npm release", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/43", + "createdAt": "2026-10-06T20:54:18Z", + "mergedAt": "2026-10-06T20:59:38Z" + }, + { + "number": 44, + "title": "Add leadscrew model contract with folder-local registration", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/44", + "createdAt": "2026-10-07T03:43:32Z", + "mergedAt": null + }, + { + "number": 45, + "title": "Add leadscrewnut model contract with folder-local registration", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/45", + "createdAt": "2026-10-07T03:43:58Z", + "mergedAt": null + }, + { + "number": 46, + "title": "Add linearrail model contract with folder-local registration", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/46", + "createdAt": "2026-10-07T03:44:24Z", + "mergedAt": null + }, + { + "number": 47, + "title": "Add linearcarriage model contract with folder-local registration", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/47", + "createdAt": "2026-10-07T03:45:10Z", + "mergedAt": null + }, + { + "number": 48, + "title": "Add timingpulley model contract with folder-local registration", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/48", + "createdAt": "2026-10-07T03:45:59Z", + "mergedAt": null + }, + { + "number": 49, + "title": "Add beltidler model contract with folder-local registration", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/49", + "createdAt": "2026-10-07T03:47:10Z", + "mergedAt": null + }, + { + "number": 50, + "title": "Add timingbelt model contract with folder-local registration", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/50", + "createdAt": "2026-10-07T03:47:50Z", + "mergedAt": null + }, + { + "number": 51, + "title": "Add NEMA motor mount contract with folder-local registration", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/51", + "createdAt": "2026-10-07T03:50:17Z", + "mergedAt": null + }, + { + "number": 52, + "title": "Add linearballbearing model contract with folder-local registration", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/52", + "createdAt": "2026-10-07T03:56:25Z", + "mergedAt": null + }, + { + "number": 53, + "title": "Add linearbearingblock model contract with folder-local registration", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/53", + "createdAt": "2026-10-07T03:56:25Z", + "mergedAt": null + }, + { + "number": 54, + "title": "[Preview only] Combined motion contracts for renderer review", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/54", + "createdAt": "2026-10-07T03:56:35Z", + "mergedAt": null + }, + { + "number": 55, + "title": "Add radial bearing shorthand and per-face flags", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/55", + "createdAt": "2026-10-07T03:58:02Z", + "mergedAt": "2026-10-07T06:28:37Z" + }, + { + "number": 60, + "title": "Add compatible mechanical model-string flag syntax", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/60", + "createdAt": "2026-10-08T05:04:50Z", + "mergedAt": "2026-10-08T05:46:57Z" + }, + { + "number": 63, + "title": "Use flags for ThreadedRod strings and JSON", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/63", + "createdAt": "2026-10-09T05:32:15Z", + "mergedAt": "2026-10-09T05:58:13Z" + }, + { + "number": 64, + "title": "Add balltransferunit model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/64", + "createdAt": "2026-10-09T06:05:16Z", + "mergedAt": "2026-10-09T15:50:43Z" + }, + { + "number": 65, + "title": "Add metric and imperial UNC hex nut sizes", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/65", + "createdAt": "2026-10-09T17:15:44Z", + "mergedAt": "2026-10-09T17:27:45Z" + }, + { + "number": 66, + "title": "Add round spacer model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/66", + "createdAt": "2026-10-09T18:03:22Z", + "mergedAt": "2026-10-09T21:36:06Z" + }, + { + "number": 67, + "title": "Add female hex standoff model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/67", + "createdAt": "2026-10-09T18:04:18Z", + "mergedAt": "2026-10-09T21:36:12Z" + }, + { + "number": 68, + "title": "Add male-female hex standoff model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/68", + "createdAt": "2026-10-09T18:04:23Z", + "mergedAt": "2026-10-09T21:36:17Z" + }, + { + "number": 69, + "title": "Add nylon insert lock nut model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/69", + "createdAt": "2026-10-09T18:04:27Z", + "mergedAt": "2026-10-09T21:36:24Z" + }, + { + "number": 70, + "title": "Add plain-face hex flange nut model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/70", + "createdAt": "2026-10-09T18:04:31Z", + "mergedAt": "2026-10-09T21:36:31Z" + }, + { + "number": 71, + "title": "Add fully threaded bolt with ISO 4162 head geometry", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/71", + "createdAt": "2026-10-09T18:04:36Z", + "mergedAt": "2026-10-09T21:36:37Z" + }, + { + "number": 72, + "title": "Add hex socket cup-point set screw model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/72", + "createdAt": "2026-10-09T18:04:39Z", + "mergedAt": "2026-10-09T21:36:42Z" + }, + { + "number": 73, + "title": "Add rectangular split washer model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/73", + "createdAt": "2026-10-09T18:06:15Z", + "mergedAt": "2026-10-09T19:03:27Z" + }, + { + "number": 74, + "title": "Add nominal ISO 8734 dowel pin model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/74", + "createdAt": "2026-10-09T18:12:07Z", + "mergedAt": "2026-10-09T21:01:10Z" + }, + { + "number": 75, + "title": "Add diamond-knurled heat-set insert model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/75", + "createdAt": "2026-10-09T18:12:47Z", + "mergedAt": "2026-10-09T21:01:23Z" + }, + { + "number": 76, + "title": "Preview combined model contracts for continuous renderer reviews", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/76", + "createdAt": "2026-10-09T19:23:37Z", + "mergedAt": null + }, + { + "number": 78, + "title": "Default dowel pins to ISO 8734 and use a boolean ISO flag", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/78", + "createdAt": "2026-10-09T21:09:39Z", + "mergedAt": "2026-10-09T21:36:48Z" + }, + { + "number": 79, + "title": "Use defaulted ISO flags for existing fastener models", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/79", + "createdAt": "2026-10-09T21:15:17Z", + "mergedAt": "2026-10-09T21:36:54Z" + }, + { + "number": 80, + "title": "Add rectangularbar model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/80", + "createdAt": "2026-10-10T00:24:05Z", + "mergedAt": "2026-10-10T01:23:18Z" + }, + { + "number": 81, + "title": "Add rectangulartube model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/81", + "createdAt": "2026-10-10T00:24:07Z", + "mergedAt": "2026-10-10T01:23:11Z" + }, + { + "number": 82, + "title": "Add anglebar model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/82", + "createdAt": "2026-10-10T00:24:09Z", + "mergedAt": "2026-10-10T01:24:23Z" + }, + { + "number": 83, + "title": "Add channelbar model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/83", + "createdAt": "2026-10-10T00:24:11Z", + "mergedAt": "2026-10-10T01:24:21Z" + }, + { + "number": 84, + "title": "Add perforatedsheet model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/84", + "createdAt": "2026-10-10T00:24:13Z", + "mergedAt": "2026-10-10T01:24:10Z" + }, + { + "number": 85, + "title": "Add fixtureplate model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/85", + "createdAt": "2026-10-10T00:24:16Z", + "mergedAt": null + }, + { + "number": 86, + "title": "Add vblock model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/86", + "createdAt": "2026-10-10T00:24:18Z", + "mergedAt": null + }, + { + "number": 87, + "title": "Add stepblock model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/87", + "createdAt": "2026-10-10T00:24:20Z", + "mergedAt": null + }, + { + "number": 88, + "title": "Add pushfitplug model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/88", + "createdAt": "2026-10-10T00:24:22Z", + "mergedAt": "2026-10-10T01:23:57Z" + }, + { + "number": 89, + "title": "Add sandwichmount model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/89", + "createdAt": "2026-10-10T00:24:24Z", + "mergedAt": null + }, + { + "number": 90, + "title": "Add cableclamp model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/90", + "createdAt": "2026-10-10T01:45:24Z", + "mergedAt": null + }, + { + "number": 91, + "title": "Add cableclip model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/91", + "createdAt": "2026-10-10T01:45:27Z", + "mergedAt": null + }, + { + "number": 92, + "title": "Add cablecomb model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/92", + "createdAt": "2026-10-10T01:45:29Z", + "mergedAt": null + }, + { + "number": 93, + "title": "Add cabletiebase model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/93", + "createdAt": "2026-10-10T01:45:31Z", + "mergedAt": null + }, + { + "number": 94, + "title": "Add cornerfoot model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/94", + "createdAt": "2026-10-10T01:45:33Z", + "mergedAt": null + }, + { + "number": 95, + "title": "Add edgegrommet model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/95", + "createdAt": "2026-10-10T01:45:35Z", + "mergedAt": null + }, + { + "number": 96, + "title": "Add flatcableclip model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/96", + "createdAt": "2026-10-10T01:45:37Z", + "mergedAt": "2026-10-11T03:27:14Z" + }, + { + "number": 97, + "title": "Add flatgasket model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/97", + "createdAt": "2026-10-10T01:45:40Z", + "mergedAt": "2026-10-11T03:27:08Z" + }, + { + "number": 98, + "title": "Add hatsection model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/98", + "createdAt": "2026-10-10T01:45:41Z", + "mergedAt": "2026-10-11T03:27:02Z" + }, + { + "number": 99, + "title": "Add hexshaft model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/99", + "createdAt": "2026-10-10T01:45:43Z", + "mergedAt": null + }, + { + "number": 100, + "title": "Add hollowshaft model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/100", + "createdAt": "2026-10-10T01:45:46Z", + "mergedAt": null + }, + { + "number": 101, + "title": "Add keyedshaft model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/101", + "createdAt": "2026-10-10T01:45:48Z", + "mergedAt": null + }, + { + "number": 102, + "title": "Add keywasher model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/102", + "createdAt": "2026-10-10T01:45:49Z", + "mergedAt": null + }, + { + "number": 103, + "title": "Add pcbcornerclip model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/103", + "createdAt": "2026-10-10T01:45:51Z", + "mergedAt": null + }, + { + "number": 104, + "title": "Add pcbedgesupport model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/104", + "createdAt": "2026-10-10T01:45:53Z", + "mergedAt": null + }, + { + "number": 105, + "title": "Add pcbrail model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/105", + "createdAt": "2026-10-10T01:45:54Z", + "mergedAt": null + }, + { + "number": 106, + "title": "Add perforatedangle model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/106", + "createdAt": "2026-10-10T01:45:56Z", + "mergedAt": "2026-10-11T03:25:08Z" + }, + { + "number": 107, + "title": "Add pottingbox model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/107", + "createdAt": "2026-10-10T01:45:58Z", + "mergedAt": null + }, + { + "number": 108, + "title": "Add rectangulargasket model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/108", + "createdAt": "2026-10-10T01:46:01Z", + "mergedAt": "2026-10-10T04:55:54Z" + }, + { + "number": 109, + "title": "Add slottedchannel model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/109", + "createdAt": "2026-10-10T01:46:02Z", + "mergedAt": "2026-10-10T04:55:35Z" + }, + { + "number": 110, + "title": "Add splitgrommet model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/110", + "createdAt": "2026-10-10T01:46:04Z", + "mergedAt": "2026-10-10T04:55:30Z" + }, + { + "number": 111, + "title": "Add tslotcoverstrip model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/111", + "createdAt": "2026-10-10T01:46:06Z", + "mergedAt": "2026-10-10T04:55:09Z" + }, + { + "number": 112, + "title": "Add tslotendcap model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/112", + "createdAt": "2026-10-10T01:46:08Z", + "mergedAt": null + }, + { + "number": 113, + "title": "Add tslotpanelretainer model contract", + "state": "open", + "url": "https://github.com/tscircuit/modelprinter/pull/113", + "createdAt": "2026-10-10T01:46:10Z", + "mergedAt": null + }, + { + "number": 114, + "title": "Add zeebar model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/114", + "createdAt": "2026-10-10T01:46:12Z", + "mergedAt": "2026-10-10T04:54:45Z" + }, + { + "number": 115, + "title": "Add U.FL three-terminal model contract", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/115", + "createdAt": "2026-10-10T06:18:16Z", + "mergedAt": "2026-10-11T03:24:59Z" + }, + { + "number": 116, + "title": "Add NEMA34 motor support", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/116", + "createdAt": "2026-10-10T19:32:02Z", + "mergedAt": "2026-10-10T23:23:52Z" + }, + { + "number": 117, + "title": "Replace model geometry enums with boolean flags", + "state": "closed", + "url": "https://github.com/tscircuit/modelprinter/pull/117", + "createdAt": "2026-10-11T03:46:37Z", + "mergedAt": "2026-10-11T17:31:26Z" + } + ], + "mainCommitSha": "6a20e9677863faaecccc0f926e667f283cc74a22" + }, + { + "name": "tscircuit/jscad-electronics", + "totalHistoricalAuthoredPullRequests": 170, + "mainModelFamilies": [ + "anglebar", + "ballbearing", + "balltransferunit", + "buttonscrew", + "cablegrommet", + "channelbar", + "clampingshaftcollar", + "compressionspring", + "dowelpin", + "femalestandoff", + "flangebolt", + "flangedbushing", + "flangenut", + "flatcableclip", + "flatgasket", + "flatheadscrew", + "flexscreen", + "hatsection", + "heatsetinsert", + "helicalgear", + "hexbolt", + "hexnut", + "hexsocketbolt", + "hollowpositioningarmtube", + "malefemalestandoff", + "nema", + "nylonlocknut", + "panscrew", + "perforatedangle", + "perforatedsheet", + "plainbushing", + "pushfitplug", + "rectangularbar", + "rectangulargasket", + "rectangulartube", + "rigidcoupler", + "setscrew", + "shaftcollar", + "sheetmetal", + "slottedchannel", + "spacer", + "splitgrommet", + "splitwasher", + "spurgear", + "threadedrod", + "torsionspring", + "tslotcoverstrip", + "tslotextrusion", + "tslotgusset", + "tslotinsidecorner", + "ufl", + "wormgear", + "zeebar" + ], + "pullRequests": [ + { + "number": 7, + "title": "Add Footprint Pads for sizing, add 0603 resistor footprint + 3d model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/7", + "createdAt": "2024-07-23T00:58:52Z", + "mergedAt": "2024-07-23T06:18:23Z" + }, + { + "number": 12, + "title": "Add Smd Lead", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/12", + "createdAt": "2024-07-31T23:31:40Z", + "mergedAt": "2024-07-31T23:31:46Z" + }, + { + "number": 13, + "title": "Implement tssop", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/13", + "createdAt": "2024-08-01T00:22:01Z", + "mergedAt": "2024-08-18T19:36:59Z" + }, + { + "number": 18, + "title": "zaxisup", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/18", + "createdAt": "2024-08-26T18:56:42Z", + "mergedAt": null + }, + { + "number": 19, + "title": "BGA Balls Derived", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/19", + "createdAt": "2024-08-26T19:12:22Z", + "mergedAt": null + }, + { + "number": 34, + "title": " initial", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/34", + "createdAt": "2024-09-17T17:36:47Z", + "mergedAt": "2024-09-27T22:19:21Z" + }, + { + "number": 43, + "title": "Footprinter3d, fix path issues, minor formatting fixes", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/43", + "createdAt": "2024-09-27T22:18:11Z", + "mergedAt": "2024-09-27T22:19:20Z" + }, + { + "number": 44, + "title": "fix QFN to not be on side, fix footprints to render properly", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/44", + "createdAt": "2024-09-27T22:21:58Z", + "mergedAt": "2024-09-27T22:23:08Z" + }, + { + "number": 46, + "title": "DIP fix, coord flip", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/46", + "createdAt": "2024-10-02T00:10:58Z", + "mergedAt": "2024-10-02T00:21:01Z" + }, + { + "number": 61, + "title": "Create CODEOWNERS", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/61", + "createdAt": "2024-10-06T15:04:58Z", + "mergedAt": "2024-10-06T15:05:36Z" + }, + { + "number": 70, + "title": "Add anas as codeowner", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/70", + "createdAt": "2024-10-16T20:57:11Z", + "mergedAt": "2024-10-17T05:13:55Z" + }, + { + "number": 82, + "title": "Update jscad-fiber", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/82", + "createdAt": "2024-12-14T00:47:02Z", + "mergedAt": "2024-12-14T00:47:48Z" + }, + { + "number": 95, + "title": "Add DIP divot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/95", + "createdAt": "2025-06-04T04:42:48Z", + "mergedAt": null + }, + { + "number": 96, + "title": "Add DIP divot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/96", + "createdAt": "2025-06-04T04:46:58Z", + "mergedAt": null + }, + { + "number": 100, + "title": "move footprinter and circuit-json to peer deps", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/100", + "createdAt": "2025-07-14T04:36:41Z", + "mergedAt": "2025-07-14T04:38:44Z" + }, + { + "number": 101, + "title": "Remove bundling for all dependencies", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/101", + "createdAt": "2025-07-14T05:12:46Z", + "mergedAt": "2025-07-14T05:13:14Z" + }, + { + "number": 102, + "title": "Add multi-row header support", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/102", + "createdAt": "2025-07-28T00:10:47Z", + "mergedAt": null + }, + { + "number": 103, + "title": "rewrite plan generation", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/103", + "createdAt": "2025-08-10T02:44:22Z", + "mergedAt": null + }, + { + "number": 104, + "title": "Planner Library Export", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/104", + "createdAt": "2025-08-10T04:36:55Z", + "mergedAt": null + }, + { + "number": 120, + "title": "Allow jscad injection (allows using jscad-planner)", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/120", + "createdAt": "2025-09-29T04:38:17Z", + "mergedAt": "2025-09-29T04:49:39Z" + }, + { + "number": 131, + "title": "Add stalebot GitHub workflow", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/131", + "createdAt": "2025-10-13T17:58:11Z", + "mergedAt": "2025-10-13T19:21:39Z" + }, + { + "number": 304, + "title": "Add crystal 3D model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/304", + "createdAt": "2026-07-25T21:31:37Z", + "mergedAt": null + }, + { + "number": 305, + "title": "Add FPC connector 3D model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/305", + "createdAt": "2026-07-25T21:31:48Z", + "mergedAt": "2026-07-25T22:42:48Z" + }, + { + "number": 306, + "title": "Add LGA 3D model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/306", + "createdAt": "2026-07-25T21:32:00Z", + "mergedAt": null + }, + { + "number": 307, + "title": "Add SMD push button 3D model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/307", + "createdAt": "2026-07-25T21:32:13Z", + "mergedAt": null + }, + { + "number": 308, + "title": "Add USB-C mid-mount 3D model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/308", + "createdAt": "2026-07-25T21:32:28Z", + "mergedAt": null + }, + { + "number": 309, + "title": "Add WSON 3D model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/309", + "createdAt": "2026-07-25T21:32:41Z", + "mergedAt": null + }, + { + "number": 310, + "title": "add vertical SMD pin header model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/310", + "createdAt": "2026-07-25T22:02:35Z", + "mergedAt": "2026-07-25T22:52:42Z" + }, + { + "number": 313, + "title": "Add parametric RJ45 component support", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/313", + "createdAt": "2026-07-27T18:29:20Z", + "mergedAt": "2026-07-27T18:36:17Z" + }, + { + "number": 314, + "title": "Add all JST footprint models", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/314", + "createdAt": "2026-07-27T23:38:11Z", + "mergedAt": null + }, + { + "number": 334, + "title": "ci: migrate npm publishing to OIDC", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/334", + "createdAt": "2026-08-19T23:24:02Z", + "mergedAt": "2026-08-20T00:54:47Z" + }, + { + "number": 335, + "title": "Add configurable FlexScreen component", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/335", + "createdAt": "2026-08-20T00:14:01Z", + "mergedAt": "2026-08-20T02:14:36Z" + }, + { + "number": 338, + "title": "Support FlexScreen model strings", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/338", + "createdAt": "2026-08-20T22:33:13Z", + "mergedAt": "2026-08-21T02:59:33Z" + }, + { + "number": 339, + "title": "Use published @tscircuit/modelprinter package", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/339", + "createdAt": "2026-08-21T04:18:03Z", + "mergedAt": "2026-08-21T04:34:21Z" + }, + { + "number": 342, + "title": "Add SOT143B model with multiview visual snapshots", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/342", + "createdAt": "2026-09-13T23:14:33Z", + "mergedAt": "2026-09-14T02:34:16Z" + }, + { + "number": 343, + "title": "Add USB-C midmount model with six-view visual fixtures", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/343", + "createdAt": "2026-09-13T23:18:57Z", + "mergedAt": "2026-09-13T23:43:34Z" + }, + { + "number": 344, + "title": "Add SMD slide-switch model with composite visual fixtures", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/344", + "createdAt": "2026-09-13T23:24:18Z", + "mergedAt": "2026-09-14T02:33:49Z" + }, + { + "number": 345, + "title": "Add JST SH top-entry connector model with six-view snapshots", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/345", + "createdAt": "2026-09-13T23:31:13Z", + "mergedAt": "2026-09-14T02:34:01Z" + }, + { + "number": 346, + "title": "Fix USB-C midmount leg connection and remove model doc", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/346", + "createdAt": "2026-09-13T23:45:39Z", + "mergedAt": "2026-09-13T23:49:12Z" + }, + { + "number": 347, + "title": "Add molded tantalum capacitor model with A\u2013D cases", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/347", + "createdAt": "2026-09-14T02:39:31Z", + "mergedAt": "2026-09-14T04:47:43Z" + }, + { + "number": 348, + "title": "Add MELF resistor model with three physical package sizes", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/348", + "createdAt": "2026-09-14T02:41:51Z", + "mergedAt": "2026-09-14T14:20:39Z" + }, + { + "number": 349, + "title": "Add two-pad crystal model with Epson and NDK packages", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/349", + "createdAt": "2026-09-14T04:51:12Z", + "mergedAt": "2026-09-14T05:30:41Z" + }, + { + "number": 350, + "title": "Add RFANT ceramic chip antenna with composite renders", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/350", + "createdAt": "2026-09-14T04:52:54Z", + "mergedAt": "2026-09-14T14:50:23Z" + }, + { + "number": 351, + "title": "Add Murata BLM41 ferrite bead model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/351", + "createdAt": "2026-09-14T15:38:23Z", + "mergedAt": null + }, + { + "number": 352, + "title": "Add AEM MF2410 fuse model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/352", + "createdAt": "2026-09-14T15:38:25Z", + "mergedAt": null + }, + { + "number": 353, + "title": "Add Omron B3U-1000P tactile switch model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/353", + "createdAt": "2026-09-14T15:38:27Z", + "mergedAt": null + }, + { + "number": 354, + "title": "Add Sunlord SWPA4030 inductor model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/354", + "createdAt": "2026-09-14T15:38:29Z", + "mergedAt": null + }, + { + "number": 355, + "title": "Remove unsupported footprint suffix routes", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/355", + "createdAt": "2026-09-14T17:10:01Z", + "mergedAt": "2026-09-14T18:19:08Z" + }, + { + "number": 357, + "title": "Add standalone X1SON2 component", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/357", + "createdAt": "2026-09-14T18:40:10Z", + "mergedAt": null + }, + { + "number": 358, + "title": "Add standalone X3DFN2 component", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/358", + "createdAt": "2026-09-14T18:40:15Z", + "mergedAt": null + }, + { + "number": 359, + "title": "Add standalone MicroSMP component", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/359", + "createdAt": "2026-09-14T18:40:19Z", + "mergedAt": null + }, + { + "number": 360, + "title": "Add standalone PowerDI123 component", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/360", + "createdAt": "2026-09-14T18:40:23Z", + "mergedAt": null + }, + { + "number": 361, + "title": "Add standalone TUMD2SM component", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/361", + "createdAt": "2026-09-14T18:40:27Z", + "mergedAt": null + }, + { + "number": 362, + "title": "Add standalone MFSM075ResettableFuse component", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/362", + "createdAt": "2026-09-14T18:40:31Z", + "mergedAt": null + }, + { + "number": 364, + "title": "Extend DFN with parameterized low-profile package variants", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/364", + "createdAt": "2026-09-14T19:04:09Z", + "mergedAt": "2026-09-14T19:13:36Z" + }, + { + "number": 365, + "title": "Add parameterized DO-219AD standard package", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/365", + "createdAt": "2026-09-14T19:04:13Z", + "mergedAt": "2026-09-14T19:16:23Z" + }, + { + "number": 366, + "title": "Add parameterized SOD-323HE standard package", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/366", + "createdAt": "2026-09-14T19:04:17Z", + "mergedAt": "2026-09-14T19:14:38Z" + }, + { + "number": 367, + "title": "Add parameterized SOIC physical outlines", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/367", + "createdAt": "2026-09-14T19:32:02Z", + "mergedAt": "2026-09-14T19:36:59Z" + }, + { + "number": 368, + "title": "Add parameterized MO-187 MSOP physical outlines", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/368", + "createdAt": "2026-09-14T19:32:07Z", + "mergedAt": "2026-09-14T19:38:41Z" + }, + { + "number": 369, + "title": "Add parameterized TSSOP physical outlines", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/369", + "createdAt": "2026-09-14T19:32:11Z", + "mergedAt": "2026-09-14T19:39:16Z" + }, + { + "number": 370, + "title": "Add parameterized QFN physical outlines", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/370", + "createdAt": "2026-09-14T19:53:12Z", + "mergedAt": "2026-09-14T20:38:48Z" + }, + { + "number": 371, + "title": "Add parameterized LQFP physical outlines", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/371", + "createdAt": "2026-09-14T19:53:17Z", + "mergedAt": "2026-09-14T20:21:08Z" + }, + { + "number": 372, + "title": "Add parameterized SSOP standard package model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/372", + "createdAt": "2026-09-14T20:56:38Z", + "mergedAt": "2026-09-14T21:04:07Z" + }, + { + "number": 373, + "title": "Add parameterized LGA standard package model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/373", + "createdAt": "2026-09-14T20:56:41Z", + "mergedAt": "2026-09-14T21:12:50Z" + }, + { + "number": 374, + "title": "Add parameterized SOT89 standard package model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/374", + "createdAt": "2026-09-14T20:56:45Z", + "mergedAt": "2026-09-14T21:12:59Z" + }, + { + "number": 375, + "title": "Add parameterized TO277 standard package model", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/375", + "createdAt": "2026-09-14T20:56:49Z", + "mergedAt": "2026-09-14T21:13:09Z" + }, + { + "number": 376, + "title": "Use the standard SOT-89 model for Footprinter3d string routing", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/376", + "createdAt": "2026-09-14T21:24:53Z", + "mergedAt": null + }, + { + "number": 377, + "title": "Support LGA and SSOP body dimensions in Footprinter3d strings", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/377", + "createdAt": "2026-09-15T15:02:36Z", + "mergedAt": "2026-09-15T15:22:13Z" + }, + { + "number": 388, + "title": "Fix floating JST housing by honoring Rotate angles in vanilla renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/388", + "createdAt": "2026-09-18T19:10:27Z", + "mergedAt": "2026-09-18T20:02:31Z" + }, + { + "number": 389, + "title": "Load modelprinter 0.0.3 from jscdn", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/389", + "createdAt": "2026-09-23T20:27:13Z", + "mergedAt": null + }, + { + "number": 398, + "title": "Add NEMA motors and migrate mechanical models from modelprinter", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/398", + "createdAt": "2026-10-02T06:41:29Z", + "mergedAt": "2026-10-02T07:54:30Z" + }, + { + "number": 399, + "title": "Use the published modelprinter spec dependency", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/399", + "createdAt": "2026-10-02T07:59:41Z", + "mergedAt": "2026-10-02T17:52:26Z" + }, + { + "number": 400, + "title": "Render NEMA rear-face holes and socket-head screws", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/400", + "createdAt": "2026-10-02T19:09:56Z", + "mergedAt": "2026-10-02T21:27:40Z" + }, + { + "number": 401, + "title": "Render NEMA wire exits and optional six-pin JST-PH connector", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/401", + "createdAt": "2026-10-03T18:59:22Z", + "mergedAt": "2026-10-03T22:10:38Z" + }, + { + "number": 402, + "title": "Add parameterized spur gears and worm screws", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/402", + "createdAt": "2026-10-03T23:06:13Z", + "mergedAt": "2026-10-03T23:10:59Z" + }, + { + "number": 403, + "title": "Add helical gears compatible with spur and worm parameter conventions", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/403", + "createdAt": "2026-10-04T00:37:12Z", + "mergedAt": "2026-10-04T00:44:06Z" + }, + { + "number": 404, + "title": "Add 3D cable meshes for USB-C, JST and US mains", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/404", + "createdAt": "2026-10-04T21:05:55Z", + "mergedAt": "2026-10-04T21:38:49Z" + }, + { + "number": 405, + "title": "Add component material metadata and preserve it in exports", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/405", + "createdAt": "2026-10-05T18:09:30Z", + "mergedAt": "2026-10-06T19:59:55Z" + }, + { + "number": 406, + "title": "Add flanged bushing renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/406", + "createdAt": "2026-10-05T18:09:42Z", + "mergedAt": "2026-10-07T00:16:05Z" + }, + { + "number": 407, + "title": "Support FPC-style flexscreen contacts and explicit tail dimensions", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/407", + "createdAt": "2026-10-05T18:11:25Z", + "mergedAt": "2026-10-05T20:25:37Z" + }, + { + "number": 408, + "title": "Add plain bushing renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/408", + "createdAt": "2026-10-05T18:11:54Z", + "mergedAt": "2026-10-07T00:14:57Z" + }, + { + "number": 409, + "title": "Add threaded rod renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/409", + "createdAt": "2026-10-05T18:12:38Z", + "mergedAt": "2026-10-07T00:30:45Z" + }, + { + "number": 410, + "title": "Add cable grommet renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/410", + "createdAt": "2026-10-05T18:16:42Z", + "mergedAt": "2026-10-07T00:19:59Z" + }, + { + "number": 411, + "title": "Add countersunk socket screw renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/411", + "createdAt": "2026-10-05T18:17:01Z", + "mergedAt": "2026-10-07T00:11:51Z" + }, + { + "number": 412, + "title": "Add T-slot inside corner renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/412", + "createdAt": "2026-10-05T18:17:23Z", + "mergedAt": "2026-10-07T00:24:14Z" + }, + { + "number": 413, + "title": "Add T-slot triangular gusset renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/413", + "createdAt": "2026-10-05T18:17:42Z", + "mergedAt": "2026-10-07T00:25:21Z" + }, + { + "number": 414, + "title": "Add ISO hex bolt renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/414", + "createdAt": "2026-10-05T18:19:20Z", + "mergedAt": "2026-10-05T22:48:54Z" + }, + { + "number": 415, + "title": "Add ISO button socket screw renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/415", + "createdAt": "2026-10-05T18:19:39Z", + "mergedAt": "2026-10-07T00:17:24Z" + }, + { + "number": 416, + "title": "Add ISO hex nut renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/416", + "createdAt": "2026-10-05T18:19:59Z", + "mergedAt": "2026-10-07T00:18:42Z" + }, + { + "number": 417, + "title": "Add T-slot extrusion renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/417", + "createdAt": "2026-10-05T18:20:21Z", + "mergedAt": "2026-10-07T00:22:45Z" + }, + { + "number": 418, + "title": "Add compression spring renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/418", + "createdAt": "2026-10-05T18:20:41Z", + "mergedAt": "2026-10-07T00:21:17Z" + }, + { + "number": 419, + "title": "Add ISO Phillips pan screw renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/419", + "createdAt": "2026-10-05T18:28:28Z", + "mergedAt": "2026-10-07T00:13:29Z" + }, + { + "number": 420, + "title": "Add set-screw shaft collar renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/420", + "createdAt": "2026-10-05T18:36:09Z", + "mergedAt": "2026-10-07T00:26:40Z" + }, + { + "number": 421, + "title": "Add clamping shaft collar renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/421", + "createdAt": "2026-10-05T18:36:37Z", + "mergedAt": "2026-10-07T00:27:58Z" + }, + { + "number": 422, + "title": "Add rigid shaft coupler renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/422", + "createdAt": "2026-10-05T18:36:59Z", + "mergedAt": "2026-10-07T00:29:16Z" + }, + { + "number": 423, + "title": "Add single and grouped bullet connector cable meshes", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/423", + "createdAt": "2026-10-06T17:29:29Z", + "mergedAt": "2026-10-06T21:27:53Z" + }, + { + "number": 424, + "title": "Orient cable plugs and conductors using explicit pin 1 sides", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/424", + "createdAt": "2026-10-06T20:24:17Z", + "mergedAt": "2026-10-06T22:02:51Z" + }, + { + "number": 425, + "title": "Discover typed model renderer registrations without changing package APIs", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/425", + "createdAt": "2026-10-06T20:29:11Z", + "mergedAt": "2026-10-06T20:41:47Z" + }, + { + "number": 426, + "title": "Generate JST PH and SH motor headers for selected pin counts", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/426", + "createdAt": "2026-10-06T20:44:07Z", + "mergedAt": "2026-10-06T22:01:35Z" + }, + { + "number": 427, + "title": "Fan adapter cable wires between independent connector pitches", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/427", + "createdAt": "2026-10-06T23:26:28Z", + "mergedAt": "2026-10-07T02:50:10Z" + }, + { + "number": 428, + "title": "Update modelprinter to released model contracts", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/428", + "createdAt": "2026-10-07T00:01:37Z", + "mergedAt": "2026-10-07T00:07:44Z" + }, + { + "number": 429, + "title": "[Preview only] Shared motion contract dependency for renderer reviews", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/429", + "createdAt": "2026-10-07T04:34:09Z", + "mergedAt": null + }, + { + "number": 430, + "title": "Add belt idler renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/430", + "createdAt": "2026-10-07T04:34:40Z", + "mergedAt": null + }, + { + "number": 431, + "title": "Add lead screw renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/431", + "createdAt": "2026-10-07T04:34:47Z", + "mergedAt": null + }, + { + "number": 432, + "title": "Add lead screw nut renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/432", + "createdAt": "2026-10-07T04:34:55Z", + "mergedAt": null + }, + { + "number": 433, + "title": "Add linear carriage renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/433", + "createdAt": "2026-10-07T04:35:02Z", + "mergedAt": null + }, + { + "number": 434, + "title": "Add linear rail renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/434", + "createdAt": "2026-10-07T04:35:09Z", + "mergedAt": null + }, + { + "number": 435, + "title": "Add NEMA motor mounting bracket renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/435", + "createdAt": "2026-10-07T04:35:16Z", + "mergedAt": null + }, + { + "number": 436, + "title": "Add thrust ball bearing renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/436", + "createdAt": "2026-10-07T04:35:23Z", + "mergedAt": null + }, + { + "number": 437, + "title": "Add timing belt renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/437", + "createdAt": "2026-10-07T04:35:30Z", + "mergedAt": null + }, + { + "number": 438, + "title": "Add timing pulley renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/438", + "createdAt": "2026-10-07T04:35:36Z", + "mergedAt": null + }, + { + "number": 439, + "title": "Render radial bearings with compact designations and face flags", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/439", + "createdAt": "2026-10-07T04:42:17Z", + "mergedAt": "2026-10-07T06:47:10Z" + }, + { + "number": 440, + "title": "Add linear ball bearing renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/440", + "createdAt": "2026-10-07T04:42:24Z", + "mergedAt": null + }, + { + "number": 441, + "title": "Add linear bearing block renderer", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/441", + "createdAt": "2026-10-07T04:42:32Z", + "mergedAt": null + }, + { + "number": 445, + "title": "Use modelprinter 0.0.18 for mechanical model flags", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/445", + "createdAt": "2026-10-08T05:58:39Z", + "mergedAt": "2026-10-08T17:06:14Z" + }, + { + "number": 447, + "title": "Use the ThreadedRod flag and boolean API", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/447", + "createdAt": "2026-10-09T05:45:10Z", + "mergedAt": "2026-10-09T06:16:11Z" + }, + { + "number": 448, + "title": "Add BallTransferUnit renderer and disable Bun lockfile saving", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/448", + "createdAt": "2026-10-09T06:14:55Z", + "mergedAt": "2026-10-09T15:50:47Z" + }, + { + "number": 449, + "title": "Support metric and imperial UNC hex nuts", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/449", + "createdAt": "2026-10-09T17:17:36Z", + "mergedAt": "2026-10-09T17:29:19Z" + }, + { + "number": 450, + "title": "Add round spacer renderer and visual snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/450", + "createdAt": "2026-10-09T18:16:02Z", + "mergedAt": "2026-10-09T21:37:54Z" + }, + { + "number": 451, + "title": "Add female hex standoff renderer and visual snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/451", + "createdAt": "2026-10-09T18:16:07Z", + "mergedAt": "2026-10-09T21:38:01Z" + }, + { + "number": 452, + "title": "Add male-female hex standoff renderer and visual snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/452", + "createdAt": "2026-10-09T18:16:10Z", + "mergedAt": "2026-10-09T21:38:06Z" + }, + { + "number": 453, + "title": "Add nylon insert lock nut renderer and colored snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/453", + "createdAt": "2026-10-09T18:16:15Z", + "mergedAt": "2026-10-09T21:38:12Z" + }, + { + "number": 454, + "title": "Add cup-point set screw renderer and visual snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/454", + "createdAt": "2026-10-09T18:16:19Z", + "mergedAt": "2026-10-09T21:38:31Z" + }, + { + "number": 455, + "title": "Add split washer renderer and handed visual snapshots", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/455", + "createdAt": "2026-10-09T18:16:24Z", + "mergedAt": "2026-10-09T19:03:30Z" + }, + { + "number": 456, + "title": "Add nominal dowel pin renderer and visual snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/456", + "createdAt": "2026-10-09T18:17:43Z", + "mergedAt": "2026-10-09T21:01:13Z" + }, + { + "number": 457, + "title": "Add plain-face flange nut renderer and visual snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/457", + "createdAt": "2026-10-09T18:21:05Z", + "mergedAt": "2026-10-09T21:38:17Z" + }, + { + "number": 458, + "title": "Add diamond-knurled heat-set insert renderer and snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/458", + "createdAt": "2026-10-09T18:22:13Z", + "mergedAt": "2026-10-09T21:01:26Z" + }, + { + "number": 459, + "title": "Add flange bolt renderer with ISO 4162 head geometry", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/459", + "createdAt": "2026-10-09T18:26:41Z", + "mergedAt": "2026-10-09T21:38:25Z" + }, + { + "number": 460, + "title": "Bundle model contracts without per-model dependency bumps", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/460", + "createdAt": "2026-10-09T20:06:29Z", + "mergedAt": "2026-10-09T20:40:17Z" + }, + { + "number": 461, + "title": "Use default ISO dowel-pin strings in renderer examples and tests", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/461", + "createdAt": "2026-10-09T21:17:23Z", + "mergedAt": "2026-10-09T21:37:49Z" + }, + { + "number": 462, + "title": "Use default ISO flag contracts for existing fastener renderers", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/462", + "createdAt": "2026-10-09T21:19:37Z", + "mergedAt": "2026-10-09T21:37:41Z" + }, + { + "number": 463, + "title": "Add rectangularbar renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/463", + "createdAt": "2026-10-10T00:24:29Z", + "mergedAt": "2026-10-10T01:23:21Z" + }, + { + "number": 464, + "title": "Add rectangulartube renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/464", + "createdAt": "2026-10-10T00:24:31Z", + "mergedAt": "2026-10-10T01:23:13Z" + }, + { + "number": 465, + "title": "Add anglebar renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/465", + "createdAt": "2026-10-10T00:24:33Z", + "mergedAt": "2026-10-10T01:24:26Z" + }, + { + "number": 466, + "title": "Add channelbar renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/466", + "createdAt": "2026-10-10T00:24:35Z", + "mergedAt": "2026-10-10T01:24:23Z" + }, + { + "number": 467, + "title": "Add perforatedsheet renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/467", + "createdAt": "2026-10-10T00:24:38Z", + "mergedAt": "2026-10-10T01:24:13Z" + }, + { + "number": 468, + "title": "Add fixtureplate renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/468", + "createdAt": "2026-10-10T00:24:40Z", + "mergedAt": null + }, + { + "number": 469, + "title": "Add vblock renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/469", + "createdAt": "2026-10-10T00:24:42Z", + "mergedAt": null + }, + { + "number": 470, + "title": "Add stepblock renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/470", + "createdAt": "2026-10-10T00:24:44Z", + "mergedAt": null + }, + { + "number": 471, + "title": "Add pushfitplug renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/471", + "createdAt": "2026-10-10T00:24:46Z", + "mergedAt": "2026-10-10T01:23:59Z" + }, + { + "number": 472, + "title": "Add sandwichmount renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/472", + "createdAt": "2026-10-10T00:24:49Z", + "mergedAt": null + }, + { + "number": 473, + "title": "Add cableclamp renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/473", + "createdAt": "2026-10-10T01:55:39Z", + "mergedAt": null + }, + { + "number": 474, + "title": "Add cableclip renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/474", + "createdAt": "2026-10-10T01:55:41Z", + "mergedAt": null + }, + { + "number": 475, + "title": "Add cablecomb renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/475", + "createdAt": "2026-10-10T01:55:44Z", + "mergedAt": null + }, + { + "number": 476, + "title": "Add cabletiebase renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/476", + "createdAt": "2026-10-10T01:55:46Z", + "mergedAt": null + }, + { + "number": 477, + "title": "Add cornerfoot renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/477", + "createdAt": "2026-10-10T01:55:48Z", + "mergedAt": null + }, + { + "number": 478, + "title": "Add edgegrommet renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/478", + "createdAt": "2026-10-10T01:55:51Z", + "mergedAt": null + }, + { + "number": 479, + "title": "Add flatcableclip renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/479", + "createdAt": "2026-10-10T01:55:53Z", + "mergedAt": "2026-10-11T03:27:17Z" + }, + { + "number": 480, + "title": "Add flatgasket renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/480", + "createdAt": "2026-10-10T01:55:55Z", + "mergedAt": "2026-10-11T03:27:11Z" + }, + { + "number": 481, + "title": "Add hatsection renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/481", + "createdAt": "2026-10-10T01:55:58Z", + "mergedAt": "2026-10-11T03:27:04Z" + }, + { + "number": 482, + "title": "Add hexshaft renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/482", + "createdAt": "2026-10-10T01:56:02Z", + "mergedAt": null + }, + { + "number": 483, + "title": "Add hollowshaft renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/483", + "createdAt": "2026-10-10T01:56:06Z", + "mergedAt": null + }, + { + "number": 484, + "title": "Add keyedshaft renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/484", + "createdAt": "2026-10-10T01:56:08Z", + "mergedAt": null + }, + { + "number": 485, + "title": "Add keywasher renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/485", + "createdAt": "2026-10-10T01:56:11Z", + "mergedAt": null + }, + { + "number": 486, + "title": "Add pcbcornerclip renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/486", + "createdAt": "2026-10-10T01:56:14Z", + "mergedAt": null + }, + { + "number": 487, + "title": "Add pcbedgesupport renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/487", + "createdAt": "2026-10-10T01:56:16Z", + "mergedAt": null + }, + { + "number": 488, + "title": "Add pcbrail renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/488", + "createdAt": "2026-10-10T01:56:18Z", + "mergedAt": null + }, + { + "number": 489, + "title": "Add perforatedangle renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/489", + "createdAt": "2026-10-10T01:56:21Z", + "mergedAt": "2026-10-11T03:25:10Z" + }, + { + "number": 490, + "title": "Add pottingbox renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/490", + "createdAt": "2026-10-10T01:56:23Z", + "mergedAt": null + }, + { + "number": 491, + "title": "Add rectangulargasket renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/491", + "createdAt": "2026-10-10T01:58:06Z", + "mergedAt": "2026-10-10T04:55:56Z" + }, + { + "number": 492, + "title": "Add slottedchannel renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/492", + "createdAt": "2026-10-10T01:58:08Z", + "mergedAt": "2026-10-10T04:55:37Z" + }, + { + "number": 493, + "title": "Add splitgrommet renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/493", + "createdAt": "2026-10-10T01:58:11Z", + "mergedAt": "2026-10-10T04:55:32Z" + }, + { + "number": 494, + "title": "Add tslotcoverstrip renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/494", + "createdAt": "2026-10-10T01:58:13Z", + "mergedAt": "2026-10-10T04:55:11Z" + }, + { + "number": 495, + "title": "Add tslotendcap renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/495", + "createdAt": "2026-10-10T01:58:16Z", + "mergedAt": null + }, + { + "number": 496, + "title": "Add tslotpanelretainer renderer and four-view snapshot", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/496", + "createdAt": "2026-10-10T01:58:18Z", + "mergedAt": null + }, + { + "number": 497, + "title": "Add zeebar renderer and four-view snapshot", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/497", + "createdAt": "2026-10-10T01:58:20Z", + "mergedAt": "2026-10-10T04:54:48Z" + }, + { + "number": 498, + "title": "Honor independent row pitch in header CAD models", + "state": "open", + "url": "https://github.com/tscircuit/jscad-electronics/pull/498", + "createdAt": "2026-10-10T05:28:18Z", + "mergedAt": null + }, + { + "number": 499, + "title": "Render U.FL antenna receptacles with aligned solder terminals", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/499", + "createdAt": "2026-10-10T06:41:10Z", + "mergedAt": "2026-10-11T03:25:02Z" + }, + { + "number": 500, + "title": "Add NEMA34 React and vanilla motor support", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/500", + "createdAt": "2026-10-10T19:32:56Z", + "mergedAt": "2026-10-11T01:13:32Z" + }, + { + "number": 501, + "title": "Consume boolean model flags throughout renderers", + "state": "closed", + "url": "https://github.com/tscircuit/jscad-electronics/pull/501", + "createdAt": "2026-10-11T04:00:05Z", + "mergedAt": "2026-10-11T17:31:41Z" + } + ], + "mainCommitSha": "470267493ef11a350601d965265bc899020a9800" + } + ], + "excludedModelFamilies": [ + "adhesivemountelectricalcomponentheatsink", + "anglebar", + "ballbearing", + "balltransferunit", + "beltidler", + "buttonscrew", + "cableclamp", + "cableclip", + "cablecomb", + "cablegrommet", + "cabletie", + "cabletiebase", + "channelbar", + "clampingshaftcollar", + "compressionspring", + "cornerfoot", + "dowelpin", + "edgegrommet", + "femalestandoff", + "fixtureplate", + "flangebolt", + "flangedbushing", + "flangenut", + "flatcableclip", + "flatgasket", + "flatheadscrew", + "flexiblehose", + "flexscreen", + "hatsection", + "heatsetinsert", + "helicalgear", + "hexbolt", + "hexnut", + "hexshaft", + "hexsocketbolt", + "hollowpositioningarmtube", + "hollowshaft", + "keyedshaft", + "keywasher", + "leadscrew", + "leadscrewnut", + "linearballbearing", + "linearbearingblock", + "linearcarriage", + "linearrail", + "malefemalestandoff", + "nema", + "nemamotormount", + "nylonlocknut", + "panscrew", + "pcbcardguide", + "pcbcornerclip", + "pcbedgesupport", + "pcbrail", + "perforatedangle", + "perforatedsheet", + "pinheader", + "plainbushing", + "pottingbox", + "pushfitplug", + "rectangularbar", + "rectangulargasket", + "rectangulartube", + "rigidcoupler", + "sandwichmount", + "setscrew", + "shaftcollar", + "sheetmetal", + "slottedchannel", + "spacer", + "splitgrommet", + "splitwasher", + "spurgear", + "stepblock", + "threadedrod", + "thrustballbearing", + "timingbelt", + "timingpulley", + "torsionspring", + "tslotcoverstrip", + "tslotendcap", + "tslotextrusion", + "tslotgusset", + "tslotinsidecorner", + "tslotpanelretainer", + "ufl", + "vblock", + "wormgear", + "zeebar" + ], + "aliasExclusions": { + "nemamount": "nemamotormount", + "thrustbearing": "thrustballbearing", + "radialbearing": "ballbearing", + "setscrewshaftcollar": "shaftcollar", + "tslotcorner": "tslotinsidecorner", + "pcbguide": "pcbcardguide", + "worm": "wormgear" + }, + "semanticReview": [ + { + "candidate": "flatwasher", + "finding": "No flatwasher/Washer component, model alias, or Footprinter3d case exists; source search 'washer' returns only splitwasher models and mention of washer faces in other fasteners.", + "relatedExisting": [ + "splitwasher", + "keywasher", + "flatgasket", + "thrustballbearing" + ], + "distinction": "Flat washer is the fundamental unbroken, planar load-spreading family. Split/key washers have different retention features. Flat gasket shares annular geometry but has a separate sealing purpose." + }, + { + "candidate": "retainingring", + "alternateNamesReviewed": ["circlip", "snapring", "retaining ring"], + "finding": "No family, alias, historical PR topic, or source-index match.", + "distinction": "Open C-section ring with plier lugs, not a washer." + }, + { + "candidate": "rigidpipe", + "relatedExisting": [ + "flexiblehose", + "hollowshaft", + "hollowpositioningarmtube" + ], + "finding": "No rigid pipe/fitting PRs; the excluded straight flexible hose has the same simple annular primitive.", + "distinction": "Generic rigid fluid transport family; geometry reuse is explicit, no new size counted as a model." + }, + { + "candidate": "roundbar", + "relatedExisting": [ + "hollowshaft", + "keyedshaft", + "hexshaft", + "threadedrod" + ], + "distinction": "Solid plain round raw stock, without hollow bore, keyway, hex section or threads." + }, + { + "candidate": "cylindermount", + "relatedExisting": ["nemamotormount", "clevis"], + "distinction": "Mounting bracket specific to cylinder end envelope and machine base, not motor bolt-pattern bracket. Choose one mounting construction, not multiple count variants." + }, + { + "candidate": "pushbuttonoperator", + "relatedExisting": ["PushButton", "SmdPushButton"], + "distinction": "Industrial panel-mounted operator includes barrel and locknut/bezel; existing root components are PCB-contact push buttons." + }, + { + "candidate": "wireduct", + "relatedExisting": ["channelbar", "slottedchannel"], + "distinction": "Wiring duct has repeating finger slots in both sidewalls and removable snap-on lid, not an open structural channel." + }, + { + "candidate": "flangebearing", + "relatedExisting": ["flangedbushing", "ballbearing"], + "distinction": "Housing plus rolling bearing and external mounting bores; not flanged plain sleeve." + }, + { + "candidate": "shaftsupport", + "relatedExisting": ["linearbearingblock", "clampingshaftcollar"], + "distinction": "Static support clamp and mounting pedestal, not a moving linear bearing or shaft locating collar." + }, + { + "candidate": "jawcoupling", + "relatedExisting": ["rigidcoupler", "beamcoupling", "oldhamcoupling"], + "distinction": "Elastomer spider and meshing jaws; each chosen coupling has a different fundamental compliance construction." + }, + { + "candidate": "bevelgear", + "relatedExisting": ["spurgear", "helicalgear", "wormgear"], + "distinction": "Conical pitch envelope for intersecting axes; nominal teeth visualization only." + } + ], + "proposals": [ + { + "groupNumber": 1, + "group": "fasteners-and-retainers", + "models": [ + { + "name": "flatwasher", + "identifyingFeatures": "Flat load-spreading annulus" + }, + { + "name": "toothedwasher", + "identifyingFeatures": "Annulus with radial locking teeth" + }, + { + "name": "wavewasher", + "identifyingFeatures": "Axially undulating annulus" + }, + { + "name": "bellevillewasher", + "identifyingFeatures": "Conical annulus" + }, + { + "name": "shoulderbolt", + "identifyingFeatures": "Large shoulder, small shank and socket head" + }, + { + "name": "ubolt", + "identifyingFeatures": "Bent U rod with two parallel legs" + }, + { + "name": "eyebolt", + "identifyingFeatures": "Axial shank with ring eye" + }, + { + "name": "carriagebolt", + "identifyingFeatures": "Domed head and square neck" + }, + { + "name": "wingnut", + "identifyingFeatures": "Central bore and two wings" + }, + { + "name": "capnut", + "identifyingFeatures": "Hex base with closed dome" + }, + { + "name": "couplingnut", + "identifyingFeatures": "Long hex sleeve with through bore" + }, + { + "name": "tslotnut", + "identifyingFeatures": "T-section threaded slug" + }, + { + "name": "rivetnut", + "identifyingFeatures": "Flanged cylindrical insert and blind bore" + }, + { + "name": "solidrivet", + "identifyingFeatures": "Domed head and solid shank" + }, + { + "name": "blindrivet", + "identifyingFeatures": "Flanged sleeve and projecting mandrel" + }, + { + "name": "clevispin", + "identifyingFeatures": "Headed shaft with transverse retention bore" + }, + { + "name": "springpin", + "identifyingFeatures": "Slit cylindrical tube" + }, + { + "name": "taperpin", + "identifyingFeatures": "Tapered solid rod" + }, + { + "name": "retainingring", + "identifyingFeatures": "Split C-ring with lug holes" + }, + { + "name": "hairpincotter", + "identifyingFeatures": "Bent hairpin wire with curved retention leg" + } + ] + }, + { + "groupNumber": 2, + "group": "motion-transmission-and-bearings", + "models": [ + { + "name": "pillowblockbearing", + "identifyingFeatures": "Bearing in foot-mounted housing" + }, + { + "name": "flangebearing", + "identifyingFeatures": "Bearing in pierced flange housing" + }, + { + "name": "needlebearing", + "identifyingFeatures": "Races and slender roller cage" + }, + { + "name": "cylindricalrollerbearing", + "identifyingFeatures": "Races and large cylindrical rollers" + }, + { + "name": "camfollower", + "identifyingFeatures": "Bearing wheel with projecting stud" + }, + { + "name": "vgroovewheel", + "identifyingFeatures": "Wheel with V running groove" + }, + { + "name": "rodend", + "identifyingFeatures": "Eye with spherical bearing and stem" + }, + { + "name": "sphericalbearing", + "identifyingFeatures": "Bored spherical inner member in outer race" + }, + { + "name": "clevis", + "identifyingFeatures": "Fork with two cross-pierced ears" + }, + { + "name": "universaljoint", + "identifyingFeatures": "Two yokes connected by cross" + }, + { + "name": "jawcoupling", + "identifyingFeatures": "Opposed jaw hubs and center spider" + }, + { + "name": "beamcoupling", + "identifyingFeatures": "Sleeve with helical flexure slots" + }, + { + "name": "oldhamcoupling", + "identifyingFeatures": "Two hubs and orthogonally grooved center disk" + }, + { + "name": "gearrack", + "identifyingFeatures": "Linear tooth row on base" + }, + { + "name": "sprocket", + "identifyingFeatures": "Chain-toothed wheel and bore" + }, + { + "name": "rollerchain", + "identifyingFeatures": "Repeated paired plate, pin and roller links" + }, + { + "name": "vbeltsheave", + "identifyingFeatures": "Pulley with V belt groove" + }, + { + "name": "conveyorroller", + "identifyingFeatures": "Drum with end bearings and axle" + }, + { + "name": "shaftsupport", + "identifyingFeatures": "Split bore pedestal on pierced feet" + }, + { + "name": "bevelgear", + "identifyingFeatures": "Conical tooth wheel and hub" + } + ] + }, + { + "groupNumber": 3, + "group": "pipes-fittings-and-valves", + "models": [ + { + "name": "rigidpipe", + "identifyingFeatures": "Annular rigid pipe with cut ends" + }, + { + "name": "pipeelbow", + "identifyingFeatures": "Right-angle curved bore" + }, + { + "name": "pipetee", + "identifyingFeatures": "Three intersecting ports" + }, + { + "name": "pipecross", + "identifyingFeatures": "Four intersecting ports" + }, + { + "name": "pipecoupling", + "identifyingFeatures": "Thick annular coupling sleeve" + }, + { + "name": "pipeunion", + "identifyingFeatures": "Two pipe ends with central hex union" + }, + { + "name": "pipenipple", + "identifyingFeatures": "Short pipe with raised threaded end envelopes" + }, + { + "name": "pipecap", + "identifyingFeatures": "Closed end with blind bore" + }, + { + "name": "pipeplug", + "identifyingFeatures": "Solid hex-headed plug" + }, + { + "name": "reducingbushing", + "identifyingFeatures": "Hex head and stepped bore" + }, + { + "name": "hosebarb", + "identifyingFeatures": "Raised conical barb rings and bore" + }, + { + "name": "bulkheadfitting", + "identifyingFeatures": "Tube with flange and locknut" + }, + { + "name": "pushconnectfitting", + "identifyingFeatures": "Fitting body with collet and bore" + }, + { + "name": "compressionfitting", + "identifyingFeatures": "Fitting body with compression nuts" + }, + { + "name": "hoseclamp", + "identifyingFeatures": "Clamping band and screw housing" + }, + { + "name": "pipeflange", + "identifyingFeatures": "Annulus with bolt circle" + }, + { + "name": "ballvalve", + "identifyingFeatures": "Inline valve body, ports and lever" + }, + { + "name": "checkvalve", + "identifyingFeatures": "Inline valve with identifiable direction feature" + }, + { + "name": "needlevalve", + "identifyingFeatures": "Valve body with stem and handwheel" + }, + { + "name": "manifold", + "identifyingFeatures": "Rectangular bar with intersecting port array" + } + ] + }, + { + "groupNumber": 4, + "group": "industrial-automation", + "models": [ + { + "name": "pneumaticcylinder", + "identifyingFeatures": "Barrel, end caps and extending rod" + }, + { + "name": "cylindermount", + "identifyingFeatures": "Cylinder bracket with mounting or pivot bores" + }, + { + "name": "airfilter", + "identifyingFeatures": "Ported head and filter bowl" + }, + { + "name": "airregulator", + "identifyingFeatures": "Ported body and adjustment knob" + }, + { + "name": "airlubricator", + "identifyingFeatures": "Ported head, oil bowl and sight dome" + }, + { + "name": "solenoidvalve", + "identifyingFeatures": "Multiport block with coil" + }, + { + "name": "pressureswitch", + "identifyingFeatures": "Pressure connection, body and electrical head" + }, + { + "name": "pressuregauge", + "identifyingFeatures": "Round case, readable dial features and stem" + }, + { + "name": "flowmeter", + "identifyingFeatures": "Transparent measuring tube with float and end fittings" + }, + { + "name": "vacuumcup", + "identifyingFeatures": "Bell-shaped suction cup and neck" + }, + { + "name": "inductiveproximitysensor", + "identifyingFeatures": "Cylindrical sensing face, locknuts and cable" + }, + { + "name": "photoelectricsensor", + "identifyingFeatures": "Rectangular housing, lenses and mount bores" + }, + { + "name": "limitswitch", + "identifyingFeatures": "Housing with lever and roller" + }, + { + "name": "pushbuttonoperator", + "identifyingFeatures": "Panel barrel, bezel and plunger" + }, + { + "name": "emergencystopswitch", + "identifyingFeatures": "Mushroom button and panel barrel" + }, + { + "name": "selectoroperator", + "identifyingFeatures": "Panel barrel and rotary lever" + }, + { + "name": "pilotlight", + "identifyingFeatures": "Bezel, lens and panel barrel" + }, + { + "name": "dinrail", + "identifyingFeatures": "Top-hat section with slotted web" + }, + { + "name": "terminalblock", + "identifyingFeatures": "DIN foot, insulated body and screw wells" + }, + { + "name": "wireduct", + "identifyingFeatures": "Slotted channel sidewalls and removable cover" + } + ] + }, + { + "groupNumber": 5, + "group": "machine-hardware-and-structural", + "models": [ + { + "name": "hinge", + "identifyingFeatures": "Leaves, alternating knuckles and pin" + }, + { + "name": "barrelhinge", + "identifyingFeatures": "Two barrels and connecting pin" + }, + { + "name": "drawlatch", + "identifyingFeatures": "Keeper, hook and lever base" + }, + { + "name": "toggleclamp", + "identifyingFeatures": "Mount base, pivoting links and clamp foot" + }, + { + "name": "levelingfoot", + "identifyingFeatures": "Base pad and adjusting stud" + }, + { + "name": "caster", + "identifyingFeatures": "Fork, wheel, top plate and swivel" + }, + { + "name": "rubberbumper", + "identifyingFeatures": "Resilient dome, flat bottom and mounting bore" + }, + { + "name": "bridgehandle", + "identifyingFeatures": "Two mounting feet and raised grip" + }, + { + "name": "handwheel", + "identifyingFeatures": "Rim, spokes and hub" + }, + { + "name": "starknob", + "identifyingFeatures": "Lobed grip and mounting bore" + }, + { + "name": "leverhandle", + "identifyingFeatures": "Mounting hub and offset lever" + }, + { + "name": "squarekey", + "identifyingFeatures": "Rectangular shaft key with end chamfers" + }, + { + "name": "woodruffkey", + "identifyingFeatures": "Half-disk shaft key" + }, + { + "name": "ibeam", + "identifyingFeatures": "I-shaped structural section" + }, + { + "name": "teebar", + "identifyingFeatures": "T-shaped structural section" + }, + { + "name": "roundbar", + "identifyingFeatures": "Solid round stock with cut ends" + }, + { + "name": "splitringhanger", + "identifyingFeatures": "Split pipe ring with top mounting boss" + }, + { + "name": "detentpin", + "identifyingFeatures": "Grip, smooth shaft and tip detent ball" + }, + { + "name": "expandedmetal", + "identifyingFeatures": "Diamond mesh of connected strands" + }, + { + "name": "coolingfan", + "identifyingFeatures": "Square frame, circular shroud, hub and blades" + } + ] + } + ], + "catalogSources": [ + { + "group": "fasteners-and-retainers", + "url": "https://www.mcmaster.com/products/flat-washers/", + "families": ["flatwasher"] + }, + { + "group": "fasteners-and-retainers", + "url": "https://www.mcmaster.com/products/vibration-control-washers/", + "families": ["toothedwasher", "wavewasher", "bellevillewasher"] + }, + { + "group": "fasteners-and-retainers", + "url": "https://www.mcmaster.com/products/steel-retaining-washers/", + "families": ["retainingring"] + }, + { + "group": "motion-transmission-and-bearings", + "url": "https://www.mcmaster.com/products/pillow-blocks/", + "families": [ + "pillowblockbearing", + "needlebearing", + "cylindricalrollerbearing" + ] + }, + { + "group": "motion-transmission-and-bearings", + "url": "https://www.mcmaster.com/products/oldham-couplings/", + "families": ["oldhamcoupling"] + }, + { + "group": "motion-transmission-and-bearings", + "url": "https://www.mcmaster.com/products/couplings/shaft-diameter~9-mm/", + "families": ["jawcoupling", "beamcoupling"] + }, + { + "group": "pipes-fittings-and-valves", + "url": "https://www.mcmaster.com/products/pipe-fittings/", + "families": ["manifold", "ballvalve", "pipeflange", "solenoidvalve"], + "note": "Catalog family validation; no dimensional/compatibility claim." + }, + { + "group": "industrial-automation", + "url": "https://www.mcmaster.com/products/pneumatics/", + "families": ["pneumaticcylinder", "cylindermount"] + }, + { + "group": "industrial-automation", + "url": "https://www.mcmaster.com/products/air-preparation-units/", + "families": ["airfilter", "airregulator", "airlubricator"] + }, + { + "group": "industrial-automation", + "url": "https://www.mcmaster.com/products/emergency-stop-switches/", + "families": ["emergencystopswitch"] + }, + { + "group": "machine-hardware-and-structural", + "url": "https://www.mcmaster.com/products/cabinet-hardware/", + "families": ["hinge"] + }, + { + "group": "machine-hardware-and-structural", + "url": "https://www.mcmaster.com/products/handles/", + "families": ["bridgehandle", "leverhandle", "starknob"] + }, + { + "group": "machine-hardware-and-structural", + "url": "https://www.mcmaster.com/products/cabinet-door-hardware/", + "families": ["drawlatch"] + } + ], + "checks": { + "proposalCount": 100, + "uniqueProposalCount": 100, + "directNameCollisions": [], + "noSizeVariantsCounted": true, + "previewContractPrFileListsReviewed": [ + "tscircuit/modelprinter#41", + "tscircuit/modelprinter#54", + "tscircuit/modelprinter#76", + "tscircuit/jscad-electronics#429" + ], + "finalDuplicateCheck": "No additional seveibar PRs created on or after 2026-10-12 were found before opening this pair." + } +} diff --git a/docs/industrial-plumbing.md b/docs/industrial-plumbing.md new file mode 100644 index 0000000..a623fd8 --- /dev/null +++ b/docs/industrial-plumbing.md @@ -0,0 +1,59 @@ +# Industrial plumbing models + +These are configurable nominal assembly models for industrial automation layouts. +Dimensions are explicit millimeters, rather than a claim about a supplier catalog, +NPT, BSP, SAE, or ISO pipe interchangeability. Threaded regions are smooth nominal +envelopes. They are suitable for assembly visualization and interference envelopes; +they are not machining drawings, hydraulic simulations, or pressure ratings. + +All props schemas are strict. Lengths accept numbers in millimeters or complete +strings with supported units (`mm`, `cm`, `m`, `in`, `inch`, `mil`, `ft`, `feet`). +Lengths must be positive and relationships must leave material around flow bores. +Counts are bounded unitless integers. Duplicate aliases, unknown fields or tokens, +malformed values, zero/negative dimensions, and impossible mating dimensions fail. +The schemas expose `*ModelPropsInput`, normalized `*ModelProps`, and +`*ModelDefinition`; `get*Dimensions()` supplies normalized named dimensions. +Bare model names use the documented defaults below; every dimension is overrideable. + +| Model | Fundamental geometry | Explicit default model string | +| --- | --- | --- | +| `rigidpipe` | Straight open tube with explicit wall thickness | `rigidpipe_od20mm_bore16mm_l100mm` | +| `pipeelbow` | Cast 90-degree elbow with a mitered internal passage, two connected perpendicular ports and enlarged end sockets | `pipeelbow_od24mm_bore16mm_leg36mm_socket28mm_socketl10mm` | +| `pipetee` | Three-way fitting with two inline ports and one perpendicular branch | `pipetee_od24mm_bore16mm_l70mm_branch35mm_socket28mm_socketl10mm` | +| `pipecross` | Four-way fitting with perpendicular through bores | `pipecross_od24mm_bore16mm_span70mm_socket28mm_socketl10mm` | +| `pipecoupling` | Inline sleeve with recessed female socket counterbores and a smaller connecting bore | `pipecoupling_od30mm_bore16mm_socket20mm_l40mm_depth14mm` | +| `pipeunion` | Captive hex swivel nut with annular seams and two tubular ends | `pipeunion_od30mm_bore16mm_l55mm_af36mm_nutl20mm_endod24mm` | +| `pipenipple` | Tubular connector with two smooth thread envelopes and a raised middle shoulder | `pipenipple_od20mm_bore16mm_l50mm_threadl12mm_shoulder22mm` | +| `pipecap` | Blind female socket closed by a floor and hex outer end | `pipecap_od30mm_socket20mm_l25mm_floor4mm_af32mm` | +| `pipeplug` | Solid male plug with a projecting hex head | `pipeplug_d20mm_l20mm_af26mm_head8mm` | +| `reducingbushing` | Hex shoulder and continuously tapered large-to-small bore | `reducingbushing_od26mm_largebore18mm_smallbore10mm_l30mm_af30mm_hexl10mm` | +| `hosebarb` | Hex wrench shoulder, reduced hose stem and several tapered retaining barbs | `hosebarb_od20mm_bore10mm_l45mm_barbod16mm_barbl25mm_pitch6mm_af24mm` | +| `bulkheadfitting` | Tubular body, fixed flange and opposing retaining nut separated by a panel gap | `bulkheadfitting_od20mm_bore12mm_l55mm_flange32mm_flanget4mm_af28mm_nutt8mm_gap10mm` | +| `pushconnectfitting` | Hex body and slotted release collet surrounding a recessed tube socket | `pushconnectfitting_od26mm_bore8mm_l40mm_tube12mm_collet20mm_colletl6mm_af28mm` | +| `compressionfitting` | Inline body, paired compression nuts and recessed tube sockets | `compressionfitting_od22mm_bore8mm_l40mm_tube12mm_af26mm_nutl10mm` | +| `hoseclamp` | Slotted circular band, worm housing, screw and hex drive head | `hoseclamp_id30mm_bandt1.5mm_bandw8mm_housingl14mm_housingw8mm_housingh7mm` | +| `pipeflange` | Raised tubular hub, annular flange and configurable bolt-circle holes | `pipeflange_flange70mm_bore16mm_hub30mm_flanget8mm_hubl20mm_bc50mm_hole6mm_bolts4` | +| `ballvalve` | Spherical central valve envelope, inline ports, stem and longitudinal lever | `ballvalve_od30mm_bore16mm_l65mm_endod24mm_handlel55mm_handlew10mm_stemh14mm_handlet4mm` | +| `checkvalve` | Inline housing with hex ends, service cap and cast flow-direction arrow | `checkvalve_od28mm_bore12mm_l60mm_endod20mm_af30mm_nutl12mm` | +| `needlevalve` | Block body, inline ports, raised stem and perforated handwheel | `needlevalve_bodyl38mm_bodyw24mm_bodyh26mm_bore8mm_port16mm_portl10mm_stemh24mm_wheel26mm_wheelt5mm` | +| `manifold` | Rectangular distribution block with inline gallery, connected upper ports and mounting holes | `manifold_l100mm_w30mm_h26mm_bore12mm_port8mm_pitch25mm_ports3_hole5mm` | + +Model string short names map directly to the corresponding dimension properties. +For example, `rigidpipe_od20mm_bore16mm_l100mm` produces `outerDiameter: 20`, +`boreDiameter: 16`, and `length: 100`. A property's full lowercase name is also +accepted, so `outerdiameter20mm` and `od20mm` are aliases and cannot both appear. +`af` means the relevant hex dimension across flats. `bolts` and `ports` are counts. + +`socketDiameter` describes the outside socket collar on elbows, tees and crosses; +on `pipecoupling` and `pipecap` it describes the inside receiving recess. `boreDiameter` +is always the open flow passage. `tubeDiameter` describes a recessed receiving +socket on push-connect and compression fittings. `panelGap` describes the clear +axial space between the fixed bulkhead flange and the retaining nut. + +Valve strings model the nominal open assembly only. There is no selectable flow +state or implied kinematic valve mechanism. `needlevalve` supplies a generic block +and handwheel envelope; its small metering internals are intentionally omitted. + +The 49 contract tests check unit conversion, defaults, complete-string routing, +strict props, alias duplicates, malformed values, impossible mating relationships +and bounded counts. Geometry and four-view snapshots live in jscad-electronics. diff --git a/docs/machine-hardware.md b/docs/machine-hardware.md new file mode 100644 index 0000000..9c00e45 --- /dev/null +++ b/docs/machine-hardware.md @@ -0,0 +1,28 @@ +# Machine hardware model catalog + +These twenty parametric families describe fundamental machine-building forms. All normalized dimensions are millimeters. Unit strings (`mm`, `cm`, `m`, `in`, `mil`, `ft`) are accepted for length fields; integer counts are unitless. Bare family names select defaults. Models reject unknown and duplicate tokens and invalid dimensional relationships. + +These are nominal visual models for assembly layouts. They do not claim McMaster-Carr part compatibility, manufacturing dimensions, certified load ratings, mating fits or a dimensional standard. Threads are smooth envelopes; latch/clamp/caster internal mechanisms and the detent-pin spring are omitted. Expanded metal is the flat-plan aperture pattern; blades have a swept plan and nominal constant thickness. Square key has clipped plan corners rather than a full 3D edge break. + +| Model | Fundamental form | Full example | +| --- | --- | --- | +| `hinge` | Flat open butt hinge, leaves in XY, hinge axis along Y; bottom Z=0. | `hinge_width60mm_length60mm_leafthickness3mm_barreldiameter10mm_pindiameter4mm_holediameter5mm` | +| `barrelhinge` | Vertical barrel hinge, axis Z, opposed mounting tabs, bottom Z=0. | `barrelhinge_height60mm_barreldiameter14mm_pindiameter6mm_leafwidth20mm_leafthickness4mm_holediameter5mm` | +| `drawlatch` | Nominal closed draw latch with base, handle, wire bail and keeper; long axis X. | `drawlatch_length100mm_width25mm_basethickness4mm_handleheight18mm_wirediameter4mm_holediameter4mm` | +| `toggleclamp` | Nominal closed hold-down toggle clamp, long axis X, handle upright, bottom Z=0. | `toggleclamp_length110mm_width35mm_basethickness5mm_handleheight55mm_armlength75mm_paddiameter14mm_holediameter5mm` | +| `levelingfoot` | Leveling foot with puck, nominal smooth threaded-stem envelope and hex lock nut, axis Z. | `levelingfoot_basediameter50mm_baseheight12mm_stemdiameter12mm_stemlength55mm_hexdiameter24mm_hexheight8mm` | +| `caster` | Nominal fixed caster assembly: rolling axis Y, plate in XY, wheel contact Z=0. | `caster_wheeldiameter75mm_wheelwidth24mm_axlediameter10mm_forkthickness5mm_platewidth50mm_platelength65mm_platethickness5mm_overallheight110mm_holediameter5mm` | +| `rubberbumper` | Cylindrical bumper with axial through bore and top counterbore, axis Z. | `rubberbumper_diameter30mm_height15mm_holediameter6mm_counterborediameter13mm_counterboredepth5mm` | +| `bridgehandle` | Bridge handle along X with two mounting feet and axial mounting holes; bottom Z=0. | `bridgehandle_length120mm_gripdiameter15mm_height40mm_mountingdiameter24mm_mountingheight5mm_holediameter5mm` | +| `handwheel` | Flat-rim spoked handwheel with axial bore, rotating axis Z. | `handwheel_outerdiameter100mm_rimwidth12mm_hubdiameter28mm_hubheight25mm_borediameter12mm_spokes4` | +| `starknob` | Lobed star knob with bottom hub and axial through bore, axis Z. | `starknob_diameter40mm_height16mm_borediameter6mm_lobes5_hubdiameter18mm_hubheight10mm` | +| `leverhandle` | Lever handle extending along +X from bored hub; bottom Z=0. | `leverhandle_length90mm_width18mm_height16mm_hubdiameter28mm_hubheight25mm_borediameter8mm` | +| `squarekey` | Parallel shaft key, length X, width Y, height Z; clipped plan corners, bottom Z=0. | `squarekey_width8mm_height8mm_length40mm_chamfer1mm` | +| `woodruffkey` | Circular segment key in XZ, thickness along Y, flat top at Z=height, bottom Z=0. | `woodruffkey_diameter30mm_thickness6mm_height12mm` | +| `ibeam` | I-section stock, centered XY cross section, length +Z from Z=0. | `ibeam_width40mm_height60mm_webthickness4mm_flangethickness6mm_length100mm` | +| `teebar` | T-section stock, centered XY envelope, flange at maximum Y, length +Z. | `teebar_width40mm_height40mm_webthickness4mm_flangethickness6mm_length100mm` | +| `roundbar` | Round stock along +Z with equal end chamfers; bottom Z=0. | `roundbar_diameter20mm_length100mm_chamfer1mm` | +| `splitringhanger` | Nominal two-piece pipe hanger: pipe axis Y, horizontal split, lug bolt holes and top mounting boss. | `splitringhanger_pipediameter32mm_bandthickness3mm_bandwidth15mm_gap2mm_lugwidth14mm_holediameter5mm` | +| `detentpin` | Nominal ball detent pin, shaft Z, transverse pull ring in XZ; mechanism internals omitted. | `detentpin_shaftdiameter8mm_shaftlength50mm_headdiameter18mm_headheight6mm_balldiameter3mm_balloffset4mm_ringdiameter24mm_ringwire2mm` | +| `expandedmetal` | Flat expanded-metal visual sheet with staggered diamond apertures and perimeter frame; XY, bottom Z=0. | `expandedmetal_width100mm_length100mm_thickness2mm_diamondwidth15mm_diamondlength30mm_strandwidth2mm_framewidth3mm` | +| `coolingfan` | Nominal axial cooling fan with square shroud, four mount holes, curved-plan blades and support spokes; airflow axis Z. | `coolingfan_size80mm_thickness25mm_hubdiameter28mm_bladecount7_bladewidth12mm_bladethickness3mm_mountingholediameter4mm_framethickness6mm` | diff --git a/docs/motion-models.md b/docs/motion-models.md new file mode 100644 index 0000000..8294e7a --- /dev/null +++ b/docs/motion-models.md @@ -0,0 +1,294 @@ +# Fundamental motion and bearing models + +These models describe configurable nominal assembly geometry in millimeters. They use generic industrial names rather than manufacturer part numbers. Count fields are unitless integers; dimensions accept millimeters and explicit common length units. All models accept the bare family name to use the defaults below, reject unknown or duplicate properties, and reject nonpositive dimensions and incompatible geometric relations. + +## `pillowblockbearing` + +Mounted radial bearing in a raised pillow housing. Shaft axis is Y; the mounting-foot bottom is Z=0. Open front race shows rolling elements. Clearance, cage and grease hardware are nominal; no load rating is implied. + +```text +pillowblockbearing_bore20mm_bw80mm_bd30mm_bt8mm_hd46mm_ch30mm_ms60mm_mh6mm +``` + +| Property | Short token | Default | +|---|---|---| +| `boreDiameter` | `bore` | 20 mm | +| `baseWidth` | `bw` | 80 mm | +| `baseDepth` | `bd` | 30 mm | +| `baseThickness` | `bt` | 8 mm | +| `housingDiameter` | `hd` | 46 mm | +| `centerHeight` | `ch` | 30 mm | +| `mountSpacing` | `ms` | 60 mm | +| `mountHoleDiameter` | `mh` | 6 mm | + +Visible features: shaft bore along Y, two mounting holes, exposed front balls and raised housing. + +## `flangebearing` + +Square four-bolt flange bearing with an axial Z shaft bore and open top rolling-element race. Flange bottom is Z=0. Nominal assembly envelope and race clearances; not a manufacturer part or load-rated design. + +```text +flangebearing_bore20mm_fw64mm_ft6mm_hd44mm_hh20mm_ms46mm_mh6mm +``` + +| Property | Short token | Default | +|---|---|---| +| `boreDiameter` | `bore` | 20 mm | +| `flangeWidth` | `fw` | 64 mm | +| `flangeThickness` | `ft` | 6 mm | +| `housingDiameter` | `hd` | 44 mm | +| `housingHeight` | `hh` | 20 mm | +| `mountSpacing` | `ms` | 46 mm | +| `mountHoleDiameter` | `mh` | 6 mm | + +Visible features: four mounting bores, hub shoulder, shaft bore and exposed balls. + +## `needlebearing` + +Open straight-needle radial bearing. Axis is Z and lower face is Z=0. Inner and outer races enclose separately visible axial needles. Roller sizes/clearances are nominal and do not define a bearing standard or rating. + +```text +needlebearing_id20mm_od30mm_w20mm_race1.5mm_rollers18 +``` + +| Property | Short token | Default | +|---|---|---| +| `innerDiameter` | `id` | 20 mm | +| `outerDiameter` | `od` | 30 mm | +| `width` | `w` | 20 mm | +| `raceThickness` | `race` | 1.5 mm | +| `rollerCount` | `rollers` | 18 | + +Visible features: long axial needles, open races and through shaft bore. + +## `cylindricalrollerbearing` + +Open cylindrical-roller radial bearing with short axial rollers between inner and outer races and an outer retaining lip. Axis Z, bottom Z=0. Assembly envelope only; cage/preload and load rating are not specified. + +```text +cylindricalrollerbearing_id20mm_od42mm_w16mm_race3mm_rollers12 +``` + +| Property | Short token | Default | +|---|---|---| +| `innerDiameter` | `id` | 20 mm | +| `outerDiameter` | `od` | 42 mm | +| `width` | `w` | 16 mm | +| `raceThickness` | `race` | 3 mm | +| `rollerCount` | `rollers` | 12 | + +Visible features: short cylindrical rollers, retaining lips and connected axial bore. + +## `camfollower` + +Stud-mounted cam follower with an exposed open roller race and hex drive socket. Stud is on Z, from Z=0 to studLength; wheel sits above it. Smooth stud represents the thread major-diameter envelope, not a screw thread. + +```text +camfollower_od32mm_w14mm_stud10mm_sl26mm_socket5mm_rollers10 +``` + +| Property | Short token | Default | +|---|---|---| +| `wheelDiameter` | `od` | 32 mm | +| `wheelWidth` | `w` | 14 mm | +| `studDiameter` | `stud` | 10 mm | +| `studLength` | `sl` | 26 mm | +| `socketDiameter` | `socket` | 5 mm | +| `rollerCount` | `rollers` | 10 | + +Visible features: stud, hex socket, outer wheel and exposed rolling elements. + +## `vgroovewheel` + +Symmetric V-groove track wheel with straight axial bore. Z axis, bottom Z=0. Groove flanks run from 20%/80% of the width to the central root; this nominal track envelope does not specify a bearing or rail standard. + +```text +vgroovewheel_od40mm_root28mm_bore8mm_w12mm +``` + +| Property | Short token | Default | +|---|---|---| +| `outerDiameter` | `od` | 40 mm | +| `rootDiameter` | `root` | 28 mm | +| `boreDiameter` | `bore` | 8 mm | +| `width` | `w` | 12 mm | + +Visible features: deep continuous circumferential V groove and through bore. + +## `rodend` + +Rod-end eye with a curved spherical inner insert and smooth male-stem envelope pointing toward -Y. Eye axis Z, bottom Z=0. Thread major diameter only, no threads, articulation limit or load rating. + +```text +rodend_hd28mm_bore12mm_w12mm_stem10mm_sl30mm +``` + +| Property | Short token | Default | +|---|---|---| +| `headDiameter` | `hd` | 28 mm | +| `boreDiameter` | `bore` | 12 mm | +| `headWidth` | `w` | 12 mm | +| `stemDiameter` | `stem` | 10 mm | +| `stemLength` | `sl` | 30 mm | + +Visible features: eye with curved spherical insert, axial bore and extended stem. + +## `sphericalbearing` + +Spherical plain bearing assembly: cylindrical outer race with spherical pocket and truncated spherical inner ring with an axial bore. Axis Z, bottom Z=0. Outer race is recessed to 72% of inner-ring width to expose the spherical shoulders. Small nominal spherical clearance; articulation and tolerances are not modeled. + +```text +sphericalbearing_od30mm_bore12mm_w14mm_ball22mm +``` + +| Property | Short token | Default | +|---|---|---| +| `outerDiameter` | `od` | 30 mm | +| `boreDiameter` | `bore` | 12 mm | +| `width` | `w` | 14 mm | +| `ballDiameter` | `ball` | 22 mm | + +Visible features: spherical mating surfaces and curved inner bore ring. + +## `clevis` + +Fork clevis with parallel ears, shared cross-pin bore along X and rear mounting bore along Y. Rear plane Y=0, underside Z=0. Bores are smooth; pin/thread geometry and strength are not implied. + +```text +clevis_w30mm_l45mm_h20mm_gap14mm_back10mm_pin8mm_mount10mm +``` + +| Property | Short token | Default | +|---|---|---| +| `width` | `w` | 30 mm | +| `length` | `l` | 45 mm | +| `height` | `h` | 20 mm | +| `gap` | `gap` | 14 mm | +| `backThickness` | `back` | 10 mm | +| `pinDiameter` | `pin` | 8 mm | +| `mountDiameter` | `mount` | 10 mm | + +Visible features: open fork slot, aligned ear pin bores and rear mounting bore. + +## `gearrack` + +Straight rack envelope along X with trapezoidal teeth above a mounting bar and two vertical mounting holes. Base underside Z=0. Teeth are nominal placement geometry, not conjugate involute teeth or a machining standard. + +```text +gearrack_pitch8mm_teeth12_w12mm_h8mm_th4mm_mh3.5mm +``` + +| Property | Short token | Default | +|---|---|---| +| `pitch` | `pitch` | 8 mm | +| `toothCount` | `teeth` | 12 | +| `width` | `w` | 12 mm | +| `baseHeight` | `h` | 8 mm | +| `toothHeight` | `th` | 4 mm | +| `mountHoleDiameter` | `mh` | 3.5 mm | + +Visible features: repeated trapezoidal rack teeth and two mounting bores. + +## `sprocket` + +Chain-sprocket envelope with circular roller pockets between radial teeth, hub shoulder and through bore. Axis Z, bottom Z=0. Tooth/pocket outline is nominal, not a chain pitch standard, conjugate roller seating profile or cutting specification. + +```text +sprocket_od60mm_root48mm_w8mm_bore12mm_teeth16_hub26mm_hh10mm +``` + +| Property | Short token | Default | +|---|---|---| +| `outerDiameter` | `od` | 60 mm | +| `rootDiameter` | `root` | 48 mm | +| `width` | `w` | 8 mm | +| `boreDiameter` | `bore` | 12 mm | +| `toothCount` | `teeth` | 16 | +| `hubDiameter` | `hub` | 26 mm | +| `hubHeight` | `hh` | 10 mm | + +Visible features: radial teeth/valleys, hub shoulder and through bore. + +## `rollerchain` + +Straight roller-chain segment along X with alternating inner/outer side plates, discrete rollers and projecting rivet pins. Roller axes Z. Nominal pitch/width placement assembly; not a named chain standard or flexible/meshing simulation. + +```text +rollerchain_pitch12.7mm_links6_roller7.9mm_w8mm_pin4mm_plate1.2mm +``` + +| Property | Short token | Default | +|---|---|---| +| `pitch` | `pitch` | 12.7 mm | +| `linkCount` | `links` | 6 | +| `rollerDiameter` | `roller` | 7.9 mm | +| `width` | `w` | 8 mm | +| `pinDiameter` | `pin` | 4 mm | +| `plateThickness` | `plate` | 1.2 mm | + +Visible features: alternating side plates, free rollers and projecting pin heads. + +## `vbeltsheave` + +Multi-groove V-belt sheave with sloped circumferential groove flanks, small root land, hub and through bore. Axis Z, bottom Z=0. Nominal groove envelope; belt section standard, taper bushing and keyway are not specified. + +```text +vbeltsheave_od60mm_bore12mm_w24mm_grooves2_depth8mm_gw9mm_hub26mm_hh8mm +``` + +| Property | Short token | Default | +|---|---|---| +| `outerDiameter` | `od` | 60 mm | +| `boreDiameter` | `bore` | 12 mm | +| `width` | `w` | 24 mm | +| `grooveCount` | `grooves` | 2 | +| `grooveDepth` | `depth` | 8 mm | +| `grooveWidth` | `gw` | 9 mm | +| `hubDiameter` | `hub` | 26 mm | +| `hubHeight` | `hh` | 8 mm | + +Visible features: separate V grooves, root lands, hub shoulder and through bore. + +## `conveyorroller` + +Hollow conveyor tube with end plugs and a continuous axle projecting beyond each end. Axis Z, tube spans Z=0..length and axle starts at negative shaftExtension. End plugs are nominal; internal bearings and shaft flats are not specified. + +```text +conveyorroller_od50mm_l150mm_wall3mm_shaft10mm_ext15mm_end4mm +``` + +| Property | Short token | Default | +|---|---|---| +| `outerDiameter` | `od` | 50 mm | +| `length` | `l` | 150 mm | +| `wallThickness` | `wall` | 3 mm | +| `shaftDiameter` | `shaft` | 10 mm | +| `shaftExtension` | `ext` | 15 mm | +| `endThickness` | `end` | 4 mm | + +Visible features: hollow tube midsection, end plugs and two projecting shaft ends. + +## `shaftsupport` + +Split shaft support with a horizontal Y shaft bore, top clamping slit, transverse X clamp-screw bore and two vertical foot mounting bores. Bottom Z=0. No thread, clamp hardware or load rating is specified. + +```text +shaftsupport_bore16mm_bw55mm_bd32mm_bt7mm_hw26mm_hd20mm_hh34mm_ch26mm_ms40mm_mh5mm_slit1.5mm_clamp4mm +``` + +| Property | Short token | Default | +|---|---|---| +| `boreDiameter` | `bore` | 16 mm | +| `baseWidth` | `bw` | 55 mm | +| `baseDepth` | `bd` | 32 mm | +| `baseThickness` | `bt` | 7 mm | +| `headWidth` | `hw` | 26 mm | +| `headDepth` | `hd` | 20 mm | +| `headHeight` | `hh` | 34 mm | +| `centerHeight` | `ch` | 26 mm | +| `mountSpacing` | `ms` | 40 mm | +| `mountHoleDiameter` | `mh` | 5 mm | +| `slitWidth` | `slit` | 1.5 mm | +| `clampHoleDiameter` | `clamp` | 4 mm | + +Visible features: shaft bore, connected top slit, clamp screw bore and mounting foot bores. diff --git a/src/industrial-parameter-utils.ts b/src/industrial-parameter-utils.ts new file mode 100644 index 0000000..613fa33 --- /dev/null +++ b/src/industrial-parameter-utils.ts @@ -0,0 +1,55 @@ +import { z } from "zod" +import { modelLengthSchema } from "./model-length-schema" +import type { RawModelprinterParams } from "./parse-model-string" +import { splitModelStringTokens } from "./split-model-string-tokens" + +/** Finite decimal lengths in millimeters; explicit common units are accepted. */ +export const industrialLengthSchema = z + .union([ + z.number().finite(), + z + .string() + .regex(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:mm|cm|m|in|inch|mil|ft|feet)?$/i) + .transform((value) => value.toLowerCase()), + ]) + .pipe(modelLengthSchema) + +/** Model-local aliases select typed schema fields; unknown/duplicate tokens fail. */ +export function parseIndustrialModelParams( + raw: RawModelprinterParams, + name: string, + schema: Schema, + aliases: Readonly>, + flags: Readonly> = {}, +): z.output { + const tokens = splitModelStringTokens(raw.string) + if (raw.fn !== name || tokens[0]?.toLowerCase() !== name) + throw new Error(`Expected ${name} without an inline value`) + const props: Record = { fn: name } + for (const token of tokens.slice(1)) { + const match = token.match(/^([a-z]+)(.*)$/i) + if (!match) throw new Error(`Invalid ${name} token "${token}"`) + const key = match[1]!.toLowerCase() + const value = match[2]! + let property: string + let parsed: unknown + if (Object.hasOwn(flags, key)) { + property = flags[key]! + if (value !== "") throw new Error(`${key} is a value-free flag`) + parsed = true + } else if (Object.hasOwn(aliases, key)) { + property = aliases[key]! + if ( + !/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:mm|cm|m|in|inch|mil|ft|feet)?$/i.test( + value, + ) + ) + throw new Error(`Invalid ${name} value "${token}"`) + parsed = /[a-z]/i.test(value) ? value : Number(value) + } else throw new Error(`Unknown ${name} token "${token}"`) + if (Object.hasOwn(props, property)) + throw new Error(`${name} property "${property}" is set more than once`) + props[property] = parsed + } + return schema.parse(props) +} diff --git a/src/models/airfilter/index.ts b/src/models/airfilter/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/airfilter/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/airfilter/parse-model-string.ts b/src/models/airfilter/parse-model-string.ts new file mode 100644 index 0000000..8d8ceac --- /dev/null +++ b/src/models/airfilter/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { airFilterModelDefinitionSchema } from "./schema" +const aliases = { + bodywidth: "bodyWidth", + bodydepth: "bodyDepth", + bodyheight: "bodyHeight", + bowldiameter: "bowlDiameter", + bowllength: "bowlLength", + portdiameter: "portDiameter", + drainlength: "drainLength", +} as const +export function parseAirFilterModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "airfilter", + airFilterModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/airfilter/register.ts b/src/models/airfilter/register.ts new file mode 100644 index 0000000..c311781 --- /dev/null +++ b/src/models/airfilter/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { airFilterModelDefinitionSchema } from "./schema" +import { parseAirFilterModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "airfilter", + schema: airFilterModelDefinitionSchema, + parse: parseAirFilterModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/airfilter/schema.ts b/src/models/airfilter/schema.ts new file mode 100644 index 0000000..6f7ddca --- /dev/null +++ b/src/models/airfilter/schema.ts @@ -0,0 +1,64 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + bodyWidth: positive.default(40), + bodyDepth: positive.default(40), + bodyHeight: positive.default(30), + bowlDiameter: positive.default(28), + bowlLength: positive.default(50), + portDiameter: positive.default(8), + drainLength: positive.default(8), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.bowlDiameter >= Math.min(p.bodyWidth, p.bodyDepth)) + context.addIssue({ + code: "custom", + message: "Incompatible airfilter dimensions: 1", + }) + if (p.portDiameter >= Math.min(p.bodyHeight, p.bodyDepth) / 2) + context.addIssue({ + code: "custom", + message: "Incompatible airfilter dimensions: 2", + }) + if (p.drainLength >= p.bowlLength / 2) + context.addIssue({ + code: "custom", + message: "Incompatible airfilter dimensions: 3", + }) +} +export const airFilterModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const airFilterModelDefinitionSchema = z + .object({ fn: z.literal("airfilter"), ...shape }) + .strict() + .superRefine(validate) +export type AirFilterModelPropsInput = z.input +export type AirFilterModelProps = z.output +export type AirFilterModelDefinition = z.output< + typeof airFilterModelDefinitionSchema +> +/** Pneumatic filter with a bowl guard, bottom drain and through ports in the head. Bowl and guard are a solid visual envelope; the filter media and transparent bowl interior are omitted. The drain is bored; there are no supplier-specific threads. All normalized lengths are millimeters. */ +export function getAirFilterDimensions(input: AirFilterModelPropsInput = {}) { + const p = airFilterModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.bodyWidth, + p.bodyDepth, + p.drainLength + p.bowlLength + p.bodyHeight, + ] as [number, number, number], + } +} diff --git a/src/models/airlubricator/index.ts b/src/models/airlubricator/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/airlubricator/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/airlubricator/parse-model-string.ts b/src/models/airlubricator/parse-model-string.ts new file mode 100644 index 0000000..98aa0fb --- /dev/null +++ b/src/models/airlubricator/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { airLubricatorModelDefinitionSchema } from "./schema" +const aliases = { + bodywidth: "bodyWidth", + bodydepth: "bodyDepth", + bodyheight: "bodyHeight", + bowldiameter: "bowlDiameter", + bowllength: "bowlLength", + portdiameter: "portDiameter", + filldiameter: "fillDiameter", + fillheight: "fillHeight", +} as const +export function parseAirLubricatorModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "airlubricator", + airLubricatorModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/airlubricator/register.ts b/src/models/airlubricator/register.ts new file mode 100644 index 0000000..b6db44b --- /dev/null +++ b/src/models/airlubricator/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { airLubricatorModelDefinitionSchema } from "./schema" +import { parseAirLubricatorModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "airlubricator", + schema: airLubricatorModelDefinitionSchema, + parse: parseAirLubricatorModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/airlubricator/schema.ts b/src/models/airlubricator/schema.ts new file mode 100644 index 0000000..84b2fa6 --- /dev/null +++ b/src/models/airlubricator/schema.ts @@ -0,0 +1,71 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + bodyWidth: positive.default(40), + bodyDepth: positive.default(40), + bodyHeight: positive.default(30), + bowlDiameter: positive.default(28), + bowlLength: positive.default(50), + portDiameter: positive.default(8), + fillDiameter: positive.default(12), + fillHeight: positive.default(8), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.bowlDiameter + 4 >= Math.min(p.bodyWidth, p.bodyDepth)) + context.addIssue({ + code: "custom", + message: "Incompatible airlubricator dimensions: 1", + }) + if (p.portDiameter >= Math.min(p.bodyHeight, p.bodyDepth) / 2) + context.addIssue({ + code: "custom", + message: "Incompatible airlubricator dimensions: 2", + }) + if (p.fillDiameter >= Math.min(p.bodyWidth, p.bodyDepth) / 2) + context.addIssue({ + code: "custom", + message: "Incompatible airlubricator dimensions: 3", + }) +} +export const airLubricatorModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const airLubricatorModelDefinitionSchema = z + .object({ fn: z.literal("airlubricator"), ...shape }) + .strict() + .superRefine(validate) +export type AirLubricatorModelPropsInput = z.input< + typeof airLubricatorModelPropsSchema +> +export type AirLubricatorModelProps = z.output< + typeof airLubricatorModelPropsSchema +> +export type AirLubricatorModelDefinition = z.output< + typeof airLubricatorModelDefinitionSchema +> +/** Air-line lubricator with cylindrical reservoir, sight recess, retaining collar and top fill cap. The solid reservoir is an external envelope and does not represent oil volume or the metering internals. All normalized lengths are millimeters. */ +export function getAirLubricatorDimensions( + input: AirLubricatorModelPropsInput = {}, +) { + const p = airLubricatorModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.bodyWidth, + p.bodyDepth, + p.bowlLength + p.bodyHeight + p.fillHeight, + ] as [number, number, number], + } +} diff --git a/src/models/airregulator/index.ts b/src/models/airregulator/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/airregulator/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/airregulator/parse-model-string.ts b/src/models/airregulator/parse-model-string.ts new file mode 100644 index 0000000..b3cb115 --- /dev/null +++ b/src/models/airregulator/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { airRegulatorModelDefinitionSchema } from "./schema" +const aliases = { + bodywidth: "bodyWidth", + bodydepth: "bodyDepth", + bodyheight: "bodyHeight", + knobdiameter: "knobDiameter", + knobheight: "knobHeight", + portdiameter: "portDiameter", + gaugediameter: "gaugeDiameter", +} as const +export function parseAirRegulatorModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "airregulator", + airRegulatorModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/airregulator/register.ts b/src/models/airregulator/register.ts new file mode 100644 index 0000000..c95959c --- /dev/null +++ b/src/models/airregulator/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { airRegulatorModelDefinitionSchema } from "./schema" +import { parseAirRegulatorModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "airregulator", + schema: airRegulatorModelDefinitionSchema, + parse: parseAirRegulatorModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/airregulator/schema.ts b/src/models/airregulator/schema.ts new file mode 100644 index 0000000..0bb8e34 --- /dev/null +++ b/src/models/airregulator/schema.ts @@ -0,0 +1,70 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + bodyWidth: positive.default(40), + bodyDepth: positive.default(36), + bodyHeight: positive.default(30), + knobDiameter: positive.default(28), + knobHeight: positive.default(20), + portDiameter: positive.default(8), + gaugeDiameter: positive.default(20), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.knobDiameter * 1.01 >= Math.min(p.bodyWidth, p.bodyDepth)) + context.addIssue({ + code: "custom", + message: "Incompatible airregulator dimensions: 1", + }) + if (p.portDiameter >= Math.min(p.bodyHeight, p.bodyDepth) / 2) + context.addIssue({ + code: "custom", + message: "Incompatible airregulator dimensions: 2", + }) + if (p.gaugeDiameter >= p.bodyHeight) + context.addIssue({ + code: "custom", + message: "Incompatible airregulator dimensions: 3", + }) +} +export const airRegulatorModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const airRegulatorModelDefinitionSchema = z + .object({ fn: z.literal("airregulator"), ...shape }) + .strict() + .superRefine(validate) +export type AirRegulatorModelPropsInput = z.input< + typeof airRegulatorModelPropsSchema +> +export type AirRegulatorModelProps = z.output< + typeof airRegulatorModelPropsSchema +> +export type AirRegulatorModelDefinition = z.output< + typeof airRegulatorModelDefinitionSchema +> +/** Air pressure regulator with a fluted adjustment knob, front gauge bezel and lateral through ports. The head and gauge are nominal visual envelopes; concealed diaphragm and calibrated gauge markings are omitted. All normalized lengths are millimeters. */ +export function getAirRegulatorDimensions( + input: AirRegulatorModelPropsInput = {}, +) { + const p = airRegulatorModelPropsSchema.parse(input) + return { + ...p, + size: [p.bodyWidth, p.bodyDepth + 5, p.bodyHeight + p.knobHeight] as [ + number, + number, + number, + ], + } +} diff --git a/src/models/ballvalve/index.ts b/src/models/ballvalve/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/ballvalve/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/ballvalve/parse-model-string.ts b/src/models/ballvalve/parse-model-string.ts new file mode 100644 index 0000000..40379d5 --- /dev/null +++ b/src/models/ballvalve/parse-model-string.ts @@ -0,0 +1,28 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { ballValveModelDefinitionSchema } from "./schema" +export function parseBallValveModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "ballvalve", + ballValveModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + l: "length", + endod: "endDiameter", + handlel: "handleLength", + handlew: "handleWidth", + stemh: "stemHeight", + handlet: "handleThickness", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + length: "length", + enddiameter: "endDiameter", + handlelength: "handleLength", + handlewidth: "handleWidth", + stemheight: "stemHeight", + handlethickness: "handleThickness", + }, + ) +} diff --git a/src/models/ballvalve/register.ts b/src/models/ballvalve/register.ts new file mode 100644 index 0000000..51deb12 --- /dev/null +++ b/src/models/ballvalve/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parseBallValveModelParams } from "./parse-model-string" +import { ballValveModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "ballvalve", + schema: ballValveModelDefinitionSchema, + parse: parseBallValveModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/ballvalve/schema.ts b/src/models/ballvalve/schema.ts new file mode 100644 index 0000000..9be9673 --- /dev/null +++ b/src/models/ballvalve/schema.ts @@ -0,0 +1,72 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(30), + boreDiameter: positiveLength.default(16), + length: positiveLength.default(65), + endDiameter: positiveLength.default(24), + handleLength: positiveLength.default(55), + handleWidth: positiveLength.default(10), + stemHeight: positiveLength.default(14), + handleThickness: positiveLength.default(4), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.endDiameter)) + issue("Requires boreDiameter < endDiameter") + if (!(p.endDiameter < p.outerDiameter)) + issue("Requires endDiameter < outerDiameter") + if (!(p.outerDiameter * 2 < p.length)) + issue("Requires outerDiameter * 2 < length") + if (!(p.handleWidth < p.outerDiameter)) + issue("Requires handleWidth < outerDiameter") + if (!(p.handleThickness < p.stemHeight)) + issue("Requires handleThickness < stemHeight") + if (!(p.handleLength > p.outerDiameter)) + issue("Requires handleLength > outerDiameter") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.endDiameter - p.boreDiameter, + p.outerDiameter - p.endDiameter, + p.length - p.outerDiameter * 2, + p.outerDiameter - p.handleWidth, + p.stemHeight - p.handleThickness, + p.handleLength - p.outerDiameter, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const ballValveModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const ballValveModelDefinitionSchema = z + .object({ fn: z.literal("ballvalve"), ...shape }) + .strict() + .superRefine(validate) +export type BallValveModelPropsInput = z.input +export type BallValveModelProps = z.output +export type BallValveModelDefinition = z.output< + typeof ballValveModelDefinitionSchema +> +export function getBallValveDimensions(input: BallValveModelPropsInput = {}) { + return ballValveModelPropsSchema.parse(input) +} diff --git a/src/models/barrelhinge/index.ts b/src/models/barrelhinge/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/barrelhinge/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/barrelhinge/parse-model-string.ts b/src/models/barrelhinge/parse-model-string.ts new file mode 100644 index 0000000..05ea33b --- /dev/null +++ b/src/models/barrelhinge/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { barrelHingeModelDefinitionSchema } from "./schema" +const aliases = { + height: "height", + barreldiameter: "barrelDiameter", + pindiameter: "pinDiameter", + leafwidth: "leafWidth", + leafthickness: "leafThickness", + holediameter: "holeDiameter", + h: "height", +} as const +export function parseBarrelHingeModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "barrelhinge", + barrelHingeModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/barrelhinge/register.ts b/src/models/barrelhinge/register.ts new file mode 100644 index 0000000..d3dc3ef --- /dev/null +++ b/src/models/barrelhinge/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { barrelHingeModelDefinitionSchema } from "./schema" +import { parseBarrelHingeModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "barrelhinge", + schema: barrelHingeModelDefinitionSchema, + parse: parseBarrelHingeModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/barrelhinge/schema.ts b/src/models/barrelhinge/schema.ts new file mode 100644 index 0000000..257a2b3 --- /dev/null +++ b/src/models/barrelhinge/schema.ts @@ -0,0 +1,67 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + height: positiveLength.default(60), + barrelDiameter: positiveLength.default(14), + pinDiameter: positiveLength.default(6), + leafWidth: positiveLength.default(20), + leafThickness: positiveLength.default(4), + holeDiameter: positiveLength.default(5), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.height > 3 * p.barrelDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid barrelhinge dimensions: height > 3*barrelDiameter", + }) + if (!(p.pinDiameter < p.barrelDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid barrelhinge dimensions: pinDiameter < barrelDiameter", + }) + if (!(p.leafThickness < p.barrelDiameter / 2)) + context.addIssue({ + code: "custom", + message: + "Invalid barrelhinge dimensions: leafThickness < barrelDiameter/2", + }) + if (!(p.leafWidth > p.barrelDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid barrelhinge dimensions: leafWidth > barrelDiameter", + }) + if (!(p.holeDiameter < Math.min(p.leafWidth, p.height / 4) / 2)) + context.addIssue({ + code: "custom", + message: + "Invalid barrelhinge dimensions: holeDiameter < Math.min(leafWidth,height/4)/2", + }) +} +export const barrelHingeModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const barrelHingeModelDefinitionSchema = z + .object({ fn: z.literal("barrelhinge"), ...shape }) + .strict() + .superRefine(validate) +export type BarrelHingeModelPropsInput = z.input< + typeof barrelHingeModelPropsSchema +> +export type BarrelHingeModelProps = z.output +export type BarrelHingeModelDefinition = z.output< + typeof barrelHingeModelDefinitionSchema +> +/** Vertical barrel hinge, axis Z, opposed mounting tabs, bottom Z=0. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getBarrelHingeDimensions( + input: BarrelHingeModelPropsInput = {}, +) { + return barrelHingeModelPropsSchema.parse(input) +} diff --git a/src/models/beamcoupling/index.ts b/src/models/beamcoupling/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/beamcoupling/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/beamcoupling/parse-model-string.ts b/src/models/beamcoupling/parse-model-string.ts new file mode 100644 index 0000000..24c0704 --- /dev/null +++ b/src/models/beamcoupling/parse-model-string.ts @@ -0,0 +1,26 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { beamCouplingModelDefinitionSchema } from "./schema" +const aliases = { + od: "outerDiameter", + l: "length", + bore: "boreDiameter", + hubl: "hubLength", + slotw: "slotWidth", + slots: "slotCount", + slotcount: "slotCount", + bridgeangle: "bridgeAngle", + outerdiameter: "outerDiameter", + length: "length", + borediameter: "boreDiameter", + hublength: "hubLength", + slotwidth: "slotWidth", +} as const +export function parseBeamCouplingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "beamcoupling", + beamCouplingModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/beamcoupling/register.ts b/src/models/beamcoupling/register.ts new file mode 100644 index 0000000..de96cff --- /dev/null +++ b/src/models/beamcoupling/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { beamCouplingModelDefinitionSchema } from "./schema" +import { parseBeamCouplingModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "beamcoupling", + schema: beamCouplingModelDefinitionSchema, + parse: parseBeamCouplingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/beamcoupling/schema.ts b/src/models/beamcoupling/schema.ts new file mode 100644 index 0000000..47d85e5 --- /dev/null +++ b/src/models/beamcoupling/schema.ts @@ -0,0 +1,77 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (v) => v > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(25), + length: positiveLength.default(30), + boreDiameter: positiveLength.default(8), + hubLength: positiveLength.default(7), + slotWidth: positiveLength.default(1), + slotCount: z.number().int().min(2).max(20).default(6), + bridgeAngle: z.number().finite().min(20).max(120).default(60), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.boreDiameter >= p.outerDiameter) + context.addIssue({ + code: "custom", + message: "Bore must be smaller than the coupling diameter", + }) + if ((p.slotCount + 1) * p.slotWidth >= p.length - 2 * p.hubLength) + context.addIssue({ + code: "custom", + message: "Slots must leave axial flexure material between solid end hubs", + }) + const bounds = [ + p.outerDiameter, + p.length, + p.boreDiameter, + p.hubLength, + p.slotWidth, + ] + if (!Number.isFinite(bounds.reduce((sum, value) => sum + value, 0))) + context.addIssue({ + code: "custom", + message: "Combined dimensions must remain finite", + }) +} +export const beamCouplingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const beamCouplingModelDefinitionSchema = z + .object({ fn: z.literal("beamcoupling"), ...shape }) + .strict() + .superRefine(validate) +export type BeamCouplingModelPropsInput = z.input< + typeof beamCouplingModelPropsSchema +> +export type BeamCouplingModelProps = z.output< + typeof beamCouplingModelPropsSchema +> +export type BeamCouplingModelDefinition = z.output< + typeof beamCouplingModelDefinitionSchema +> +/** Coaxial beam coupling with staggered transverse flexure slots and intact radial bridges. Slots alternate by 90 degrees; this represents a slotted beam coupling, not a continuous machined helix. All lengths normalize to millimeters. */ +export function getBeamCouplingDimensions( + input: BeamCouplingModelPropsInput = {}, +) { + const p = beamCouplingModelPropsSchema.parse(input) + return { + ...p, + size: [p.outerDiameter, p.outerDiameter, p.length] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.length, + flexureLength: p.length - 2 * p.hubLength, + slotPitch: (p.length - 2 * p.hubLength) / (p.slotCount + 1), + } +} diff --git a/src/models/bellevillewasher/index.ts b/src/models/bellevillewasher/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/bellevillewasher/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/bellevillewasher/parse-model-string.ts b/src/models/bellevillewasher/parse-model-string.ts new file mode 100644 index 0000000..c5795e5 --- /dev/null +++ b/src/models/bellevillewasher/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { bellevilleWasherModelDefinitionSchema } from "./schema" +export function parseBellevilleWasherModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "bellevillewasher", + bellevilleWasherModelDefinitionSchema, + { + id: "innerDiameter", + innerdiameter: "innerDiameter", + od: "outerDiameter", + outerdiameter: "outerDiameter", + t: "thickness", + thickness: "thickness", + ch: "coneHeight", + coneheight: "coneHeight", + }, + ) +} diff --git a/src/models/bellevillewasher/register.ts b/src/models/bellevillewasher/register.ts new file mode 100644 index 0000000..bfd046a --- /dev/null +++ b/src/models/bellevillewasher/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { bellevilleWasherModelDefinitionSchema } from "./schema" +import { parseBellevilleWasherModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "bellevillewasher", + schema: bellevilleWasherModelDefinitionSchema, + parse: parseBellevilleWasherModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/bellevillewasher/schema.ts b/src/models/bellevillewasher/schema.ts new file mode 100644 index 0000000..a0fb00f --- /dev/null +++ b/src/models/bellevillewasher/schema.ts @@ -0,0 +1,66 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + innerDiameter: positiveLength.default(6.4), + outerDiameter: positiveLength.default(14), + thickness: positiveLength.default(0.8), + coneHeight: positiveLength.default(1.5), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![(p.outerDiameter - p.innerDiameter) / 2].every( + (gap) => gap >= extent * 0.0001, + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if (!(p.innerDiameter < p.outerDiameter)) + ctx.addIssue({ + code: "custom", + message: + "Invalid bellevillewasher dimensions: mating features must fit within the body", + }) +} +export const bellevilleWasherModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const bellevilleWasherModelDefinitionSchema = z + .object({ fn: z.literal("bellevillewasher"), ...shape }) + .strict() + .superRefine(validate) +export type BellevilleWasherModelPropsInput = z.input< + typeof bellevilleWasherModelPropsSchema +> +export type BellevilleWasherModelProps = z.output< + typeof bellevilleWasherModelPropsSchema +> +export type BellevilleWasherModelDefinition = z.output< + typeof bellevilleWasherModelDefinitionSchema +> +export function getBellevilleWasherDimensions( + input: BellevilleWasherModelPropsInput = {}, +) { + return bellevilleWasherModelPropsSchema.parse(input) +} diff --git a/src/models/bevelgear/index.ts b/src/models/bevelgear/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/bevelgear/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/bevelgear/parse-model-string.ts b/src/models/bevelgear/parse-model-string.ts new file mode 100644 index 0000000..d320560 --- /dev/null +++ b/src/models/bevelgear/parse-model-string.ts @@ -0,0 +1,28 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { bevelGearModelDefinitionSchema } from "./schema" +const aliases = { + od: "outerDiameter", + td: "topDiameter", + h: "height", + bore: "boreDiameter", + depth: "toothDepth", + hubd: "hubDiameter", + hubh: "hubHeight", + teeth: "teeth", + outerdiameter: "outerDiameter", + topdiameter: "topDiameter", + height: "height", + borediameter: "boreDiameter", + toothdepth: "toothDepth", + hubdiameter: "hubDiameter", + hubheight: "hubHeight", +} as const +export function parseBevelGearModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "bevelgear", + bevelGearModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/bevelgear/register.ts b/src/models/bevelgear/register.ts new file mode 100644 index 0000000..4531ef0 --- /dev/null +++ b/src/models/bevelgear/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { bevelGearModelDefinitionSchema } from "./schema" +import { parseBevelGearModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "bevelgear", + schema: bevelGearModelDefinitionSchema, + parse: parseBevelGearModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/bevelgear/schema.ts b/src/models/bevelgear/schema.ts new file mode 100644 index 0000000..e69ad37 --- /dev/null +++ b/src/models/bevelgear/schema.ts @@ -0,0 +1,80 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (v) => v > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(40), + topDiameter: positiveLength.default(24), + height: positiveLength.default(14), + boreDiameter: positiveLength.default(8), + toothDepth: positiveLength.default(3), + hubDiameter: positiveLength.default(20), + hubHeight: positiveLength.default(8), + teeth: z.number().int().min(6).max(96).default(16), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.topDiameter >= p.outerDiameter) + context.addIssue({ + code: "custom", + message: "Top diameter must be smaller than the large gear face", + }) + if (2 * p.toothDepth >= p.topDiameter - p.boreDiameter) + context.addIssue({ + code: "custom", + message: "Tooth roots must leave positive material around the bore", + }) + if (p.hubDiameter > p.topDiameter || p.hubDiameter <= p.boreDiameter) + context.addIssue({ + code: "custom", + message: "Hub must fit on the small gear face and around the shaft bore", + }) + const bounds = [ + p.outerDiameter, + p.topDiameter, + p.height, + p.boreDiameter, + p.toothDepth, + p.hubDiameter, + p.hubHeight, + ] + if (!Number.isFinite(bounds.reduce((sum, value) => sum + value, 0))) + context.addIssue({ + code: "custom", + message: "Combined dimensions must remain finite", + }) +} +export const bevelGearModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const bevelGearModelDefinitionSchema = z + .object({ fn: z.literal("bevelgear"), ...shape }) + .strict() + .superRefine(validate) +export type BevelGearModelPropsInput = z.input +export type BevelGearModelProps = z.output +export type BevelGearModelDefinition = z.output< + typeof bevelGearModelDefinitionSchema +> +/** Straight bevel gear with a tapering tooth envelope and a bored hub on its small face. Teeth use a nominal trapezoidal section for assembly visualization, not a conjugate or manufacturable tooth profile. All lengths normalize to millimeters. */ +export function getBevelGearDimensions(input: BevelGearModelPropsInput = {}) { + const p = bevelGearModelPropsSchema.parse(input) + return { + ...p, + size: [p.outerDiameter, p.outerDiameter, p.height + p.hubHeight] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.height + p.hubHeight, + pitchAngle: 360 / p.teeth, + rootDiameter: p.outerDiameter - 2 * p.toothDepth, + topRootDiameter: p.topDiameter - 2 * p.toothDepth, + } +} diff --git a/src/models/blindrivet/index.ts b/src/models/blindrivet/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/blindrivet/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/blindrivet/parse-model-string.ts b/src/models/blindrivet/parse-model-string.ts new file mode 100644 index 0000000..10f5232 --- /dev/null +++ b/src/models/blindrivet/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { blindRivetModelDefinitionSchema } from "./schema" +export function parseBlindRivetModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "blindrivet", + blindRivetModelDefinitionSchema, + { + d: "bodyDiameter", + bodydiameter: "bodyDiameter", + l: "bodyLength", + bodylength: "bodyLength", + fd: "flangeDiameter", + flangediameter: "flangeDiameter", + ft: "flangeThickness", + flangethickness: "flangeThickness", + md: "mandrelDiameter", + mandreldiameter: "mandrelDiameter", + ml: "mandrelLength", + mandrellength: "mandrelLength", + }, + ) +} diff --git a/src/models/blindrivet/register.ts b/src/models/blindrivet/register.ts new file mode 100644 index 0000000..38c7a3d --- /dev/null +++ b/src/models/blindrivet/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { blindRivetModelDefinitionSchema } from "./schema" +import { parseBlindRivetModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "blindrivet", + schema: blindRivetModelDefinitionSchema, + parse: parseBlindRivetModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/blindrivet/schema.ts b/src/models/blindrivet/schema.ts new file mode 100644 index 0000000..9bdbffd --- /dev/null +++ b/src/models/blindrivet/schema.ts @@ -0,0 +1,72 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + bodyDiameter: positiveLength.default(4), + bodyLength: positiveLength.default(10), + flangeDiameter: positiveLength.default(8), + flangeThickness: positiveLength.default(1.2), + mandrelDiameter: positiveLength.default(2), + mandrelLength: positiveLength.default(20), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![ + (p.bodyDiameter - p.mandrelDiameter * 1.1) / 2, + (p.flangeDiameter - p.bodyDiameter) / 2, + ].every((gap) => gap >= extent * 0.0001) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if ( + !( + p.mandrelDiameter < p.bodyDiameter && + p.bodyDiameter < p.flangeDiameter && + p.mandrelLength > p.bodyLength && + p.flangeThickness < p.bodyLength + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Invalid blindrivet dimensions: mating features must fit within the body", + }) +} +export const blindRivetModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const blindRivetModelDefinitionSchema = z + .object({ fn: z.literal("blindrivet"), ...shape }) + .strict() + .superRefine(validate) +export type BlindRivetModelPropsInput = z.input< + typeof blindRivetModelPropsSchema +> +export type BlindRivetModelProps = z.output +export type BlindRivetModelDefinition = z.output< + typeof blindRivetModelDefinitionSchema +> +export function getBlindRivetDimensions(input: BlindRivetModelPropsInput = {}) { + return blindRivetModelPropsSchema.parse(input) +} diff --git a/src/models/bridgehandle/index.ts b/src/models/bridgehandle/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/bridgehandle/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/bridgehandle/parse-model-string.ts b/src/models/bridgehandle/parse-model-string.ts new file mode 100644 index 0000000..454645a --- /dev/null +++ b/src/models/bridgehandle/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { bridgeHandleModelDefinitionSchema } from "./schema" +const aliases = { + length: "length", + gripdiameter: "gripDiameter", + height: "height", + mountingdiameter: "mountingDiameter", + mountingheight: "mountingHeight", + holediameter: "holeDiameter", + h: "height", + l: "length", +} as const +export function parseBridgeHandleModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "bridgehandle", + bridgeHandleModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/bridgehandle/register.ts b/src/models/bridgehandle/register.ts new file mode 100644 index 0000000..3e24098 --- /dev/null +++ b/src/models/bridgehandle/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { bridgeHandleModelDefinitionSchema } from "./schema" +import { parseBridgeHandleModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "bridgehandle", + schema: bridgeHandleModelDefinitionSchema, + parse: parseBridgeHandleModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/bridgehandle/schema.ts b/src/models/bridgehandle/schema.ts new file mode 100644 index 0000000..d3aead1 --- /dev/null +++ b/src/models/bridgehandle/schema.ts @@ -0,0 +1,69 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + length: positiveLength.default(120), + gripDiameter: positiveLength.default(15), + height: positiveLength.default(40), + mountingDiameter: positiveLength.default(24), + mountingHeight: positiveLength.default(5), + holeDiameter: positiveLength.default(5), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.length > 3 * p.mountingDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid bridgehandle dimensions: length > 3*mountingDiameter", + }) + if (!(p.mountingDiameter > p.gripDiameter)) + context.addIssue({ + code: "custom", + message: + "Invalid bridgehandle dimensions: mountingDiameter > gripDiameter", + }) + if (!(p.height > p.gripDiameter + p.mountingHeight)) + context.addIssue({ + code: "custom", + message: + "Invalid bridgehandle dimensions: height > gripDiameter+mountingHeight", + }) + if (!(p.holeDiameter < p.gripDiameter / 2)) + context.addIssue({ + code: "custom", + message: "Invalid bridgehandle dimensions: holeDiameter < gripDiameter/2", + }) + if (!(p.mountingHeight < p.height / 3)) + context.addIssue({ + code: "custom", + message: "Invalid bridgehandle dimensions: mountingHeight < height/3", + }) +} +export const bridgeHandleModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const bridgeHandleModelDefinitionSchema = z + .object({ fn: z.literal("bridgehandle"), ...shape }) + .strict() + .superRefine(validate) +export type BridgeHandleModelPropsInput = z.input< + typeof bridgeHandleModelPropsSchema +> +export type BridgeHandleModelProps = z.output< + typeof bridgeHandleModelPropsSchema +> +export type BridgeHandleModelDefinition = z.output< + typeof bridgeHandleModelDefinitionSchema +> +/** Bridge handle along X with two mounting feet and axial mounting holes; bottom Z=0. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getBridgeHandleDimensions( + input: BridgeHandleModelPropsInput = {}, +) { + return bridgeHandleModelPropsSchema.parse(input) +} diff --git a/src/models/bulkheadfitting/index.ts b/src/models/bulkheadfitting/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/bulkheadfitting/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/bulkheadfitting/parse-model-string.ts b/src/models/bulkheadfitting/parse-model-string.ts new file mode 100644 index 0000000..2679c99 --- /dev/null +++ b/src/models/bulkheadfitting/parse-model-string.ts @@ -0,0 +1,28 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { bulkheadFittingModelDefinitionSchema } from "./schema" +export function parseBulkheadFittingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "bulkheadfitting", + bulkheadFittingModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + l: "length", + flange: "flangeDiameter", + flanget: "flangeThickness", + af: "nutAcrossFlats", + nutt: "nutThickness", + gap: "panelGap", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + length: "length", + flangediameter: "flangeDiameter", + flangethickness: "flangeThickness", + nutacrossflats: "nutAcrossFlats", + nutthickness: "nutThickness", + panelgap: "panelGap", + }, + ) +} diff --git a/src/models/bulkheadfitting/register.ts b/src/models/bulkheadfitting/register.ts new file mode 100644 index 0000000..be94002 --- /dev/null +++ b/src/models/bulkheadfitting/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parseBulkheadFittingModelParams } from "./parse-model-string" +import { bulkheadFittingModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "bulkheadfitting", + schema: bulkheadFittingModelDefinitionSchema, + parse: parseBulkheadFittingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/bulkheadfitting/schema.ts b/src/models/bulkheadfitting/schema.ts new file mode 100644 index 0000000..2e3457d --- /dev/null +++ b/src/models/bulkheadfitting/schema.ts @@ -0,0 +1,79 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(20), + boreDiameter: positiveLength.default(12), + length: positiveLength.default(55), + flangeDiameter: positiveLength.default(32), + flangeThickness: positiveLength.default(4), + nutAcrossFlats: positiveLength.default(28), + nutThickness: positiveLength.default(8), + panelGap: positiveLength.default(10), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.outerDiameter)) + issue("Requires boreDiameter < outerDiameter") + if (!(p.outerDiameter < p.nutAcrossFlats)) + issue("Requires outerDiameter < nutAcrossFlats") + if (!(p.nutAcrossFlats < p.flangeDiameter)) + issue("Requires nutAcrossFlats < flangeDiameter") + if (!(p.panelGap + p.flangeThickness + p.nutThickness < p.length)) + issue("Requires panelGap + flangeThickness + nutThickness < length") + if ( + p.panelGap + p.flangeThickness * 2 >= p.length || + p.panelGap + p.nutThickness * 2 >= p.length + ) + issue("Flange and nut must remain on the axial body") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.outerDiameter - p.boreDiameter, + p.nutAcrossFlats - p.outerDiameter, + p.flangeDiameter - p.nutAcrossFlats, + p.length - (p.panelGap + p.flangeThickness + p.nutThickness), + p.length - p.panelGap - p.flangeThickness * 2, + p.length - p.panelGap - p.nutThickness * 2, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const bulkheadFittingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const bulkheadFittingModelDefinitionSchema = z + .object({ fn: z.literal("bulkheadfitting"), ...shape }) + .strict() + .superRefine(validate) +export type BulkheadFittingModelPropsInput = z.input< + typeof bulkheadFittingModelPropsSchema +> +export type BulkheadFittingModelProps = z.output< + typeof bulkheadFittingModelPropsSchema +> +export type BulkheadFittingModelDefinition = z.output< + typeof bulkheadFittingModelDefinitionSchema +> +export function getBulkheadFittingDimensions( + input: BulkheadFittingModelPropsInput = {}, +) { + return bulkheadFittingModelPropsSchema.parse(input) +} diff --git a/src/models/camfollower/index.ts b/src/models/camfollower/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/camfollower/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/camfollower/parse-model-string.ts b/src/models/camfollower/parse-model-string.ts new file mode 100644 index 0000000..3a56eba --- /dev/null +++ b/src/models/camfollower/parse-model-string.ts @@ -0,0 +1,25 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { camFollowerModelDefinitionSchema } from "./schema" +const aliases = { + od: "wheelDiameter", + w: "wheelWidth", + stud: "studDiameter", + sl: "studLength", + socket: "socketDiameter", + rollers: "rollerCount", + wheeldiameter: "wheelDiameter", + wheelwidth: "wheelWidth", + studdiameter: "studDiameter", + studlength: "studLength", + socketdiameter: "socketDiameter", + rollercount: "rollerCount", +} as const +export function parseCamFollowerModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "camfollower", + camFollowerModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/camfollower/register.ts b/src/models/camfollower/register.ts new file mode 100644 index 0000000..8c431d3 --- /dev/null +++ b/src/models/camfollower/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { camFollowerModelDefinitionSchema } from "./schema" +import { parseCamFollowerModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "camfollower", + schema: camFollowerModelDefinitionSchema, + parse: parseCamFollowerModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/camfollower/schema.ts b/src/models/camfollower/schema.ts new file mode 100644 index 0000000..2f84e9a --- /dev/null +++ b/src/models/camfollower/schema.ts @@ -0,0 +1,81 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + wheelDiameter: positiveLength.default(32), + wheelWidth: positiveLength.default(14), + studDiameter: positiveLength.default(10), + studLength: positiveLength.default(26), + socketDiameter: positiveLength.default(5), + rollerCount: z.number().int().min(2).max(64).default(10), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.wheelDiameter <= p.studDiameter * 2.2) + context.addIssue({ + code: "custom", + message: "The wheel must leave radial space for races and rollers", + }) + if (p.wheelWidth <= p.socketDiameter) + context.addIssue({ + code: "custom", + message: "Wheel width must exceed the drive-socket diameter", + }) + if (p.socketDiameter >= p.studDiameter) + context.addIssue({ + code: "custom", + message: "Drive socket must remain smaller than the stud", + }) + if (p.studLength <= p.studDiameter) + context.addIssue({ + code: "custom", + message: "Stud length must exceed the stud diameter", + }) + if ( + (p.wheelDiameter * 0.82 - p.studDiameter * 1.45) * 0.4 >= + 2 * + ((p.wheelDiameter * 0.82 + p.studDiameter * 1.45) / 4) * + Math.sin(Math.PI / p.rollerCount) + ) + context.addIssue({ + code: "custom", + message: + "Adjacent cam-follower rollers must have positive circumferential clearance", + }) +} +export const camFollowerModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const camFollowerModelDefinitionSchema = z + .object({ fn: z.literal("camfollower"), ...shape }) + .strict() + .superRefine(validate) +export type CamFollowerModelPropsInput = z.input< + typeof camFollowerModelPropsSchema +> +export type CamFollowerModelProps = z.output +export type CamFollowerModelDefinition = z.output< + typeof camFollowerModelDefinitionSchema +> +/** Stud-mounted cam follower with an exposed open roller race and hex drive socket. Stud is on Z, from Z=0 to studLength; wheel sits above it. Smooth stud represents the thread major-diameter envelope, not a screw thread. All lengths are normalized to millimeters. */ +export function getCamFollowerDimensions( + input: CamFollowerModelPropsInput = {}, +) { + const p = camFollowerModelPropsSchema.parse(input) + return { + ...p, + size: [p.wheelDiameter, p.wheelDiameter, p.studLength + p.wheelWidth] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.studLength + p.wheelWidth, + } +} diff --git a/src/models/capnut/index.ts b/src/models/capnut/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/capnut/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/capnut/parse-model-string.ts b/src/models/capnut/parse-model-string.ts new file mode 100644 index 0000000..085385d --- /dev/null +++ b/src/models/capnut/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { capNutModelDefinitionSchema } from "./schema" +export function parseCapNutModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "capnut", + capNutModelDefinitionSchema, + { + id: "boreDiameter", + borediameter: "boreDiameter", + af: "acrossFlats", + acrossflats: "acrossFlats", + h: "bodyHeight", + bodyheight: "bodyHeight", + ch: "capHeight", + capheight: "capHeight", + wt: "wallThickness", + wallthickness: "wallThickness", + }, + ) +} diff --git a/src/models/capnut/register.ts b/src/models/capnut/register.ts new file mode 100644 index 0000000..75dea59 --- /dev/null +++ b/src/models/capnut/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { capNutModelDefinitionSchema } from "./schema" +import { parseCapNutModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "capnut", + schema: capNutModelDefinitionSchema, + parse: parseCapNutModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/capnut/schema.ts b/src/models/capnut/schema.ts new file mode 100644 index 0000000..3a613d4 --- /dev/null +++ b/src/models/capnut/schema.ts @@ -0,0 +1,67 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + boreDiameter: positiveLength.default(6), + acrossFlats: positiveLength.default(10), + bodyHeight: positiveLength.default(5), + capHeight: positiveLength.default(5), + wallThickness: positiveLength.default(1), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![ + (p.acrossFlats - p.boreDiameter) / 2 - p.wallThickness, + p.bodyHeight - p.wallThickness, + p.capHeight - p.wallThickness, + ].every((gap) => gap >= extent * 0.0001) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if ( + !( + p.boreDiameter + 2 * p.wallThickness < p.acrossFlats && + p.wallThickness < p.bodyHeight && + p.wallThickness < p.capHeight + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Invalid capnut dimensions: mating features must fit within the body", + }) +} +export const capNutModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const capNutModelDefinitionSchema = z + .object({ fn: z.literal("capnut"), ...shape }) + .strict() + .superRefine(validate) +export type CapNutModelPropsInput = z.input +export type CapNutModelProps = z.output +export type CapNutModelDefinition = z.output +export function getCapNutDimensions(input: CapNutModelPropsInput = {}) { + return capNutModelPropsSchema.parse(input) +} diff --git a/src/models/carriagebolt/index.ts b/src/models/carriagebolt/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/carriagebolt/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/carriagebolt/parse-model-string.ts b/src/models/carriagebolt/parse-model-string.ts new file mode 100644 index 0000000..8e3b3e7 --- /dev/null +++ b/src/models/carriagebolt/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { carriageBoltModelDefinitionSchema } from "./schema" +export function parseCarriageBoltModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "carriagebolt", + carriageBoltModelDefinitionSchema, + { + d: "shaftDiameter", + shaftdiameter: "shaftDiameter", + l: "shaftLength", + shaftlength: "shaftLength", + nw: "neckWidth", + neckwidth: "neckWidth", + nh: "neckHeight", + neckheight: "neckHeight", + hd: "headDiameter", + headdiameter: "headDiameter", + hh: "headHeight", + headheight: "headHeight", + }, + ) +} diff --git a/src/models/carriagebolt/register.ts b/src/models/carriagebolt/register.ts new file mode 100644 index 0000000..0366ad2 --- /dev/null +++ b/src/models/carriagebolt/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { carriageBoltModelDefinitionSchema } from "./schema" +import { parseCarriageBoltModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "carriagebolt", + schema: carriageBoltModelDefinitionSchema, + parse: parseCarriageBoltModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/carriagebolt/schema.ts b/src/models/carriagebolt/schema.ts new file mode 100644 index 0000000..cff5c09 --- /dev/null +++ b/src/models/carriagebolt/schema.ts @@ -0,0 +1,74 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + shaftDiameter: positiveLength.default(6), + shaftLength: positiveLength.default(25), + neckWidth: positiveLength.default(6.5), + neckHeight: positiveLength.default(3), + headDiameter: positiveLength.default(14), + headHeight: positiveLength.default(4), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![(p.headDiameter - p.neckWidth * Math.SQRT2) / 2].every( + (gap) => gap >= extent * 0.0001, + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if ( + !( + p.shaftDiameter <= p.neckWidth && + p.neckWidth * Math.SQRT2 < p.headDiameter && + p.headHeight < p.headDiameter / 2 + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Invalid carriagebolt dimensions: mating features must fit within the body", + }) +} +export const carriageBoltModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const carriageBoltModelDefinitionSchema = z + .object({ fn: z.literal("carriagebolt"), ...shape }) + .strict() + .superRefine(validate) +export type CarriageBoltModelPropsInput = z.input< + typeof carriageBoltModelPropsSchema +> +export type CarriageBoltModelProps = z.output< + typeof carriageBoltModelPropsSchema +> +export type CarriageBoltModelDefinition = z.output< + typeof carriageBoltModelDefinitionSchema +> +export function getCarriageBoltDimensions( + input: CarriageBoltModelPropsInput = {}, +) { + return carriageBoltModelPropsSchema.parse(input) +} diff --git a/src/models/caster/index.ts b/src/models/caster/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/caster/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/caster/parse-model-string.ts b/src/models/caster/parse-model-string.ts new file mode 100644 index 0000000..2398cc3 --- /dev/null +++ b/src/models/caster/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { casterModelDefinitionSchema } from "./schema" +const aliases = { + wheeldiameter: "wheelDiameter", + wheelwidth: "wheelWidth", + axlediameter: "axleDiameter", + forkthickness: "forkThickness", + platewidth: "plateWidth", + platelength: "plateLength", + platethickness: "plateThickness", + overallheight: "overallHeight", + holediameter: "holeDiameter", +} as const +export function parseCasterModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "caster", + casterModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/caster/register.ts b/src/models/caster/register.ts new file mode 100644 index 0000000..20e7257 --- /dev/null +++ b/src/models/caster/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { casterModelDefinitionSchema } from "./schema" +import { parseCasterModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "caster", + schema: casterModelDefinitionSchema, + parse: parseCasterModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/caster/schema.ts b/src/models/caster/schema.ts new file mode 100644 index 0000000..856901e --- /dev/null +++ b/src/models/caster/schema.ts @@ -0,0 +1,75 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + wheelDiameter: positiveLength.default(75), + wheelWidth: positiveLength.default(24), + axleDiameter: positiveLength.default(10), + forkThickness: positiveLength.default(5), + plateWidth: positiveLength.default(50), + plateLength: positiveLength.default(65), + plateThickness: positiveLength.default(5), + overallHeight: positiveLength.default(110), + holeDiameter: positiveLength.default(5), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.axleDiameter < p.wheelDiameter / 3)) + context.addIssue({ + code: "custom", + message: "Invalid caster dimensions: axleDiameter < wheelDiameter/3", + }) + if (!(p.wheelWidth < p.wheelDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid caster dimensions: wheelWidth < wheelDiameter", + }) + if (!(p.forkThickness < p.wheelWidth / 2)) + context.addIssue({ + code: "custom", + message: "Invalid caster dimensions: forkThickness < wheelWidth/2", + }) + if (!(p.plateWidth > p.wheelWidth + 2 * p.forkThickness + 2)) + context.addIssue({ + code: "custom", + message: + "Invalid caster dimensions: plateWidth > wheelWidth+2*forkThickness+2", + }) + if (!(p.plateLength > p.wheelDiameter / 2)) + context.addIssue({ + code: "custom", + message: "Invalid caster dimensions: plateLength > wheelDiameter/2", + }) + if (!(p.overallHeight > p.wheelDiameter + p.plateThickness)) + context.addIssue({ + code: "custom", + message: + "Invalid caster dimensions: overallHeight > wheelDiameter+plateThickness", + }) + if (!(p.holeDiameter < Math.min(p.plateWidth, p.plateLength) / 8)) + context.addIssue({ + code: "custom", + message: + "Invalid caster dimensions: holeDiameter < Math.min(plateWidth,plateLength)/8", + }) +} +export const casterModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const casterModelDefinitionSchema = z + .object({ fn: z.literal("caster"), ...shape }) + .strict() + .superRefine(validate) +export type CasterModelPropsInput = z.input +export type CasterModelProps = z.output +export type CasterModelDefinition = z.output +/** Nominal fixed caster assembly: rolling axis Y, plate in XY, wheel contact Z=0. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getCasterDimensions(input: CasterModelPropsInput = {}) { + return casterModelPropsSchema.parse(input) +} diff --git a/src/models/checkvalve/index.ts b/src/models/checkvalve/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/checkvalve/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/checkvalve/parse-model-string.ts b/src/models/checkvalve/parse-model-string.ts new file mode 100644 index 0000000..e41e904 --- /dev/null +++ b/src/models/checkvalve/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { checkValveModelDefinitionSchema } from "./schema" +export function parseCheckValveModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "checkvalve", + checkValveModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + l: "length", + endod: "endDiameter", + af: "hexAcrossFlats", + nutl: "nutLength", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + length: "length", + enddiameter: "endDiameter", + hexacrossflats: "hexAcrossFlats", + nutlength: "nutLength", + }, + ) +} diff --git a/src/models/checkvalve/register.ts b/src/models/checkvalve/register.ts new file mode 100644 index 0000000..1737e7b --- /dev/null +++ b/src/models/checkvalve/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parseCheckValveModelParams } from "./parse-model-string" +import { checkValveModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "checkvalve", + schema: checkValveModelDefinitionSchema, + parse: parseCheckValveModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/checkvalve/schema.ts b/src/models/checkvalve/schema.ts new file mode 100644 index 0000000..43b70b9 --- /dev/null +++ b/src/models/checkvalve/schema.ts @@ -0,0 +1,67 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(28), + boreDiameter: positiveLength.default(12), + length: positiveLength.default(60), + endDiameter: positiveLength.default(20), + hexAcrossFlats: positiveLength.default(30), + nutLength: positiveLength.default(12), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.endDiameter)) + issue("Requires boreDiameter < endDiameter") + if (!(p.endDiameter < p.outerDiameter)) + issue("Requires endDiameter < outerDiameter") + if (!(p.outerDiameter < p.hexAcrossFlats)) + issue("Requires outerDiameter < hexAcrossFlats") + if (!(p.nutLength * 2 < p.length)) issue("Requires nutLength * 2 < length") + if (!(p.outerDiameter < p.length)) issue("Requires outerDiameter < length") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.endDiameter - p.boreDiameter, + p.outerDiameter - p.endDiameter, + p.hexAcrossFlats - p.outerDiameter, + p.length - p.nutLength * 2, + p.length - p.outerDiameter, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const checkValveModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const checkValveModelDefinitionSchema = z + .object({ fn: z.literal("checkvalve"), ...shape }) + .strict() + .superRefine(validate) +export type CheckValveModelPropsInput = z.input< + typeof checkValveModelPropsSchema +> +export type CheckValveModelProps = z.output +export type CheckValveModelDefinition = z.output< + typeof checkValveModelDefinitionSchema +> +export function getCheckValveDimensions(input: CheckValveModelPropsInput = {}) { + return checkValveModelPropsSchema.parse(input) +} diff --git a/src/models/clevis/index.ts b/src/models/clevis/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/clevis/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/clevis/parse-model-string.ts b/src/models/clevis/parse-model-string.ts new file mode 100644 index 0000000..1939285 --- /dev/null +++ b/src/models/clevis/parse-model-string.ts @@ -0,0 +1,26 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { clevisModelDefinitionSchema } from "./schema" +const aliases = { + w: "width", + l: "length", + h: "height", + gap: "gap", + back: "backThickness", + pin: "pinDiameter", + mount: "mountDiameter", + width: "width", + length: "length", + height: "height", + backthickness: "backThickness", + pindiameter: "pinDiameter", + mountdiameter: "mountDiameter", +} as const +export function parseClevisModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "clevis", + clevisModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/clevis/register.ts b/src/models/clevis/register.ts new file mode 100644 index 0000000..39130bb --- /dev/null +++ b/src/models/clevis/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { clevisModelDefinitionSchema } from "./schema" +import { parseClevisModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "clevis", + schema: clevisModelDefinitionSchema, + parse: parseClevisModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/clevis/schema.ts b/src/models/clevis/schema.ts new file mode 100644 index 0000000..1533036 --- /dev/null +++ b/src/models/clevis/schema.ts @@ -0,0 +1,62 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + width: positiveLength.default(30), + length: positiveLength.default(45), + height: positiveLength.default(20), + gap: positiveLength.default(14), + backThickness: positiveLength.default(10), + pinDiameter: positiveLength.default(8), + mountDiameter: positiveLength.default(10), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.gap >= p.width) + context.addIssue({ + code: "custom", + message: "Ear gap must leave two positive-width arms", + }) + if (p.pinDiameter >= p.height) + context.addIssue({ + code: "custom", + message: "Cross-pin hole must fit within each ear height", + }) + if (p.mountDiameter >= Math.min(p.width, p.height)) + context.addIssue({ + code: "custom", + message: "Rear mounting bore must remain inside the rear wall", + }) + if (p.length <= p.backThickness + p.pinDiameter * 2) + context.addIssue({ + code: "custom", + message: + "Fork length must leave a rear wall and complete pin-hole edge margins", + }) +} +export const clevisModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const clevisModelDefinitionSchema = z + .object({ fn: z.literal("clevis"), ...shape }) + .strict() + .superRefine(validate) +export type ClevisModelPropsInput = z.input +export type ClevisModelProps = z.output +export type ClevisModelDefinition = z.output +/** Fork clevis with parallel ears, shared cross-pin bore along X and rear mounting bore along Y. Rear plane Y=0, underside Z=0. Bores are smooth; pin/thread geometry and strength are not implied. All lengths are normalized to millimeters. */ +export function getClevisDimensions(input: ClevisModelPropsInput = {}) { + const p = clevisModelPropsSchema.parse(input) + return { + ...p, + size: [p.width, p.length, p.height] as [number, number, number], + bottomZ: 0, + topZ: p.height, + } +} diff --git a/src/models/clevispin/index.ts b/src/models/clevispin/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/clevispin/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/clevispin/parse-model-string.ts b/src/models/clevispin/parse-model-string.ts new file mode 100644 index 0000000..4229378 --- /dev/null +++ b/src/models/clevispin/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { clevisPinModelDefinitionSchema } from "./schema" +export function parseClevisPinModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "clevispin", + clevisPinModelDefinitionSchema, + { + d: "shaftDiameter", + shaftdiameter: "shaftDiameter", + l: "shaftLength", + shaftlength: "shaftLength", + hd: "headDiameter", + headdiameter: "headDiameter", + hh: "headHeight", + headheight: "headHeight", + hol: "holeDiameter", + holediameter: "holeDiameter", + ho: "holeOffset", + holeoffset: "holeOffset", + }, + ) +} diff --git a/src/models/clevispin/register.ts b/src/models/clevispin/register.ts new file mode 100644 index 0000000..80f5563 --- /dev/null +++ b/src/models/clevispin/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { clevisPinModelDefinitionSchema } from "./schema" +import { parseClevisPinModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "clevispin", + schema: clevisPinModelDefinitionSchema, + parse: parseClevisPinModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/clevispin/schema.ts b/src/models/clevispin/schema.ts new file mode 100644 index 0000000..c7d633c --- /dev/null +++ b/src/models/clevispin/schema.ts @@ -0,0 +1,72 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + shaftDiameter: positiveLength.default(6), + shaftLength: positiveLength.default(25), + headDiameter: positiveLength.default(10), + headHeight: positiveLength.default(2), + holeDiameter: positiveLength.default(2), + holeOffset: positiveLength.default(3), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![ + (p.shaftDiameter - p.holeDiameter) / 2, + p.holeOffset - p.holeDiameter / 2, + p.shaftLength - p.holeOffset - p.holeDiameter / 2, + (p.headDiameter - p.shaftDiameter) / 2, + ].every((gap) => gap >= extent * 0.0001) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if ( + !( + p.shaftDiameter < p.headDiameter && + p.holeDiameter < p.shaftDiameter && + p.holeOffset > p.holeDiameter / 2 && + p.holeOffset + p.holeDiameter / 2 < p.shaftLength + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Invalid clevispin dimensions: mating features must fit within the body", + }) +} +export const clevisPinModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const clevisPinModelDefinitionSchema = z + .object({ fn: z.literal("clevispin"), ...shape }) + .strict() + .superRefine(validate) +export type ClevisPinModelPropsInput = z.input +export type ClevisPinModelProps = z.output +export type ClevisPinModelDefinition = z.output< + typeof clevisPinModelDefinitionSchema +> +export function getClevisPinDimensions(input: ClevisPinModelPropsInput = {}) { + return clevisPinModelPropsSchema.parse(input) +} diff --git a/src/models/compressionfitting/index.ts b/src/models/compressionfitting/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/compressionfitting/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/compressionfitting/parse-model-string.ts b/src/models/compressionfitting/parse-model-string.ts new file mode 100644 index 0000000..5d37590 --- /dev/null +++ b/src/models/compressionfitting/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { compressionFittingModelDefinitionSchema } from "./schema" +export function parseCompressionFittingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "compressionfitting", + compressionFittingModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + l: "length", + tube: "tubeDiameter", + af: "nutAcrossFlats", + nutl: "nutLength", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + length: "length", + tubediameter: "tubeDiameter", + nutacrossflats: "nutAcrossFlats", + nutlength: "nutLength", + }, + ) +} diff --git a/src/models/compressionfitting/register.ts b/src/models/compressionfitting/register.ts new file mode 100644 index 0000000..49469b6 --- /dev/null +++ b/src/models/compressionfitting/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parseCompressionFittingModelParams } from "./parse-model-string" +import { compressionFittingModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "compressionfitting", + schema: compressionFittingModelDefinitionSchema, + parse: parseCompressionFittingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/compressionfitting/schema.ts b/src/models/compressionfitting/schema.ts new file mode 100644 index 0000000..3db705d --- /dev/null +++ b/src/models/compressionfitting/schema.ts @@ -0,0 +1,69 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(22), + boreDiameter: positiveLength.default(8), + length: positiveLength.default(40), + tubeDiameter: positiveLength.default(12), + nutAcrossFlats: positiveLength.default(26), + nutLength: positiveLength.default(10), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.tubeDiameter)) + issue("Requires boreDiameter < tubeDiameter") + if (!(p.tubeDiameter < p.outerDiameter)) + issue("Requires tubeDiameter < outerDiameter") + if (!(p.outerDiameter < p.nutAcrossFlats)) + issue("Requires outerDiameter < nutAcrossFlats") + if (!(p.nutLength * 2 < p.length)) issue("Requires nutLength * 2 < length") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.tubeDiameter - p.boreDiameter, + p.outerDiameter - p.tubeDiameter, + p.nutAcrossFlats - p.outerDiameter, + p.length - p.nutLength * 2, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const compressionFittingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const compressionFittingModelDefinitionSchema = z + .object({ fn: z.literal("compressionfitting"), ...shape }) + .strict() + .superRefine(validate) +export type CompressionFittingModelPropsInput = z.input< + typeof compressionFittingModelPropsSchema +> +export type CompressionFittingModelProps = z.output< + typeof compressionFittingModelPropsSchema +> +export type CompressionFittingModelDefinition = z.output< + typeof compressionFittingModelDefinitionSchema +> +export function getCompressionFittingDimensions( + input: CompressionFittingModelPropsInput = {}, +) { + return compressionFittingModelPropsSchema.parse(input) +} diff --git a/src/models/conveyorroller/index.ts b/src/models/conveyorroller/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/conveyorroller/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/conveyorroller/parse-model-string.ts b/src/models/conveyorroller/parse-model-string.ts new file mode 100644 index 0000000..1aa9e34 --- /dev/null +++ b/src/models/conveyorroller/parse-model-string.ts @@ -0,0 +1,25 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { conveyorRollerModelDefinitionSchema } from "./schema" +const aliases = { + od: "outerDiameter", + l: "length", + wall: "wallThickness", + shaft: "shaftDiameter", + ext: "shaftExtension", + end: "endThickness", + outerdiameter: "outerDiameter", + length: "length", + wallthickness: "wallThickness", + shaftdiameter: "shaftDiameter", + shaftextension: "shaftExtension", + endthickness: "endThickness", +} as const +export function parseConveyorRollerModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "conveyorroller", + conveyorRollerModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/conveyorroller/register.ts b/src/models/conveyorroller/register.ts new file mode 100644 index 0000000..eb4e9c7 --- /dev/null +++ b/src/models/conveyorroller/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { conveyorRollerModelDefinitionSchema } from "./schema" +import { parseConveyorRollerModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "conveyorroller", + schema: conveyorRollerModelDefinitionSchema, + parse: parseConveyorRollerModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/conveyorroller/schema.ts b/src/models/conveyorroller/schema.ts new file mode 100644 index 0000000..dbcaf11 --- /dev/null +++ b/src/models/conveyorroller/schema.ts @@ -0,0 +1,67 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(50), + length: positiveLength.default(150), + wallThickness: positiveLength.default(3), + shaftDiameter: positiveLength.default(10), + shaftExtension: positiveLength.default(15), + endThickness: positiveLength.default(4), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.outerDiameter <= p.shaftDiameter + 2 * p.wallThickness) + context.addIssue({ + code: "custom", + message: "Tube wall must leave radial clearance around the axle", + }) + if (p.length <= 2 * p.endThickness) + context.addIssue({ + code: "custom", + message: "End plugs must leave a positive-length hollow middle", + }) + if (p.outerDiameter / 2 - p.wallThickness <= p.shaftDiameter / 2) + context.addIssue({ + code: "custom", + message: "Wall thickness must leave radial clearance around the axle", + }) +} +export const conveyorRollerModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const conveyorRollerModelDefinitionSchema = z + .object({ fn: z.literal("conveyorroller"), ...shape }) + .strict() + .superRefine(validate) +export type ConveyorRollerModelPropsInput = z.input< + typeof conveyorRollerModelPropsSchema +> +export type ConveyorRollerModelProps = z.output< + typeof conveyorRollerModelPropsSchema +> +export type ConveyorRollerModelDefinition = z.output< + typeof conveyorRollerModelDefinitionSchema +> +/** Hollow conveyor tube with end plugs and a continuous axle projecting beyond each end. Axis Z, tube spans Z=0..length and axle starts at negative shaftExtension. End plugs are nominal; internal bearings and shaft flats are not specified. All lengths are normalized to millimeters. */ +export function getConveyorRollerDimensions( + input: ConveyorRollerModelPropsInput = {}, +) { + const p = conveyorRollerModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.outerDiameter, + p.outerDiameter, + p.length + 2 * p.shaftExtension, + ] as [number, number, number], + bottomZ: -p.shaftExtension, + topZ: p.length + p.shaftExtension, + } +} diff --git a/src/models/coolingfan/index.ts b/src/models/coolingfan/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/coolingfan/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/coolingfan/parse-model-string.ts b/src/models/coolingfan/parse-model-string.ts new file mode 100644 index 0000000..1bc4f39 --- /dev/null +++ b/src/models/coolingfan/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { coolingFanModelDefinitionSchema } from "./schema" +const aliases = { + size: "size", + thickness: "thickness", + hubdiameter: "hubDiameter", + bladecount: "bladeCount", + bladewidth: "bladeWidth", + bladethickness: "bladeThickness", + mountingholediameter: "mountingHoleDiameter", + framethickness: "frameThickness", + t: "thickness", +} as const +export function parseCoolingFanModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "coolingfan", + coolingFanModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/coolingfan/register.ts b/src/models/coolingfan/register.ts new file mode 100644 index 0000000..9fa3214 --- /dev/null +++ b/src/models/coolingfan/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { coolingFanModelDefinitionSchema } from "./schema" +import { parseCoolingFanModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "coolingfan", + schema: coolingFanModelDefinitionSchema, + parse: parseCoolingFanModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/coolingfan/schema.ts b/src/models/coolingfan/schema.ts new file mode 100644 index 0000000..b08e7b9 --- /dev/null +++ b/src/models/coolingfan/schema.ts @@ -0,0 +1,72 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + size: positiveLength.default(80), + thickness: positiveLength.default(25), + hubDiameter: positiveLength.default(28), + bladeCount: z.number().int().min(3).max(12).default(7), + bladeWidth: positiveLength.default(12), + bladeThickness: positiveLength.default(3), + mountingHoleDiameter: positiveLength.default(4), + frameThickness: positiveLength.default(6), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.frameThickness < p.size / 8)) + context.addIssue({ + code: "custom", + message: "Invalid coolingfan dimensions: frameThickness < size/8", + }) + if (!(p.mountingHoleDiameter < p.frameThickness * 0.8)) + context.addIssue({ + code: "custom", + message: + "Invalid coolingfan dimensions: mountingHoleDiameter < frameThickness*.8", + }) + if (!(p.hubDiameter < p.size - 4 * p.frameThickness)) + context.addIssue({ + code: "custom", + message: + "Invalid coolingfan dimensions: hubDiameter < size-4*frameThickness", + }) + if (!(p.bladeWidth < p.size / 4)) + context.addIssue({ + code: "custom", + message: "Invalid coolingfan dimensions: bladeWidth < size/4", + }) + if (!(p.bladeThickness < p.thickness / 3)) + context.addIssue({ + code: "custom", + message: "Invalid coolingfan dimensions: bladeThickness < thickness/3", + }) + if (!(p.thickness < p.size)) + context.addIssue({ + code: "custom", + message: "Invalid coolingfan dimensions: thickness < size", + }) +} +export const coolingFanModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const coolingFanModelDefinitionSchema = z + .object({ fn: z.literal("coolingfan"), ...shape }) + .strict() + .superRefine(validate) +export type CoolingFanModelPropsInput = z.input< + typeof coolingFanModelPropsSchema +> +export type CoolingFanModelProps = z.output +export type CoolingFanModelDefinition = z.output< + typeof coolingFanModelDefinitionSchema +> +/** Nominal axial cooling fan with square shroud, four mount holes, curved-plan blades and support spokes; airflow axis Z. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getCoolingFanDimensions(input: CoolingFanModelPropsInput = {}) { + return coolingFanModelPropsSchema.parse(input) +} diff --git a/src/models/couplingnut/index.ts b/src/models/couplingnut/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/couplingnut/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/couplingnut/parse-model-string.ts b/src/models/couplingnut/parse-model-string.ts new file mode 100644 index 0000000..8eff278 --- /dev/null +++ b/src/models/couplingnut/parse-model-string.ts @@ -0,0 +1,18 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { couplingNutModelDefinitionSchema } from "./schema" +export function parseCouplingNutModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "couplingnut", + couplingNutModelDefinitionSchema, + { + id: "boreDiameter", + borediameter: "boreDiameter", + af: "acrossFlats", + acrossflats: "acrossFlats", + h: "height", + height: "height", + }, + ) +} diff --git a/src/models/couplingnut/register.ts b/src/models/couplingnut/register.ts new file mode 100644 index 0000000..eaa9928 --- /dev/null +++ b/src/models/couplingnut/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { couplingNutModelDefinitionSchema } from "./schema" +import { parseCouplingNutModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "couplingnut", + schema: couplingNutModelDefinitionSchema, + parse: parseCouplingNutModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/couplingnut/schema.ts b/src/models/couplingnut/schema.ts new file mode 100644 index 0000000..4a9722b --- /dev/null +++ b/src/models/couplingnut/schema.ts @@ -0,0 +1,63 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + boreDiameter: positiveLength.default(6), + acrossFlats: positiveLength.default(10), + height: positiveLength.default(18), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![(p.acrossFlats - p.boreDiameter) / 2].every( + (gap) => gap >= extent * 0.0001, + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if (!(p.boreDiameter < p.acrossFlats && p.height > p.acrossFlats)) + ctx.addIssue({ + code: "custom", + message: + "Invalid couplingnut dimensions: mating features must fit within the body", + }) +} +export const couplingNutModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const couplingNutModelDefinitionSchema = z + .object({ fn: z.literal("couplingnut"), ...shape }) + .strict() + .superRefine(validate) +export type CouplingNutModelPropsInput = z.input< + typeof couplingNutModelPropsSchema +> +export type CouplingNutModelProps = z.output +export type CouplingNutModelDefinition = z.output< + typeof couplingNutModelDefinitionSchema +> +export function getCouplingNutDimensions( + input: CouplingNutModelPropsInput = {}, +) { + return couplingNutModelPropsSchema.parse(input) +} diff --git a/src/models/cylindermount/index.ts b/src/models/cylindermount/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/cylindermount/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/cylindermount/parse-model-string.ts b/src/models/cylindermount/parse-model-string.ts new file mode 100644 index 0000000..00781a9 --- /dev/null +++ b/src/models/cylindermount/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { cylinderMountModelDefinitionSchema } from "./schema" +const aliases = { + width: "width", + depth: "depth", + height: "height", + thickness: "thickness", + borediameter: "boreDiameter", + holediameter: "holeDiameter", + w: "width", + h: "height", + t: "thickness", +} as const +export function parseCylinderMountModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "cylindermount", + cylinderMountModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/cylindermount/register.ts b/src/models/cylindermount/register.ts new file mode 100644 index 0000000..500cddc --- /dev/null +++ b/src/models/cylindermount/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { cylinderMountModelDefinitionSchema } from "./schema" +import { parseCylinderMountModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "cylindermount", + schema: cylinderMountModelDefinitionSchema, + parse: parseCylinderMountModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/cylindermount/schema.ts b/src/models/cylindermount/schema.ts new file mode 100644 index 0000000..e211d7f --- /dev/null +++ b/src/models/cylindermount/schema.ts @@ -0,0 +1,68 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + width: positive.default(50), + depth: positive.default(25), + height: positive.default(45), + thickness: positive.default(5), + boreDiameter: positive.default(20), + holeDiameter: positive.default(5), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (2 * p.thickness >= Math.min(p.width, p.depth, p.height)) + context.addIssue({ + code: "custom", + message: "Incompatible cylindermount dimensions: 1", + }) + if ( + p.boreDiameter + 2 * p.thickness >= Math.min(p.width, p.height) || + p.boreDiameter >= p.height * 0.8 + ) + context.addIssue({ + code: "custom", + message: "Incompatible cylindermount dimensions: 2", + }) + if (p.holeDiameter >= Math.min(p.thickness * 2, p.depth / 2)) + context.addIssue({ + code: "custom", + message: "Incompatible cylindermount dimensions: 3", + }) +} +export const cylinderMountModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const cylinderMountModelDefinitionSchema = z + .object({ fn: z.literal("cylindermount"), ...shape }) + .strict() + .superRefine(validate) +export type CylinderMountModelPropsInput = z.input< + typeof cylinderMountModelPropsSchema +> +export type CylinderMountModelProps = z.output< + typeof cylinderMountModelPropsSchema +> +export type CylinderMountModelDefinition = z.output< + typeof cylinderMountModelDefinitionSchema +> +/** L-shaped rod-end or cylinder mounting bracket with one horizontal clearance bore in the upright and two mounting holes in the foot. Width is X, depth is Y, height is Z; all bores are nominal clearance openings. All normalized lengths are millimeters. */ +export function getCylinderMountDimensions( + input: CylinderMountModelPropsInput = {}, +) { + const p = cylinderMountModelPropsSchema.parse(input) + return { + ...p, + size: [p.width, p.depth, p.height] as [number, number, number], + } +} diff --git a/src/models/cylindricalrollerbearing/index.ts b/src/models/cylindricalrollerbearing/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/cylindricalrollerbearing/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/cylindricalrollerbearing/parse-model-string.ts b/src/models/cylindricalrollerbearing/parse-model-string.ts new file mode 100644 index 0000000..64b017f --- /dev/null +++ b/src/models/cylindricalrollerbearing/parse-model-string.ts @@ -0,0 +1,25 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { cylindricalRollerBearingModelDefinitionSchema } from "./schema" +const aliases = { + id: "innerDiameter", + od: "outerDiameter", + w: "width", + race: "raceThickness", + rollers: "rollerCount", + innerdiameter: "innerDiameter", + outerdiameter: "outerDiameter", + width: "width", + racethickness: "raceThickness", + rollercount: "rollerCount", +} as const +export function parseCylindricalRollerBearingModelParams( + raw: RawModelprinterParams, +) { + return parseIndustrialModelParams( + raw, + "cylindricalrollerbearing", + cylindricalRollerBearingModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/cylindricalrollerbearing/register.ts b/src/models/cylindricalrollerbearing/register.ts new file mode 100644 index 0000000..b67d946 --- /dev/null +++ b/src/models/cylindricalrollerbearing/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { cylindricalRollerBearingModelDefinitionSchema } from "./schema" +import { parseCylindricalRollerBearingModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "cylindricalrollerbearing", + schema: cylindricalRollerBearingModelDefinitionSchema, + parse: parseCylindricalRollerBearingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/cylindricalrollerbearing/schema.ts b/src/models/cylindricalrollerbearing/schema.ts new file mode 100644 index 0000000..9e72805 --- /dev/null +++ b/src/models/cylindricalrollerbearing/schema.ts @@ -0,0 +1,70 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + innerDiameter: positiveLength.default(20), + outerDiameter: positiveLength.default(42), + width: positiveLength.default(16), + raceThickness: positiveLength.default(3), + rollerCount: z.number().int().min(2).max(64).default(12), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.outerDiameter - p.innerDiameter <= 4 * p.raceThickness) + context.addIssue({ + code: "custom", + message: "The races must leave a positive radial roller gap", + }) + if (p.width <= p.raceThickness * 2) + context.addIssue({ + code: "custom", + message: "Width must exceed twice the race thickness", + }) + if ( + ((p.outerDiameter - p.innerDiameter) / 2 - 2 * p.raceThickness) * 0.85 >= + ((p.outerDiameter + p.innerDiameter) / 2) * + Math.sin(Math.PI / p.rollerCount) + ) + context.addIssue({ + code: "custom", + message: "Adjacent rollers must have positive circumferential clearance", + }) +} +export const cylindricalRollerBearingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const cylindricalRollerBearingModelDefinitionSchema = z + .object({ fn: z.literal("cylindricalrollerbearing"), ...shape }) + .strict() + .superRefine(validate) +export type CylindricalRollerBearingModelPropsInput = z.input< + typeof cylindricalRollerBearingModelPropsSchema +> +export type CylindricalRollerBearingModelProps = z.output< + typeof cylindricalRollerBearingModelPropsSchema +> +export type CylindricalRollerBearingModelDefinition = z.output< + typeof cylindricalRollerBearingModelDefinitionSchema +> +/** Open cylindrical-roller radial bearing with short axial rollers between inner and outer races and an outer retaining lip. Axis Z, bottom Z=0. Assembly envelope only; cage/preload and load rating are not specified. All lengths are normalized to millimeters. */ +export function getCylindricalRollerBearingDimensions( + input: CylindricalRollerBearingModelPropsInput = {}, +) { + const p = cylindricalRollerBearingModelPropsSchema.parse(input) + return { + ...p, + size: [p.outerDiameter, p.outerDiameter, p.width] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.width, + } +} diff --git a/src/models/detentpin/index.ts b/src/models/detentpin/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/detentpin/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/detentpin/parse-model-string.ts b/src/models/detentpin/parse-model-string.ts new file mode 100644 index 0000000..b19684a --- /dev/null +++ b/src/models/detentpin/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { detentPinModelDefinitionSchema } from "./schema" +const aliases = { + shaftdiameter: "shaftDiameter", + shaftlength: "shaftLength", + headdiameter: "headDiameter", + headheight: "headHeight", + balldiameter: "ballDiameter", + balloffset: "ballOffset", + ringdiameter: "ringDiameter", + ringwire: "ringWire", +} as const +export function parseDetentPinModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "detentpin", + detentPinModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/detentpin/register.ts b/src/models/detentpin/register.ts new file mode 100644 index 0000000..b798854 --- /dev/null +++ b/src/models/detentpin/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { detentPinModelDefinitionSchema } from "./schema" +import { parseDetentPinModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "detentpin", + schema: detentPinModelDefinitionSchema, + parse: parseDetentPinModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/detentpin/schema.ts b/src/models/detentpin/schema.ts new file mode 100644 index 0000000..9a72048 --- /dev/null +++ b/src/models/detentpin/schema.ts @@ -0,0 +1,68 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + shaftDiameter: positiveLength.default(8), + shaftLength: positiveLength.default(50), + headDiameter: positiveLength.default(18), + headHeight: positiveLength.default(6), + ballDiameter: positiveLength.default(3), + ballOffset: positiveLength.default(4), + ringDiameter: positiveLength.default(24), + ringWire: positiveLength.default(2), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.headDiameter > p.shaftDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid detentpin dimensions: headDiameter > shaftDiameter", + }) + if (!(p.ballDiameter < p.shaftDiameter / 2)) + context.addIssue({ + code: "custom", + message: "Invalid detentpin dimensions: ballDiameter < shaftDiameter/2", + }) + if (!(p.ballOffset > p.ballDiameter / 2)) + context.addIssue({ + code: "custom", + message: "Invalid detentpin dimensions: ballOffset > ballDiameter/2", + }) + if (!(p.ballOffset < p.shaftLength / 3)) + context.addIssue({ + code: "custom", + message: "Invalid detentpin dimensions: ballOffset < shaftLength/3", + }) + if (!(p.ringDiameter > 4 * p.ringWire)) + context.addIssue({ + code: "custom", + message: "Invalid detentpin dimensions: ringDiameter > 4*ringWire", + }) + if (!(p.ringWire < p.headHeight / 2)) + context.addIssue({ + code: "custom", + message: "Invalid detentpin dimensions: ringWire < headHeight/2", + }) +} +export const detentPinModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const detentPinModelDefinitionSchema = z + .object({ fn: z.literal("detentpin"), ...shape }) + .strict() + .superRefine(validate) +export type DetentPinModelPropsInput = z.input +export type DetentPinModelProps = z.output +export type DetentPinModelDefinition = z.output< + typeof detentPinModelDefinitionSchema +> +/** Nominal ball detent pin, shaft Z, transverse pull ring in XZ; mechanism internals omitted. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getDetentPinDimensions(input: DetentPinModelPropsInput = {}) { + return detentPinModelPropsSchema.parse(input) +} diff --git a/src/models/dinrail/index.ts b/src/models/dinrail/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/dinrail/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/dinrail/parse-model-string.ts b/src/models/dinrail/parse-model-string.ts new file mode 100644 index 0000000..ad43196 --- /dev/null +++ b/src/models/dinrail/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { dinRailModelDefinitionSchema } from "./schema" +const aliases = { + width: "width", + height: "height", + thickness: "thickness", + length: "length", + holediameter: "holeDiameter", + slotlength: "slotLength", + slotpitch: "slotPitch", + w: "width", + h: "height", + l: "length", + t: "thickness", +} as const +export function parseDinRailModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "dinrail", + dinRailModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/dinrail/register.ts b/src/models/dinrail/register.ts new file mode 100644 index 0000000..722a9e6 --- /dev/null +++ b/src/models/dinrail/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { dinRailModelDefinitionSchema } from "./schema" +import { parseDinRailModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "dinrail", + schema: dinRailModelDefinitionSchema, + parse: parseDinRailModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/dinrail/schema.ts b/src/models/dinrail/schema.ts new file mode 100644 index 0000000..8e1928b --- /dev/null +++ b/src/models/dinrail/schema.ts @@ -0,0 +1,80 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + width: positive.default(35), + height: positive.default(7.5), + thickness: positive.default(1), + length: positive.default(100), + holeDiameter: positive.default(5), + slotLength: positive.default(10), + slotPitch: positive.default(25), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (2 * p.thickness >= p.height) + context.addIssue({ + code: "custom", + message: "Incompatible dinrail dimensions: 1", + }) + if (4 * p.thickness >= p.width) + context.addIssue({ + code: "custom", + message: "Incompatible dinrail dimensions: 2", + }) + if (p.holeDiameter >= p.width * 0.45) + context.addIssue({ + code: "custom", + message: "Incompatible dinrail dimensions: 3", + }) + if (p.slotLength < p.holeDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible dinrail dimensions: 4", + }) + if (p.slotPitch <= p.slotLength) + context.addIssue({ + code: "custom", + message: "Incompatible dinrail dimensions: 5", + }) + if (p.length < p.slotPitch) + context.addIssue({ + code: "custom", + message: "Incompatible dinrail dimensions: 6", + }) + if (p.length / p.slotPitch > 512) + context.addIssue({ + code: "custom", + message: "Too many repeated rail or duct features; maximum 512", + }) +} +export const dinRailModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const dinRailModelDefinitionSchema = z + .object({ fn: z.literal("dinrail"), ...shape }) + .strict() + .superRefine(validate) +export type DinRailModelPropsInput = z.input +export type DinRailModelProps = z.output +export type DinRailModelDefinition = z.output< + typeof dinRailModelDefinitionSchema +> +/** Generic top-hat DIN rail with a central floor, upright webs, outturned flanges and repeated oblong mounting holes. Rail length runs along X and the section width is Y. Dimensions are nominal; this model does not assert compliance with a rail standard. All normalized lengths are millimeters. */ +export function getDinRailDimensions(input: DinRailModelPropsInput = {}) { + const p = dinRailModelPropsSchema.parse(input) + return { + ...p, + size: [p.length, p.width, p.height] as [number, number, number], + } +} diff --git a/src/models/drawlatch/index.ts b/src/models/drawlatch/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/drawlatch/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/drawlatch/parse-model-string.ts b/src/models/drawlatch/parse-model-string.ts new file mode 100644 index 0000000..b95bb30 --- /dev/null +++ b/src/models/drawlatch/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { drawLatchModelDefinitionSchema } from "./schema" +const aliases = { + length: "length", + width: "width", + basethickness: "baseThickness", + handleheight: "handleHeight", + wirediameter: "wireDiameter", + holediameter: "holeDiameter", + w: "width", + l: "length", +} as const +export function parseDrawLatchModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "drawlatch", + drawLatchModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/drawlatch/register.ts b/src/models/drawlatch/register.ts new file mode 100644 index 0000000..25712dc --- /dev/null +++ b/src/models/drawlatch/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { drawLatchModelDefinitionSchema } from "./schema" +import { parseDrawLatchModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "drawlatch", + schema: drawLatchModelDefinitionSchema, + parse: parseDrawLatchModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/drawlatch/schema.ts b/src/models/drawlatch/schema.ts new file mode 100644 index 0000000..3103fe6 --- /dev/null +++ b/src/models/drawlatch/schema.ts @@ -0,0 +1,61 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + length: positiveLength.default(100), + width: positiveLength.default(25), + baseThickness: positiveLength.default(4), + handleHeight: positiveLength.default(18), + wireDiameter: positiveLength.default(4), + holeDiameter: positiveLength.default(4), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.length > 3 * p.width)) + context.addIssue({ + code: "custom", + message: "Invalid drawlatch dimensions: length > 3*width", + }) + if (!(p.width > 4 * p.wireDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid drawlatch dimensions: width > 4*wireDiameter", + }) + if (!(p.handleHeight > 2 * p.wireDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid drawlatch dimensions: handleHeight > 2*wireDiameter", + }) + if (!(p.baseThickness < p.handleHeight / 2)) + context.addIssue({ + code: "custom", + message: "Invalid drawlatch dimensions: baseThickness < handleHeight/2", + }) + if (!(p.holeDiameter < p.width / 4)) + context.addIssue({ + code: "custom", + message: "Invalid drawlatch dimensions: holeDiameter < width/4", + }) +} +export const drawLatchModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const drawLatchModelDefinitionSchema = z + .object({ fn: z.literal("drawlatch"), ...shape }) + .strict() + .superRefine(validate) +export type DrawLatchModelPropsInput = z.input +export type DrawLatchModelProps = z.output +export type DrawLatchModelDefinition = z.output< + typeof drawLatchModelDefinitionSchema +> +/** Nominal closed draw latch with base, handle, wire bail and keeper; long axis X. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getDrawLatchDimensions(input: DrawLatchModelPropsInput = {}) { + return drawLatchModelPropsSchema.parse(input) +} diff --git a/src/models/emergencystopswitch/index.ts b/src/models/emergencystopswitch/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/emergencystopswitch/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/emergencystopswitch/parse-model-string.ts b/src/models/emergencystopswitch/parse-model-string.ts new file mode 100644 index 0000000..c8e40f5 --- /dev/null +++ b/src/models/emergencystopswitch/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { emergencyStopSwitchModelDefinitionSchema } from "./schema" +const aliases = { + paneldiameter: "panelDiameter", + headdiameter: "headDiameter", + bodylength: "bodyLength", + headheight: "headHeight", + collardiameter: "collarDiameter", +} as const +export function parseEmergencyStopSwitchModelParams( + raw: RawModelprinterParams, +) { + return parseIndustrialModelParams( + raw, + "emergencystopswitch", + emergencyStopSwitchModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/emergencystopswitch/register.ts b/src/models/emergencystopswitch/register.ts new file mode 100644 index 0000000..79674ef --- /dev/null +++ b/src/models/emergencystopswitch/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { emergencyStopSwitchModelDefinitionSchema } from "./schema" +import { parseEmergencyStopSwitchModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "emergencystopswitch", + schema: emergencyStopSwitchModelDefinitionSchema, + parse: parseEmergencyStopSwitchModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/emergencystopswitch/schema.ts b/src/models/emergencystopswitch/schema.ts new file mode 100644 index 0000000..7ccaa3d --- /dev/null +++ b/src/models/emergencystopswitch/schema.ts @@ -0,0 +1,68 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + panelDiameter: positive.default(22), + headDiameter: positive.default(40), + bodyLength: positive.default(45), + headHeight: positive.default(18), + collarDiameter: positive.default(30), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.collarDiameter <= p.panelDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible emergencystopswitch dimensions: 1", + }) + if (p.headDiameter <= p.collarDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible emergencystopswitch dimensions: 2", + }) + if (p.headHeight >= p.bodyLength) + context.addIssue({ + code: "custom", + message: "Incompatible emergencystopswitch dimensions: 3", + }) +} +export const emergencyStopSwitchModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const emergencyStopSwitchModelDefinitionSchema = z + .object({ fn: z.literal("emergencystopswitch"), ...shape }) + .strict() + .superRefine(validate) +export type EmergencyStopSwitchModelPropsInput = z.input< + typeof emergencyStopSwitchModelPropsSchema +> +export type EmergencyStopSwitchModelProps = z.output< + typeof emergencyStopSwitchModelPropsSchema +> +export type EmergencyStopSwitchModelDefinition = z.output< + typeof emergencyStopSwitchModelDefinitionSchema +> +/** Mushroom-head emergency stop operator with a panel collar, tapered head, rear contact block and terminal tabs. This is a mechanical placement model and does not assert contact configuration, latching performance or safety certification. All normalized lengths are millimeters. */ +export function getEmergencyStopSwitchDimensions( + input: EmergencyStopSwitchModelPropsInput = {}, +) { + const p = emergencyStopSwitchModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.headDiameter, + p.headDiameter, + p.bodyLength + 4 + p.headHeight + 6, + ] as [number, number, number], + } +} diff --git a/src/models/expandedmetal/index.ts b/src/models/expandedmetal/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/expandedmetal/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/expandedmetal/parse-model-string.ts b/src/models/expandedmetal/parse-model-string.ts new file mode 100644 index 0000000..d860173 --- /dev/null +++ b/src/models/expandedmetal/parse-model-string.ts @@ -0,0 +1,23 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { expandedMetalModelDefinitionSchema } from "./schema" +const aliases = { + width: "width", + length: "length", + thickness: "thickness", + diamondwidth: "diamondWidth", + diamondlength: "diamondLength", + strandwidth: "strandWidth", + framewidth: "frameWidth", + w: "width", + l: "length", + t: "thickness", +} as const +export function parseExpandedMetalModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "expandedmetal", + expandedMetalModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/expandedmetal/register.ts b/src/models/expandedmetal/register.ts new file mode 100644 index 0000000..2c74264 --- /dev/null +++ b/src/models/expandedmetal/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { expandedMetalModelDefinitionSchema } from "./schema" +import { parseExpandedMetalModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "expandedmetal", + schema: expandedMetalModelDefinitionSchema, + parse: parseExpandedMetalModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/expandedmetal/schema.ts b/src/models/expandedmetal/schema.ts new file mode 100644 index 0000000..e7650b4 --- /dev/null +++ b/src/models/expandedmetal/schema.ts @@ -0,0 +1,71 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + width: positiveLength.default(100), + length: positiveLength.default(100), + thickness: positiveLength.default(2), + diamondWidth: positiveLength.default(15), + diamondLength: positiveLength.default(30), + strandWidth: positiveLength.default(2), + frameWidth: positiveLength.default(3), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.width > 2 * p.diamondWidth)) + context.addIssue({ + code: "custom", + message: "Invalid expandedmetal dimensions: width > 2*diamondWidth", + }) + if (!(p.length > 2 * p.diamondLength)) + context.addIssue({ + code: "custom", + message: "Invalid expandedmetal dimensions: length > 2*diamondLength", + }) + if (!(p.strandWidth < Math.min(p.diamondWidth, p.diamondLength) / 4)) + context.addIssue({ + code: "custom", + message: + "Invalid expandedmetal dimensions: strandWidth < Math.min(diamondWidth,diamondLength)/4", + }) + if (!(p.frameWidth < Math.min(p.width, p.length) / 8)) + context.addIssue({ + code: "custom", + message: + "Invalid expandedmetal dimensions: frameWidth < Math.min(width,length)/8", + }) + if (!(((p.width / p.diamondWidth) * p.length) / p.diamondLength <= 200)) + context.addIssue({ + code: "custom", + message: + "Invalid expandedmetal dimensions: width/diamondWidth*length/diamondLength <= 200", + }) +} +export const expandedMetalModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const expandedMetalModelDefinitionSchema = z + .object({ fn: z.literal("expandedmetal"), ...shape }) + .strict() + .superRefine(validate) +export type ExpandedMetalModelPropsInput = z.input< + typeof expandedMetalModelPropsSchema +> +export type ExpandedMetalModelProps = z.output< + typeof expandedMetalModelPropsSchema +> +export type ExpandedMetalModelDefinition = z.output< + typeof expandedMetalModelDefinitionSchema +> +/** Flat expanded-metal visual sheet with staggered diamond apertures and perimeter frame; XY, bottom Z=0. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getExpandedMetalDimensions( + input: ExpandedMetalModelPropsInput = {}, +) { + return expandedMetalModelPropsSchema.parse(input) +} diff --git a/src/models/eyebolt/index.ts b/src/models/eyebolt/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/eyebolt/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/eyebolt/parse-model-string.ts b/src/models/eyebolt/parse-model-string.ts new file mode 100644 index 0000000..4c8e48c --- /dev/null +++ b/src/models/eyebolt/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { eyeBoltModelDefinitionSchema } from "./schema" +export function parseEyeBoltModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "eyebolt", + eyeBoltModelDefinitionSchema, + { + d: "shaftDiameter", + shaftdiameter: "shaftDiameter", + l: "shaftLength", + shaftlength: "shaftLength", + id: "eyeInnerDiameter", + eyeinnerdiameter: "eyeInnerDiameter", + wd: "eyeWireDiameter", + eyewirediameter: "eyeWireDiameter", + }, + ) +} diff --git a/src/models/eyebolt/register.ts b/src/models/eyebolt/register.ts new file mode 100644 index 0000000..da1a260 --- /dev/null +++ b/src/models/eyebolt/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { eyeBoltModelDefinitionSchema } from "./schema" +import { parseEyeBoltModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "eyebolt", + schema: eyeBoltModelDefinitionSchema, + parse: parseEyeBoltModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/eyebolt/schema.ts b/src/models/eyebolt/schema.ts new file mode 100644 index 0000000..753d875 --- /dev/null +++ b/src/models/eyebolt/schema.ts @@ -0,0 +1,65 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + shaftDiameter: positiveLength.default(6), + shaftLength: positiveLength.default(24), + eyeInnerDiameter: positiveLength.default(14), + eyeWireDiameter: positiveLength.default(5), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![p.eyeInnerDiameter - p.eyeWireDiameter].every( + (gap) => gap >= extent * 0.0001, + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if ( + !( + p.eyeWireDiameter < p.eyeInnerDiameter && + p.shaftDiameter < p.eyeInnerDiameter + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Invalid eyebolt dimensions: mating features must fit within the body", + }) +} +export const eyeBoltModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const eyeBoltModelDefinitionSchema = z + .object({ fn: z.literal("eyebolt"), ...shape }) + .strict() + .superRefine(validate) +export type EyeBoltModelPropsInput = z.input +export type EyeBoltModelProps = z.output +export type EyeBoltModelDefinition = z.output< + typeof eyeBoltModelDefinitionSchema +> +export function getEyeBoltDimensions(input: EyeBoltModelPropsInput = {}) { + return eyeBoltModelPropsSchema.parse(input) +} diff --git a/src/models/flangebearing/index.ts b/src/models/flangebearing/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/flangebearing/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/flangebearing/parse-model-string.ts b/src/models/flangebearing/parse-model-string.ts new file mode 100644 index 0000000..59d71b1 --- /dev/null +++ b/src/models/flangebearing/parse-model-string.ts @@ -0,0 +1,27 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { flangeBearingModelDefinitionSchema } from "./schema" +const aliases = { + bore: "boreDiameter", + fw: "flangeWidth", + ft: "flangeThickness", + hd: "housingDiameter", + hh: "housingHeight", + ms: "mountSpacing", + mh: "mountHoleDiameter", + borediameter: "boreDiameter", + flangewidth: "flangeWidth", + flangethickness: "flangeThickness", + housingdiameter: "housingDiameter", + housingheight: "housingHeight", + mountspacing: "mountSpacing", + mountholediameter: "mountHoleDiameter", +} as const +export function parseFlangeBearingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "flangebearing", + flangeBearingModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/flangebearing/register.ts b/src/models/flangebearing/register.ts new file mode 100644 index 0000000..d210f5d --- /dev/null +++ b/src/models/flangebearing/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { flangeBearingModelDefinitionSchema } from "./schema" +import { parseFlangeBearingModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "flangebearing", + schema: flangeBearingModelDefinitionSchema, + parse: parseFlangeBearingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/flangebearing/schema.ts b/src/models/flangebearing/schema.ts new file mode 100644 index 0000000..64a33e2 --- /dev/null +++ b/src/models/flangebearing/schema.ts @@ -0,0 +1,91 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + boreDiameter: positiveLength.default(20), + flangeWidth: positiveLength.default(64), + flangeThickness: positiveLength.default(6), + housingDiameter: positiveLength.default(44), + housingHeight: positiveLength.default(20), + mountSpacing: positiveLength.default(46), + mountHoleDiameter: positiveLength.default(6), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.housingDiameter <= p.boreDiameter + 12) + context.addIssue({ + code: "custom", + message: "Housing must leave space around the shaft for races and balls", + }) + if (p.mountSpacing + p.mountHoleDiameter >= p.flangeWidth) + context.addIssue({ + code: "custom", + message: "Mounting holes must remain within the flange", + }) + if (Math.SQRT2 * p.mountSpacing - p.mountHoleDiameter <= p.housingDiameter) + context.addIssue({ + code: "custom", + message: "Corner mounting holes must clear the raised bearing housing", + }) + if (p.housingHeight <= 4) + context.addIssue({ + code: "custom", + message: "Housing height must leave room for rolling elements", + }) + if ( + (((p.housingDiameter - p.boreDiameter) * 0.72 - 6) * 0.85) / 2 >= + p.flangeThickness + p.housingHeight + ) + context.addIssue({ + code: "custom", + message: "Ball diameter must fit within bearing height", + }) + if ( + (((p.housingDiameter - p.boreDiameter) * 0.72 - 6) * 0.85) / 2 >= + ((2 * p.boreDiameter + (p.housingDiameter - p.boreDiameter) * 0.72) / 2) * + Math.sin(Math.PI / 10) + ) + context.addIssue({ + code: "custom", + message: + "Adjacent bearing balls must have positive circumferential clearance", + }) +} +export const flangeBearingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const flangeBearingModelDefinitionSchema = z + .object({ fn: z.literal("flangebearing"), ...shape }) + .strict() + .superRefine(validate) +export type FlangeBearingModelPropsInput = z.input< + typeof flangeBearingModelPropsSchema +> +export type FlangeBearingModelProps = z.output< + typeof flangeBearingModelPropsSchema +> +export type FlangeBearingModelDefinition = z.output< + typeof flangeBearingModelDefinitionSchema +> +/** Square four-bolt flange bearing with an axial Z shaft bore and open top rolling-element race. Flange bottom is Z=0. Nominal assembly envelope and race clearances; not a manufacturer part or load-rated design. All lengths are normalized to millimeters. */ +export function getFlangeBearingDimensions( + input: FlangeBearingModelPropsInput = {}, +) { + const p = flangeBearingModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.flangeWidth, + p.flangeWidth, + p.flangeThickness + p.housingHeight, + ] as [number, number, number], + bottomZ: 0, + topZ: p.flangeThickness + p.housingHeight, + } +} diff --git a/src/models/flatwasher/index.ts b/src/models/flatwasher/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/flatwasher/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/flatwasher/parse-model-string.ts b/src/models/flatwasher/parse-model-string.ts new file mode 100644 index 0000000..d11a2e3 --- /dev/null +++ b/src/models/flatwasher/parse-model-string.ts @@ -0,0 +1,18 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { flatWasherModelDefinitionSchema } from "./schema" +export function parseFlatWasherModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "flatwasher", + flatWasherModelDefinitionSchema, + { + id: "innerDiameter", + innerdiameter: "innerDiameter", + od: "outerDiameter", + outerdiameter: "outerDiameter", + t: "thickness", + thickness: "thickness", + }, + ) +} diff --git a/src/models/flatwasher/register.ts b/src/models/flatwasher/register.ts new file mode 100644 index 0000000..8358c04 --- /dev/null +++ b/src/models/flatwasher/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { flatWasherModelDefinitionSchema } from "./schema" +import { parseFlatWasherModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "flatwasher", + schema: flatWasherModelDefinitionSchema, + parse: parseFlatWasherModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/flatwasher/schema.ts b/src/models/flatwasher/schema.ts new file mode 100644 index 0000000..b64d84b --- /dev/null +++ b/src/models/flatwasher/schema.ts @@ -0,0 +1,61 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + innerDiameter: positiveLength.default(6.4), + outerDiameter: positiveLength.default(12), + thickness: positiveLength.default(1.6), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![(p.outerDiameter - p.innerDiameter) / 2].every( + (gap) => gap >= extent * 0.0001, + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if (!(p.innerDiameter < p.outerDiameter)) + ctx.addIssue({ + code: "custom", + message: + "Invalid flatwasher dimensions: mating features must fit within the body", + }) +} +export const flatWasherModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const flatWasherModelDefinitionSchema = z + .object({ fn: z.literal("flatwasher"), ...shape }) + .strict() + .superRefine(validate) +export type FlatWasherModelPropsInput = z.input< + typeof flatWasherModelPropsSchema +> +export type FlatWasherModelProps = z.output +export type FlatWasherModelDefinition = z.output< + typeof flatWasherModelDefinitionSchema +> +export function getFlatWasherDimensions(input: FlatWasherModelPropsInput = {}) { + return flatWasherModelPropsSchema.parse(input) +} diff --git a/src/models/flowmeter/index.ts b/src/models/flowmeter/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/flowmeter/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/flowmeter/parse-model-string.ts b/src/models/flowmeter/parse-model-string.ts new file mode 100644 index 0000000..158dd69 --- /dev/null +++ b/src/models/flowmeter/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { flowMeterModelDefinitionSchema } from "./schema" +const aliases = { + length: "length", + diameter: "diameter", + borediameter: "boreDiameter", + enddiameter: "endDiameter", + endlength: "endLength", + floatdiameter: "floatDiameter", + d: "diameter", + l: "length", +} as const +export function parseFlowMeterModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "flowmeter", + flowMeterModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/flowmeter/register.ts b/src/models/flowmeter/register.ts new file mode 100644 index 0000000..dc9293b --- /dev/null +++ b/src/models/flowmeter/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { flowMeterModelDefinitionSchema } from "./schema" +import { parseFlowMeterModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "flowmeter", + schema: flowMeterModelDefinitionSchema, + parse: parseFlowMeterModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/flowmeter/schema.ts b/src/models/flowmeter/schema.ts new file mode 100644 index 0000000..7865727 --- /dev/null +++ b/src/models/flowmeter/schema.ts @@ -0,0 +1,64 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + length: positive.default(120), + diameter: positive.default(24), + boreDiameter: positive.default(12), + endDiameter: positive.default(32), + endLength: positive.default(12), + floatDiameter: positive.default(8), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.boreDiameter >= p.diameter) + context.addIssue({ + code: "custom", + message: "Incompatible flowmeter dimensions: 1", + }) + if (p.endDiameter <= p.diameter) + context.addIssue({ + code: "custom", + message: "Incompatible flowmeter dimensions: 2", + }) + if (2 * p.endLength >= p.length) + context.addIssue({ + code: "custom", + message: "Incompatible flowmeter dimensions: 3", + }) + if (p.floatDiameter >= p.boreDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible flowmeter dimensions: 4", + }) +} +export const flowMeterModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const flowMeterModelDefinitionSchema = z + .object({ fn: z.literal("flowmeter"), ...shape }) + .strict() + .superRefine(validate) +export type FlowMeterModelPropsInput = z.input +export type FlowMeterModelProps = z.output +export type FlowMeterModelDefinition = z.output< + typeof flowMeterModelDefinitionSchema +> +/** Vertical variable-area flowmeter with hollow end fittings, a rear tube wall, visible float and scale ticks. The front wall is intentionally cut away to show the nominal internal float through the inspection window; this is an explanatory envelope model rather than a closed pressure vessel. All normalized lengths are millimeters. */ +export function getFlowMeterDimensions(input: FlowMeterModelPropsInput = {}) { + const p = flowMeterModelPropsSchema.parse(input) + return { + ...p, + size: [p.endDiameter, p.endDiameter, p.length] as [number, number, number], + } +} diff --git a/src/models/gearrack/index.ts b/src/models/gearrack/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/gearrack/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/gearrack/parse-model-string.ts b/src/models/gearrack/parse-model-string.ts new file mode 100644 index 0000000..ab4028b --- /dev/null +++ b/src/models/gearrack/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { gearRackModelDefinitionSchema } from "./schema" +const aliases = { + pitch: "pitch", + teeth: "toothCount", + w: "width", + h: "baseHeight", + th: "toothHeight", + mh: "mountHoleDiameter", + toothcount: "toothCount", + width: "width", + baseheight: "baseHeight", + toothheight: "toothHeight", + mountholediameter: "mountHoleDiameter", +} as const +export function parseGearRackModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "gearrack", + gearRackModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/gearrack/register.ts b/src/models/gearrack/register.ts new file mode 100644 index 0000000..102f8de --- /dev/null +++ b/src/models/gearrack/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { gearRackModelDefinitionSchema } from "./schema" +import { parseGearRackModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "gearrack", + schema: gearRackModelDefinitionSchema, + parse: parseGearRackModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/gearrack/schema.ts b/src/models/gearrack/schema.ts new file mode 100644 index 0000000..56286ee --- /dev/null +++ b/src/models/gearrack/schema.ts @@ -0,0 +1,61 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + pitch: positiveLength.default(8), + toothCount: z.number().int().min(2).max(64).default(12), + width: positiveLength.default(12), + baseHeight: positiveLength.default(8), + toothHeight: positiveLength.default(4), + mountHoleDiameter: positiveLength.default(3.5), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.mountHoleDiameter >= Math.min(p.width, p.pitch)) + context.addIssue({ + code: "custom", + message: "Mounting holes must fit within rack width and end tooth pitch", + }) + if (p.toothHeight >= p.pitch) + context.addIssue({ + code: "custom", + message: "Tooth height must be smaller than pitch", + }) + if (p.toothCount < 3) + context.addIssue({ + code: "custom", + message: "Rack must have at least three teeth", + }) +} +export const gearRackModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const gearRackModelDefinitionSchema = z + .object({ fn: z.literal("gearrack"), ...shape }) + .strict() + .superRefine(validate) +export type GearRackModelPropsInput = z.input +export type GearRackModelProps = z.output +export type GearRackModelDefinition = z.output< + typeof gearRackModelDefinitionSchema +> +/** Straight rack envelope along X with trapezoidal teeth above a mounting bar and two vertical mounting holes. Base underside Z=0. Teeth are nominal placement geometry, not conjugate involute teeth or a machining standard. All lengths are normalized to millimeters. */ +export function getGearRackDimensions(input: GearRackModelPropsInput = {}) { + const p = gearRackModelPropsSchema.parse(input) + return { + ...p, + size: [p.pitch * p.toothCount, p.width, p.baseHeight + p.toothHeight] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.baseHeight + p.toothHeight, + } +} diff --git a/src/models/hairpincotter/index.ts b/src/models/hairpincotter/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/hairpincotter/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/hairpincotter/parse-model-string.ts b/src/models/hairpincotter/parse-model-string.ts new file mode 100644 index 0000000..a2d1191 --- /dev/null +++ b/src/models/hairpincotter/parse-model-string.ts @@ -0,0 +1,18 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { hairpinCotterModelDefinitionSchema } from "./schema" +export function parseHairpinCotterModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "hairpincotter", + hairpinCotterModelDefinitionSchema, + { + d: "wireDiameter", + wirediameter: "wireDiameter", + l: "length", + length: "length", + ld: "loopDiameter", + loopdiameter: "loopDiameter", + }, + ) +} diff --git a/src/models/hairpincotter/register.ts b/src/models/hairpincotter/register.ts new file mode 100644 index 0000000..919da43 --- /dev/null +++ b/src/models/hairpincotter/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { hairpinCotterModelDefinitionSchema } from "./schema" +import { parseHairpinCotterModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "hairpincotter", + schema: hairpinCotterModelDefinitionSchema, + parse: parseHairpinCotterModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/hairpincotter/schema.ts b/src/models/hairpincotter/schema.ts new file mode 100644 index 0000000..95b6ded --- /dev/null +++ b/src/models/hairpincotter/schema.ts @@ -0,0 +1,66 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + wireDiameter: positiveLength.default(2), + length: positiveLength.default(40), + loopDiameter: positiveLength.default(10), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![ + (p.loopDiameter - 3 * p.wireDiameter) / 2, + p.length - 2 * p.loopDiameter, + ].every((gap) => gap >= extent * 0.0001) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if (!(p.wireDiameter * 3 < p.loopDiameter && p.length > p.loopDiameter * 2)) + ctx.addIssue({ + code: "custom", + message: + "Invalid hairpincotter dimensions: mating features must fit within the body", + }) +} +export const hairpinCotterModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const hairpinCotterModelDefinitionSchema = z + .object({ fn: z.literal("hairpincotter"), ...shape }) + .strict() + .superRefine(validate) +export type HairpinCotterModelPropsInput = z.input< + typeof hairpinCotterModelPropsSchema +> +export type HairpinCotterModelProps = z.output< + typeof hairpinCotterModelPropsSchema +> +export type HairpinCotterModelDefinition = z.output< + typeof hairpinCotterModelDefinitionSchema +> +export function getHairpinCotterDimensions( + input: HairpinCotterModelPropsInput = {}, +) { + return hairpinCotterModelPropsSchema.parse(input) +} diff --git a/src/models/handwheel/index.ts b/src/models/handwheel/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/handwheel/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/handwheel/parse-model-string.ts b/src/models/handwheel/parse-model-string.ts new file mode 100644 index 0000000..abbf64f --- /dev/null +++ b/src/models/handwheel/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { handwheelModelDefinitionSchema } from "./schema" +const aliases = { + outerdiameter: "outerDiameter", + rimwidth: "rimWidth", + hubdiameter: "hubDiameter", + hubheight: "hubHeight", + borediameter: "boreDiameter", + spokes: "spokes", + od: "outerDiameter", + bore: "boreDiameter", +} as const +export function parseHandwheelModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "handwheel", + handwheelModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/handwheel/register.ts b/src/models/handwheel/register.ts new file mode 100644 index 0000000..ef9f011 --- /dev/null +++ b/src/models/handwheel/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { handwheelModelDefinitionSchema } from "./schema" +import { parseHandwheelModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "handwheel", + schema: handwheelModelDefinitionSchema, + parse: parseHandwheelModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/handwheel/schema.ts b/src/models/handwheel/schema.ts new file mode 100644 index 0000000..b466970 --- /dev/null +++ b/src/models/handwheel/schema.ts @@ -0,0 +1,57 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + outerDiameter: positiveLength.default(100), + rimWidth: positiveLength.default(12), + hubDiameter: positiveLength.default(28), + hubHeight: positiveLength.default(25), + boreDiameter: positiveLength.default(12), + spokes: z.number().int().min(3).max(12).default(4), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.rimWidth < p.outerDiameter / 5)) + context.addIssue({ + code: "custom", + message: "Invalid handwheel dimensions: rimWidth < outerDiameter/5", + }) + if (!(p.hubDiameter < p.outerDiameter - 4 * p.rimWidth)) + context.addIssue({ + code: "custom", + message: + "Invalid handwheel dimensions: hubDiameter < outerDiameter-4*rimWidth", + }) + if (!(p.boreDiameter < p.hubDiameter * 0.8)) + context.addIssue({ + code: "custom", + message: "Invalid handwheel dimensions: boreDiameter < hubDiameter*.8", + }) + if (!(p.hubHeight >= p.rimWidth)) + context.addIssue({ + code: "custom", + message: "Invalid handwheel dimensions: hubHeight >= rimWidth", + }) +} +export const handwheelModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const handwheelModelDefinitionSchema = z + .object({ fn: z.literal("handwheel"), ...shape }) + .strict() + .superRefine(validate) +export type HandwheelModelPropsInput = z.input +export type HandwheelModelProps = z.output +export type HandwheelModelDefinition = z.output< + typeof handwheelModelDefinitionSchema +> +/** Flat-rim spoked handwheel with axial bore, rotating axis Z. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getHandwheelDimensions(input: HandwheelModelPropsInput = {}) { + return handwheelModelPropsSchema.parse(input) +} diff --git a/src/models/hinge/index.ts b/src/models/hinge/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/hinge/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/hinge/parse-model-string.ts b/src/models/hinge/parse-model-string.ts new file mode 100644 index 0000000..e995f8a --- /dev/null +++ b/src/models/hinge/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { hingeModelDefinitionSchema } from "./schema" +const aliases = { + width: "width", + length: "length", + leafthickness: "leafThickness", + barreldiameter: "barrelDiameter", + pindiameter: "pinDiameter", + holediameter: "holeDiameter", + w: "width", + l: "length", +} as const +export function parseHingeModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "hinge", + hingeModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/hinge/register.ts b/src/models/hinge/register.ts new file mode 100644 index 0000000..8a09805 --- /dev/null +++ b/src/models/hinge/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { hingeModelDefinitionSchema } from "./schema" +import { parseHingeModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "hinge", + schema: hingeModelDefinitionSchema, + parse: parseHingeModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/hinge/schema.ts b/src/models/hinge/schema.ts new file mode 100644 index 0000000..1bb7970 --- /dev/null +++ b/src/models/hinge/schema.ts @@ -0,0 +1,60 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + width: positiveLength.default(60), + length: positiveLength.default(60), + leafThickness: positiveLength.default(3), + barrelDiameter: positiveLength.default(10), + pinDiameter: positiveLength.default(4), + holeDiameter: positiveLength.default(5), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.width > 3 * p.barrelDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid hinge dimensions: width > 3*barrelDiameter", + }) + if (!(p.length > 3 * p.barrelDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid hinge dimensions: length > 3*barrelDiameter", + }) + if (!(p.pinDiameter < p.barrelDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid hinge dimensions: pinDiameter < barrelDiameter", + }) + if (!(p.leafThickness < p.barrelDiameter / 2)) + context.addIssue({ + code: "custom", + message: "Invalid hinge dimensions: leafThickness < barrelDiameter/2", + }) + if (!(p.holeDiameter < Math.min(p.width, p.length) / 8)) + context.addIssue({ + code: "custom", + message: + "Invalid hinge dimensions: holeDiameter < Math.min(width,length)/8", + }) +} +export const hingeModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const hingeModelDefinitionSchema = z + .object({ fn: z.literal("hinge"), ...shape }) + .strict() + .superRefine(validate) +export type HingeModelPropsInput = z.input +export type HingeModelProps = z.output +export type HingeModelDefinition = z.output +/** Flat open butt hinge, leaves in XY, hinge axis along Y; bottom Z=0. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getHingeDimensions(input: HingeModelPropsInput = {}) { + return hingeModelPropsSchema.parse(input) +} diff --git a/src/models/hosebarb/index.ts b/src/models/hosebarb/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/hosebarb/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/hosebarb/parse-model-string.ts b/src/models/hosebarb/parse-model-string.ts new file mode 100644 index 0000000..534ffcd --- /dev/null +++ b/src/models/hosebarb/parse-model-string.ts @@ -0,0 +1,26 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { hoseBarbModelDefinitionSchema } from "./schema" +export function parseHoseBarbModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "hosebarb", + hoseBarbModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + l: "length", + barbod: "barbDiameter", + barbl: "barbLength", + pitch: "barbPitch", + af: "hexAcrossFlats", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + length: "length", + barbdiameter: "barbDiameter", + barblength: "barbLength", + barbpitch: "barbPitch", + hexacrossflats: "hexAcrossFlats", + }, + ) +} diff --git a/src/models/hosebarb/register.ts b/src/models/hosebarb/register.ts new file mode 100644 index 0000000..add5edf --- /dev/null +++ b/src/models/hosebarb/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parseHoseBarbModelParams } from "./parse-model-string" +import { hoseBarbModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "hosebarb", + schema: hoseBarbModelDefinitionSchema, + parse: parseHoseBarbModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/hosebarb/schema.ts b/src/models/hosebarb/schema.ts new file mode 100644 index 0000000..428caf5 --- /dev/null +++ b/src/models/hosebarb/schema.ts @@ -0,0 +1,68 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(20), + boreDiameter: positiveLength.default(10), + length: positiveLength.default(45), + barbDiameter: positiveLength.default(16), + barbLength: positiveLength.default(25), + barbPitch: positiveLength.default(6), + hexAcrossFlats: positiveLength.default(24), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.barbDiameter * 0.8)) + issue("Requires boreDiameter < barbDiameter * 0.8") + if (!(p.barbDiameter < p.outerDiameter)) + issue("Requires barbDiameter < outerDiameter") + if (!(p.outerDiameter < p.hexAcrossFlats)) + issue("Requires outerDiameter < hexAcrossFlats") + if (!(p.barbLength < p.length)) issue("Requires barbLength < length") + if (!(p.barbPitch * 2 <= p.barbLength)) + issue("Requires barbPitch * 2 <= barbLength") + if (!(p.barbLength / p.barbPitch < 30)) + issue("Requires barbLength / barbPitch < 30") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.barbDiameter * 0.8 - p.boreDiameter, + p.outerDiameter - p.barbDiameter, + p.hexAcrossFlats - p.outerDiameter, + p.length - p.barbLength, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const hoseBarbModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const hoseBarbModelDefinitionSchema = z + .object({ fn: z.literal("hosebarb"), ...shape }) + .strict() + .superRefine(validate) +export type HoseBarbModelPropsInput = z.input +export type HoseBarbModelProps = z.output +export type HoseBarbModelDefinition = z.output< + typeof hoseBarbModelDefinitionSchema +> +export function getHoseBarbDimensions(input: HoseBarbModelPropsInput = {}) { + return hoseBarbModelPropsSchema.parse(input) +} diff --git a/src/models/hoseclamp/index.ts b/src/models/hoseclamp/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/hoseclamp/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/hoseclamp/parse-model-string.ts b/src/models/hoseclamp/parse-model-string.ts new file mode 100644 index 0000000..689df62 --- /dev/null +++ b/src/models/hoseclamp/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { hoseClampModelDefinitionSchema } from "./schema" +export function parseHoseClampModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "hoseclamp", + hoseClampModelDefinitionSchema, + { + id: "innerDiameter", + bandt: "bandThickness", + bandw: "bandWidth", + housingl: "housingLength", + housingw: "housingWidth", + housingh: "housingHeight", + innerdiameter: "innerDiameter", + bandthickness: "bandThickness", + bandwidth: "bandWidth", + housinglength: "housingLength", + housingwidth: "housingWidth", + housingheight: "housingHeight", + }, + ) +} diff --git a/src/models/hoseclamp/register.ts b/src/models/hoseclamp/register.ts new file mode 100644 index 0000000..7d8b178 --- /dev/null +++ b/src/models/hoseclamp/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parseHoseClampModelParams } from "./parse-model-string" +import { hoseClampModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "hoseclamp", + schema: hoseClampModelDefinitionSchema, + parse: parseHoseClampModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/hoseclamp/schema.ts b/src/models/hoseclamp/schema.ts new file mode 100644 index 0000000..5e7a2ac --- /dev/null +++ b/src/models/hoseclamp/schema.ts @@ -0,0 +1,67 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + innerDiameter: positiveLength.default(30), + bandThickness: positiveLength.default(1.5), + bandWidth: positiveLength.default(8), + housingLength: positiveLength.default(14), + housingWidth: positiveLength.default(8), + housingHeight: positiveLength.default(7), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.bandThickness * 4 < p.innerDiameter)) + issue("Requires bandThickness * 4 < innerDiameter") + if (!(p.housingWidth < p.innerDiameter)) + issue("Requires housingWidth < innerDiameter") + if (!(p.housingLength < p.innerDiameter)) + issue("Requires housingLength < innerDiameter") + if (!(p.housingHeight < p.innerDiameter)) + issue("Requires housingHeight < innerDiameter") + if (!(p.bandWidth < p.innerDiameter)) + issue("Requires bandWidth < innerDiameter") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.innerDiameter - p.bandThickness * 4, + p.innerDiameter - p.housingWidth, + p.innerDiameter - p.housingLength, + p.innerDiameter - p.housingHeight, + p.innerDiameter - p.bandWidth, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const hoseClampModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const hoseClampModelDefinitionSchema = z + .object({ fn: z.literal("hoseclamp"), ...shape }) + .strict() + .superRefine(validate) +export type HoseClampModelPropsInput = z.input +export type HoseClampModelProps = z.output +export type HoseClampModelDefinition = z.output< + typeof hoseClampModelDefinitionSchema +> +export function getHoseClampDimensions(input: HoseClampModelPropsInput = {}) { + return hoseClampModelPropsSchema.parse(input) +} diff --git a/src/models/ibeam/index.ts b/src/models/ibeam/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/ibeam/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/ibeam/parse-model-string.ts b/src/models/ibeam/parse-model-string.ts new file mode 100644 index 0000000..d1edbce --- /dev/null +++ b/src/models/ibeam/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { iBeamModelDefinitionSchema } from "./schema" +const aliases = { + width: "width", + height: "height", + webthickness: "webThickness", + flangethickness: "flangeThickness", + length: "length", + w: "width", + h: "height", + l: "length", +} as const +export function parseIBeamModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "ibeam", + iBeamModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/ibeam/register.ts b/src/models/ibeam/register.ts new file mode 100644 index 0000000..4bd4c47 --- /dev/null +++ b/src/models/ibeam/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { iBeamModelDefinitionSchema } from "./schema" +import { parseIBeamModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "ibeam", + schema: iBeamModelDefinitionSchema, + parse: parseIBeamModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/ibeam/schema.ts b/src/models/ibeam/schema.ts new file mode 100644 index 0000000..bfd4b98 --- /dev/null +++ b/src/models/ibeam/schema.ts @@ -0,0 +1,43 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + width: positiveLength.default(40), + height: positiveLength.default(60), + webThickness: positiveLength.default(4), + flangeThickness: positiveLength.default(6), + length: positiveLength.default(100), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.webThickness < p.width)) + context.addIssue({ + code: "custom", + message: "Invalid ibeam dimensions: webThickness < width", + }) + if (!(2 * p.flangeThickness < p.height)) + context.addIssue({ + code: "custom", + message: "Invalid ibeam dimensions: 2*flangeThickness < height", + }) +} +export const iBeamModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const iBeamModelDefinitionSchema = z + .object({ fn: z.literal("ibeam"), ...shape }) + .strict() + .superRefine(validate) +export type IBeamModelPropsInput = z.input +export type IBeamModelProps = z.output +export type IBeamModelDefinition = z.output +/** I-section stock, centered XY cross section, length +Z from Z=0. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getIBeamDimensions(input: IBeamModelPropsInput = {}) { + return iBeamModelPropsSchema.parse(input) +} diff --git a/src/models/inductiveproximitysensor/index.ts b/src/models/inductiveproximitysensor/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/inductiveproximitysensor/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/inductiveproximitysensor/parse-model-string.ts b/src/models/inductiveproximitysensor/parse-model-string.ts new file mode 100644 index 0000000..2d54918 --- /dev/null +++ b/src/models/inductiveproximitysensor/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { inductiveProximitySensorModelDefinitionSchema } from "./schema" +const aliases = { + barreldiameter: "barrelDiameter", + barrellength: "barrelLength", + facethickness: "faceThickness", + cablediameter: "cableDiameter", + cablelength: "cableLength", + nutwidth: "nutWidth", + nutthickness: "nutThickness", +} as const +export function parseInductiveProximitySensorModelParams( + raw: RawModelprinterParams, +) { + return parseIndustrialModelParams( + raw, + "inductiveproximitysensor", + inductiveProximitySensorModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/inductiveproximitysensor/register.ts b/src/models/inductiveproximitysensor/register.ts new file mode 100644 index 0000000..725d15e --- /dev/null +++ b/src/models/inductiveproximitysensor/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { inductiveProximitySensorModelDefinitionSchema } from "./schema" +import { parseInductiveProximitySensorModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "inductiveproximitysensor", + schema: inductiveProximitySensorModelDefinitionSchema, + parse: parseInductiveProximitySensorModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/inductiveproximitysensor/schema.ts b/src/models/inductiveproximitysensor/schema.ts new file mode 100644 index 0000000..5ecd871 --- /dev/null +++ b/src/models/inductiveproximitysensor/schema.ts @@ -0,0 +1,75 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + barrelDiameter: positive.default(18), + barrelLength: positive.default(55), + faceThickness: positive.default(2), + cableDiameter: positive.default(5), + cableLength: positive.default(15), + nutWidth: positive.default(22), + nutThickness: positive.default(4), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.faceThickness >= p.barrelLength / 4) + context.addIssue({ + code: "custom", + message: "Incompatible inductiveproximitysensor dimensions: 1", + }) + if (p.cableDiameter >= p.barrelDiameter / 2) + context.addIssue({ + code: "custom", + message: "Incompatible inductiveproximitysensor dimensions: 2", + }) + if (p.nutWidth <= p.barrelDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible inductiveproximitysensor dimensions: 3", + }) + if (2 * p.nutThickness >= p.barrelLength / 2) + context.addIssue({ + code: "custom", + message: "Incompatible inductiveproximitysensor dimensions: 4", + }) +} +export const inductiveProximitySensorModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const inductiveProximitySensorModelDefinitionSchema = z + .object({ fn: z.literal("inductiveproximitysensor"), ...shape }) + .strict() + .superRefine(validate) +export type InductiveProximitySensorModelPropsInput = z.input< + typeof inductiveProximitySensorModelPropsSchema +> +export type InductiveProximitySensorModelProps = z.output< + typeof inductiveProximitySensorModelPropsSchema +> +export type InductiveProximitySensorModelDefinition = z.output< + typeof inductiveProximitySensorModelDefinitionSchema +> +/** Cylindrical inductive proximity sensor with two hex mounting nuts, a distinct sensing face on +Z and short cable tail on -Z. Circumferential ribs suggest nominal barrel threading; they are not a functional helical thread. All normalized lengths are millimeters. */ +export function getInductiveProximitySensorDimensions( + input: InductiveProximitySensorModelPropsInput = {}, +) { + const p = inductiveProximitySensorModelPropsSchema.parse(input) + return { + ...p, + size: [ + (2 * p.nutWidth) / Math.sqrt(3), + p.nutWidth, + p.barrelLength + p.faceThickness + p.cableLength, + ] as [number, number, number], + } +} diff --git a/src/models/jawcoupling/index.ts b/src/models/jawcoupling/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/jawcoupling/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/jawcoupling/parse-model-string.ts b/src/models/jawcoupling/parse-model-string.ts new file mode 100644 index 0000000..d83fa06 --- /dev/null +++ b/src/models/jawcoupling/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { jawCouplingModelDefinitionSchema } from "./schema" +const aliases = { + od: "outerDiameter", + l: "length", + bore: "boreDiameter", + hubl: "hubLength", + jawd: "jawDepth", + jawcount: "jawCount", + outerdiameter: "outerDiameter", + length: "length", + borediameter: "boreDiameter", + hublength: "hubLength", + jawdepth: "jawDepth", +} as const +export function parseJawCouplingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "jawcoupling", + jawCouplingModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/jawcoupling/register.ts b/src/models/jawcoupling/register.ts new file mode 100644 index 0000000..bf75a99 --- /dev/null +++ b/src/models/jawcoupling/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { jawCouplingModelDefinitionSchema } from "./schema" +import { parseJawCouplingModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "jawcoupling", + schema: jawCouplingModelDefinitionSchema, + parse: parseJawCouplingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/jawcoupling/schema.ts b/src/models/jawcoupling/schema.ts new file mode 100644 index 0000000..b825869 --- /dev/null +++ b/src/models/jawcoupling/schema.ts @@ -0,0 +1,83 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (v) => v > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(30), + length: positiveLength.default(40), + boreDiameter: positiveLength.default(8), + hubLength: positiveLength.default(12), + jawDepth: positiveLength.default(12), + jawCount: z.number().int().min(3).max(12).default(3), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.boreDiameter >= p.outerDiameter * 0.7) + context.addIssue({ + code: "custom", + message: "Bore must leave material in the hubs and jaws", + }) + if (2 * p.hubLength >= p.length) + context.addIssue({ + code: "custom", + message: "Hubs must leave a central jaw region", + }) + if ( + p.jawDepth <= (p.length - 2 * p.hubLength) / 2 || + p.jawDepth > p.length - 2 * p.hubLength + ) + context.addIssue({ + code: "custom", + message: + "Opposing jaws must engage in the central region without reaching the opposite hub", + }) + const bounds = [ + p.outerDiameter, + p.length, + p.boreDiameter, + p.hubLength, + p.jawDepth, + ] + if (!Number.isFinite(bounds.reduce((sum, value) => sum + value, 0))) + context.addIssue({ + code: "custom", + message: "Combined dimensions must remain finite", + }) +} +export const jawCouplingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const jawCouplingModelDefinitionSchema = z + .object({ fn: z.literal("jawcoupling"), ...shape }) + .strict() + .superRefine(validate) +export type JawCouplingModelPropsInput = z.input< + typeof jawCouplingModelPropsSchema +> +export type JawCouplingModelProps = z.output +export type JawCouplingModelDefinition = z.output< + typeof jawCouplingModelDefinitionSchema +> +/** Two coaxial hubs with interleaved drive jaws and a separate six-or-more-arm spider. Shafts run along +Z; bottom hub starts at Z=0. All lengths normalize to millimeters. */ +export function getJawCouplingDimensions( + input: JawCouplingModelPropsInput = {}, +) { + const p = jawCouplingModelPropsSchema.parse(input) + return { + ...p, + size: [p.outerDiameter, p.outerDiameter, p.length] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.length, + centralLength: p.length - 2 * p.hubLength, + jawInnerRadius: (p.boreDiameter + p.outerDiameter) / 4, + } +} diff --git a/src/models/levelingfoot/index.ts b/src/models/levelingfoot/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/levelingfoot/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/levelingfoot/parse-model-string.ts b/src/models/levelingfoot/parse-model-string.ts new file mode 100644 index 0000000..aae13a8 --- /dev/null +++ b/src/models/levelingfoot/parse-model-string.ts @@ -0,0 +1,19 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { levelingFootModelDefinitionSchema } from "./schema" +const aliases = { + basediameter: "baseDiameter", + baseheight: "baseHeight", + stemdiameter: "stemDiameter", + stemlength: "stemLength", + hexdiameter: "hexDiameter", + hexheight: "hexHeight", +} as const +export function parseLevelingFootModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "levelingfoot", + levelingFootModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/levelingfoot/register.ts b/src/models/levelingfoot/register.ts new file mode 100644 index 0000000..dfbaf3a --- /dev/null +++ b/src/models/levelingfoot/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { levelingFootModelDefinitionSchema } from "./schema" +import { parseLevelingFootModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "levelingfoot", + schema: levelingFootModelDefinitionSchema, + parse: parseLevelingFootModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/levelingfoot/schema.ts b/src/models/levelingfoot/schema.ts new file mode 100644 index 0000000..dc472b6 --- /dev/null +++ b/src/models/levelingfoot/schema.ts @@ -0,0 +1,62 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + baseDiameter: positiveLength.default(50), + baseHeight: positiveLength.default(12), + stemDiameter: positiveLength.default(12), + stemLength: positiveLength.default(55), + hexDiameter: positiveLength.default(24), + hexHeight: positiveLength.default(8), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.stemDiameter < p.hexDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid levelingfoot dimensions: stemDiameter < hexDiameter", + }) + if (!(p.hexDiameter < p.baseDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid levelingfoot dimensions: hexDiameter < baseDiameter", + }) + if (!(p.baseHeight < p.baseDiameter / 2)) + context.addIssue({ + code: "custom", + message: "Invalid levelingfoot dimensions: baseHeight < baseDiameter/2", + }) + if (!(p.hexHeight < p.stemLength / 2)) + context.addIssue({ + code: "custom", + message: "Invalid levelingfoot dimensions: hexHeight < stemLength/2", + }) +} +export const levelingFootModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const levelingFootModelDefinitionSchema = z + .object({ fn: z.literal("levelingfoot"), ...shape }) + .strict() + .superRefine(validate) +export type LevelingFootModelPropsInput = z.input< + typeof levelingFootModelPropsSchema +> +export type LevelingFootModelProps = z.output< + typeof levelingFootModelPropsSchema +> +export type LevelingFootModelDefinition = z.output< + typeof levelingFootModelDefinitionSchema +> +/** Leveling foot with puck, nominal smooth threaded-stem envelope and hex lock nut, axis Z. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getLevelingFootDimensions( + input: LevelingFootModelPropsInput = {}, +) { + return levelingFootModelPropsSchema.parse(input) +} diff --git a/src/models/leverhandle/index.ts b/src/models/leverhandle/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/leverhandle/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/leverhandle/parse-model-string.ts b/src/models/leverhandle/parse-model-string.ts new file mode 100644 index 0000000..ed614b6 --- /dev/null +++ b/src/models/leverhandle/parse-model-string.ts @@ -0,0 +1,23 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { leverHandleModelDefinitionSchema } from "./schema" +const aliases = { + length: "length", + width: "width", + height: "height", + hubdiameter: "hubDiameter", + hubheight: "hubHeight", + borediameter: "boreDiameter", + w: "width", + h: "height", + l: "length", + bore: "boreDiameter", +} as const +export function parseLeverHandleModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "leverhandle", + leverHandleModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/leverhandle/register.ts b/src/models/leverhandle/register.ts new file mode 100644 index 0000000..4a97379 --- /dev/null +++ b/src/models/leverhandle/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { leverHandleModelDefinitionSchema } from "./schema" +import { parseLeverHandleModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "leverhandle", + schema: leverHandleModelDefinitionSchema, + parse: parseLeverHandleModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/leverhandle/schema.ts b/src/models/leverhandle/schema.ts new file mode 100644 index 0000000..c8e67cd --- /dev/null +++ b/src/models/leverhandle/schema.ts @@ -0,0 +1,60 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + length: positiveLength.default(90), + width: positiveLength.default(18), + height: positiveLength.default(16), + hubDiameter: positiveLength.default(28), + hubHeight: positiveLength.default(25), + boreDiameter: positiveLength.default(8), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.length > 2 * p.hubDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid leverhandle dimensions: length > 2*hubDiameter", + }) + if (!(p.width < p.hubDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid leverhandle dimensions: width < hubDiameter", + }) + if (!(p.height <= p.hubHeight)) + context.addIssue({ + code: "custom", + message: "Invalid leverhandle dimensions: height <= hubHeight", + }) + if (!(p.boreDiameter < p.hubDiameter * 0.7)) + context.addIssue({ + code: "custom", + message: "Invalid leverhandle dimensions: boreDiameter < hubDiameter*.7", + }) +} +export const leverHandleModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const leverHandleModelDefinitionSchema = z + .object({ fn: z.literal("leverhandle"), ...shape }) + .strict() + .superRefine(validate) +export type LeverHandleModelPropsInput = z.input< + typeof leverHandleModelPropsSchema +> +export type LeverHandleModelProps = z.output +export type LeverHandleModelDefinition = z.output< + typeof leverHandleModelDefinitionSchema +> +/** Lever handle extending along +X from bored hub; bottom Z=0. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getLeverHandleDimensions( + input: LeverHandleModelPropsInput = {}, +) { + return leverHandleModelPropsSchema.parse(input) +} diff --git a/src/models/limitswitch/index.ts b/src/models/limitswitch/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/limitswitch/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/limitswitch/parse-model-string.ts b/src/models/limitswitch/parse-model-string.ts new file mode 100644 index 0000000..d98f849 --- /dev/null +++ b/src/models/limitswitch/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { limitSwitchModelDefinitionSchema } from "./schema" +const aliases = { + width: "width", + depth: "depth", + height: "height", + actuatorlength: "actuatorLength", + rollerdiameter: "rollerDiameter", + leverthickness: "leverThickness", + holediameter: "holeDiameter", + w: "width", + h: "height", +} as const +export function parseLimitSwitchModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "limitswitch", + limitSwitchModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/limitswitch/register.ts b/src/models/limitswitch/register.ts new file mode 100644 index 0000000..9cd74b4 --- /dev/null +++ b/src/models/limitswitch/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { limitSwitchModelDefinitionSchema } from "./schema" +import { parseLimitSwitchModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "limitswitch", + schema: limitSwitchModelDefinitionSchema, + parse: parseLimitSwitchModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/limitswitch/schema.ts b/src/models/limitswitch/schema.ts new file mode 100644 index 0000000..158592e --- /dev/null +++ b/src/models/limitswitch/schema.ts @@ -0,0 +1,68 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + width: positive.default(30), + depth: positive.default(20), + height: positive.default(55), + actuatorLength: positive.default(30), + rollerDiameter: positive.default(12), + leverThickness: positive.default(3), + holeDiameter: positive.default(4), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.rollerDiameter >= p.actuatorLength) + context.addIssue({ + code: "custom", + message: "Incompatible limitswitch dimensions: 1", + }) + if (p.leverThickness >= p.depth / 2) + context.addIssue({ + code: "custom", + message: "Incompatible limitswitch dimensions: 2", + }) + if (p.holeDiameter >= Math.min(p.width / 4, p.height * 0.3)) + context.addIssue({ + code: "custom", + message: "Incompatible limitswitch dimensions: 3", + }) +} +export const limitSwitchModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const limitSwitchModelDefinitionSchema = z + .object({ fn: z.literal("limitswitch"), ...shape }) + .strict() + .superRefine(validate) +export type LimitSwitchModelPropsInput = z.input< + typeof limitSwitchModelPropsSchema +> +export type LimitSwitchModelProps = z.output +export type LimitSwitchModelDefinition = z.output< + typeof limitSwitchModelDefinitionSchema +> +/** Industrial roller-lever limit switch with a transverse pivot, straight lever, end roller and two mounting bores. The lever is fixed in its nominal horizontal position; electrical contacts and motion are omitted. All normalized lengths are millimeters. */ +export function getLimitSwitchDimensions( + input: LimitSwitchModelPropsInput = {}, +) { + const p = limitSwitchModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.width / 2 + p.actuatorLength + p.rollerDiameter / 2, + p.depth + 2, + p.height + 8 + p.rollerDiameter / 2, + ] as [number, number, number], + } +} diff --git a/src/models/manifold/index.ts b/src/models/manifold/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/manifold/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/manifold/parse-model-string.ts b/src/models/manifold/parse-model-string.ts new file mode 100644 index 0000000..8dad607 --- /dev/null +++ b/src/models/manifold/parse-model-string.ts @@ -0,0 +1,28 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { manifoldModelDefinitionSchema } from "./schema" +export function parseManifoldModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "manifold", + manifoldModelDefinitionSchema, + { + l: "length", + w: "width", + h: "height", + bore: "boreDiameter", + port: "portDiameter", + pitch: "portPitch", + ports: "portCount", + hole: "mountingHoleDiameter", + length: "length", + width: "width", + height: "height", + borediameter: "boreDiameter", + portdiameter: "portDiameter", + portpitch: "portPitch", + portcount: "portCount", + mountingholediameter: "mountingHoleDiameter", + }, + ) +} diff --git a/src/models/manifold/register.ts b/src/models/manifold/register.ts new file mode 100644 index 0000000..03191ca --- /dev/null +++ b/src/models/manifold/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parseManifoldModelParams } from "./parse-model-string" +import { manifoldModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "manifold", + schema: manifoldModelDefinitionSchema, + parse: parseManifoldModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/manifold/schema.ts b/src/models/manifold/schema.ts new file mode 100644 index 0000000..68b4ce2 --- /dev/null +++ b/src/models/manifold/schema.ts @@ -0,0 +1,77 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + length: positiveLength.default(100), + width: positiveLength.default(30), + height: positiveLength.default(26), + boreDiameter: positiveLength.default(12), + portDiameter: positiveLength.default(8), + portPitch: positiveLength.default(25), + portCount: z.number().int().min(2).max(12).default(3), + mountingHoleDiameter: positiveLength.default(5), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.width)) issue("Requires boreDiameter < width") + if (!(p.boreDiameter < p.height)) issue("Requires boreDiameter < height") + if (!(p.portDiameter < p.boreDiameter)) + issue("Requires portDiameter < boreDiameter") + if (!(p.portDiameter < p.portPitch)) + issue("Requires portDiameter < portPitch") + if (!(p.portPitch * (p.portCount - 1) + p.portDiameter < p.length)) + issue("Requires portPitch * (portCount - 1) + portDiameter < length") + if (!(p.mountingHoleDiameter * 3 + p.boreDiameter < p.width)) + issue("Requires mountingHoleDiameter * 2 + boreDiameter < width") + if (!(p.mountingHoleDiameter * 2 < p.portPitch)) + issue("Requires mountingHoleDiameter * 2 < portPitch") + if (p.mountingHoleDiameter * 4 >= p.length) + issue("Mounting holes must remain within the manifold end faces") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.width - p.boreDiameter, + p.height - p.boreDiameter, + p.boreDiameter - p.portDiameter, + p.portPitch - p.portDiameter, + p.length - (p.portPitch * (p.portCount - 1) + p.portDiameter), + p.width - (p.mountingHoleDiameter * 2 + p.boreDiameter), + p.portPitch - p.mountingHoleDiameter * 2, + p.width - p.boreDiameter - p.mountingHoleDiameter * 3, + p.length - p.mountingHoleDiameter * 4, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const manifoldModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const manifoldModelDefinitionSchema = z + .object({ fn: z.literal("manifold"), ...shape }) + .strict() + .superRefine(validate) +export type ManifoldModelPropsInput = z.input +export type ManifoldModelProps = z.output +export type ManifoldModelDefinition = z.output< + typeof manifoldModelDefinitionSchema +> +export function getManifoldDimensions(input: ManifoldModelPropsInput = {}) { + return manifoldModelPropsSchema.parse(input) +} diff --git a/src/models/needlebearing/index.ts b/src/models/needlebearing/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/needlebearing/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/needlebearing/parse-model-string.ts b/src/models/needlebearing/parse-model-string.ts new file mode 100644 index 0000000..19d8e78 --- /dev/null +++ b/src/models/needlebearing/parse-model-string.ts @@ -0,0 +1,23 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { needleBearingModelDefinitionSchema } from "./schema" +const aliases = { + id: "innerDiameter", + od: "outerDiameter", + w: "width", + race: "raceThickness", + rollers: "rollerCount", + innerdiameter: "innerDiameter", + outerdiameter: "outerDiameter", + width: "width", + racethickness: "raceThickness", + rollercount: "rollerCount", +} as const +export function parseNeedleBearingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "needlebearing", + needleBearingModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/needlebearing/register.ts b/src/models/needlebearing/register.ts new file mode 100644 index 0000000..3467921 --- /dev/null +++ b/src/models/needlebearing/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { needleBearingModelDefinitionSchema } from "./schema" +import { parseNeedleBearingModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "needlebearing", + schema: needleBearingModelDefinitionSchema, + parse: parseNeedleBearingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/needlebearing/schema.ts b/src/models/needlebearing/schema.ts new file mode 100644 index 0000000..49afeb3 --- /dev/null +++ b/src/models/needlebearing/schema.ts @@ -0,0 +1,70 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + innerDiameter: positiveLength.default(20), + outerDiameter: positiveLength.default(30), + width: positiveLength.default(20), + raceThickness: positiveLength.default(1.5), + rollerCount: z.number().int().min(2).max(64).default(18), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.outerDiameter - p.innerDiameter <= 4 * p.raceThickness) + context.addIssue({ + code: "custom", + message: "The races must leave a positive radial roller gap", + }) + if (p.width <= p.raceThickness * 2) + context.addIssue({ + code: "custom", + message: "Width must exceed twice the race thickness", + }) + if ( + ((p.outerDiameter - p.innerDiameter) / 2 - 2 * p.raceThickness) * 0.85 >= + ((p.outerDiameter + p.innerDiameter) / 2) * + Math.sin(Math.PI / p.rollerCount) + ) + context.addIssue({ + code: "custom", + message: "Adjacent needles must have positive circumferential clearance", + }) +} +export const needleBearingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const needleBearingModelDefinitionSchema = z + .object({ fn: z.literal("needlebearing"), ...shape }) + .strict() + .superRefine(validate) +export type NeedleBearingModelPropsInput = z.input< + typeof needleBearingModelPropsSchema +> +export type NeedleBearingModelProps = z.output< + typeof needleBearingModelPropsSchema +> +export type NeedleBearingModelDefinition = z.output< + typeof needleBearingModelDefinitionSchema +> +/** Open straight-needle radial bearing. Axis is Z and lower face is Z=0. Inner and outer races enclose separately visible axial needles. Roller sizes/clearances are nominal and do not define a bearing standard or rating. All lengths are normalized to millimeters. */ +export function getNeedleBearingDimensions( + input: NeedleBearingModelPropsInput = {}, +) { + const p = needleBearingModelPropsSchema.parse(input) + return { + ...p, + size: [p.outerDiameter, p.outerDiameter, p.width] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.width, + } +} diff --git a/src/models/needlevalve/index.ts b/src/models/needlevalve/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/needlevalve/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/needlevalve/parse-model-string.ts b/src/models/needlevalve/parse-model-string.ts new file mode 100644 index 0000000..e95a2c1 --- /dev/null +++ b/src/models/needlevalve/parse-model-string.ts @@ -0,0 +1,30 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { needleValveModelDefinitionSchema } from "./schema" +export function parseNeedleValveModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "needlevalve", + needleValveModelDefinitionSchema, + { + bodyl: "bodyLength", + bodyw: "bodyWidth", + bodyh: "bodyHeight", + bore: "boreDiameter", + port: "portDiameter", + portl: "portLength", + stemh: "stemHeight", + wheel: "wheelDiameter", + wheelt: "wheelThickness", + bodylength: "bodyLength", + bodywidth: "bodyWidth", + bodyheight: "bodyHeight", + borediameter: "boreDiameter", + portdiameter: "portDiameter", + portlength: "portLength", + stemheight: "stemHeight", + wheeldiameter: "wheelDiameter", + wheelthickness: "wheelThickness", + }, + ) +} diff --git a/src/models/needlevalve/register.ts b/src/models/needlevalve/register.ts new file mode 100644 index 0000000..1fbc8ba --- /dev/null +++ b/src/models/needlevalve/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parseNeedleValveModelParams } from "./parse-model-string" +import { needleValveModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "needlevalve", + schema: needleValveModelDefinitionSchema, + parse: parseNeedleValveModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/needlevalve/schema.ts b/src/models/needlevalve/schema.ts new file mode 100644 index 0000000..c53e05d --- /dev/null +++ b/src/models/needlevalve/schema.ts @@ -0,0 +1,74 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + bodyLength: positiveLength.default(38), + bodyWidth: positiveLength.default(24), + bodyHeight: positiveLength.default(26), + boreDiameter: positiveLength.default(8), + portDiameter: positiveLength.default(16), + portLength: positiveLength.default(10), + stemHeight: positiveLength.default(24), + wheelDiameter: positiveLength.default(26), + wheelThickness: positiveLength.default(5), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.portDiameter)) + issue("Requires boreDiameter < portDiameter") + if (!(p.portDiameter < p.bodyWidth)) + issue("Requires portDiameter < bodyWidth") + if (!(p.portDiameter < p.bodyHeight)) + issue("Requires portDiameter < bodyHeight") + if (!(p.wheelThickness < p.stemHeight)) + issue("Requires wheelThickness < stemHeight") + if (!(p.wheelDiameter > p.portDiameter)) + issue("Requires wheelDiameter > portDiameter") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.portDiameter - p.boreDiameter, + p.bodyWidth - p.portDiameter, + p.bodyHeight - p.portDiameter, + p.stemHeight - p.wheelThickness, + p.wheelDiameter - p.portDiameter, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const needleValveModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const needleValveModelDefinitionSchema = z + .object({ fn: z.literal("needlevalve"), ...shape }) + .strict() + .superRefine(validate) +export type NeedleValveModelPropsInput = z.input< + typeof needleValveModelPropsSchema +> +export type NeedleValveModelProps = z.output +export type NeedleValveModelDefinition = z.output< + typeof needleValveModelDefinitionSchema +> +export function getNeedleValveDimensions( + input: NeedleValveModelPropsInput = {}, +) { + return needleValveModelPropsSchema.parse(input) +} diff --git a/src/models/oldhamcoupling/index.ts b/src/models/oldhamcoupling/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/oldhamcoupling/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/oldhamcoupling/parse-model-string.ts b/src/models/oldhamcoupling/parse-model-string.ts new file mode 100644 index 0000000..1d46b76 --- /dev/null +++ b/src/models/oldhamcoupling/parse-model-string.ts @@ -0,0 +1,26 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { oldhamCouplingModelDefinitionSchema } from "./schema" +const aliases = { + od: "outerDiameter", + l: "length", + bore: "boreDiameter", + hubl: "hubLength", + slotw: "slotWidth", + slotd: "slotDepth", + clearance: "clearance", + outerdiameter: "outerDiameter", + length: "length", + borediameter: "boreDiameter", + hublength: "hubLength", + slotwidth: "slotWidth", + slotdepth: "slotDepth", +} as const +export function parseOldhamCouplingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "oldhamcoupling", + oldhamCouplingModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/oldhamcoupling/register.ts b/src/models/oldhamcoupling/register.ts new file mode 100644 index 0000000..85ebb87 --- /dev/null +++ b/src/models/oldhamcoupling/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { oldhamCouplingModelDefinitionSchema } from "./schema" +import { parseOldhamCouplingModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "oldhamcoupling", + schema: oldhamCouplingModelDefinitionSchema, + parse: parseOldhamCouplingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/oldhamcoupling/schema.ts b/src/models/oldhamcoupling/schema.ts new file mode 100644 index 0000000..754466e --- /dev/null +++ b/src/models/oldhamcoupling/schema.ts @@ -0,0 +1,93 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (v) => v > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(30), + length: positiveLength.default(36), + boreDiameter: positiveLength.default(8), + hubLength: positiveLength.default(12), + slotWidth: positiveLength.default(7), + slotDepth: positiveLength.default(3), + clearance: industrialLengthSchema + .refine((v) => v >= 0, "Clearance cannot be negative") + .default(0.2), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.boreDiameter >= p.outerDiameter * 0.6) + context.addIssue({ + code: "custom", + message: "Bore must leave material in the center disk", + }) + if (p.length - 2 * p.hubLength <= 2 * p.clearance) + context.addIssue({ + code: "custom", + message: "Hubs must leave room for a positive thickness center disk", + }) + if (p.slotWidth >= p.outerDiameter * 0.7 || p.slotWidth <= 2 * p.clearance) + context.addIssue({ + code: "custom", + message: "Slot width must leave hub walls and positive tongue width", + }) + if (p.slotDepth >= p.hubLength || p.slotDepth <= 2 * p.clearance) + context.addIssue({ + code: "custom", + message: + "Slot depth must leave a hub floor and positive tongue engagement", + }) + const bounds = [ + p.outerDiameter, + p.length, + p.boreDiameter, + p.hubLength, + p.slotWidth, + p.slotDepth, + ] + if (!Number.isFinite(bounds.reduce((sum, value) => sum + value, 0))) + context.addIssue({ + code: "custom", + message: "Combined dimensions must remain finite", + }) +} +export const oldhamCouplingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const oldhamCouplingModelDefinitionSchema = z + .object({ fn: z.literal("oldhamcoupling"), ...shape }) + .strict() + .superRefine(validate) +export type OldhamCouplingModelPropsInput = z.input< + typeof oldhamCouplingModelPropsSchema +> +export type OldhamCouplingModelProps = z.output< + typeof oldhamCouplingModelPropsSchema +> +export type OldhamCouplingModelDefinition = z.output< + typeof oldhamCouplingModelDefinitionSchema +> +/** Two coaxial slotted hubs and a separate center disk with orthogonal tongues. The two face slots and disk tongues engage along X and Y respectively; bottom datum is Z=0. All lengths normalize to millimeters. */ +export function getOldhamCouplingDimensions( + input: OldhamCouplingModelPropsInput = {}, +) { + const p = oldhamCouplingModelPropsSchema.parse(input) + return { + ...p, + size: [p.outerDiameter, p.outerDiameter, p.length] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.length, + diskThickness: p.length - 2 * p.hubLength - 2 * p.clearance, + diskDiameter: p.outerDiameter * 0.88, + tongueWidth: p.slotWidth - 2 * p.clearance, + tongueHeight: p.slotDepth - p.clearance, + } +} diff --git a/src/models/photoelectricsensor/index.ts b/src/models/photoelectricsensor/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/photoelectricsensor/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/photoelectricsensor/parse-model-string.ts b/src/models/photoelectricsensor/parse-model-string.ts new file mode 100644 index 0000000..381f030 --- /dev/null +++ b/src/models/photoelectricsensor/parse-model-string.ts @@ -0,0 +1,23 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { photoelectricSensorModelDefinitionSchema } from "./schema" +const aliases = { + width: "width", + depth: "depth", + height: "height", + lensdiameter: "lensDiameter", + cablediameter: "cableDiameter", + mountholediameter: "mountHoleDiameter", + w: "width", + h: "height", +} as const +export function parsePhotoelectricSensorModelParams( + raw: RawModelprinterParams, +) { + return parseIndustrialModelParams( + raw, + "photoelectricsensor", + photoelectricSensorModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/photoelectricsensor/register.ts b/src/models/photoelectricsensor/register.ts new file mode 100644 index 0000000..1e85e69 --- /dev/null +++ b/src/models/photoelectricsensor/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { photoelectricSensorModelDefinitionSchema } from "./schema" +import { parsePhotoelectricSensorModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "photoelectricsensor", + schema: photoelectricSensorModelDefinitionSchema, + parse: parsePhotoelectricSensorModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/photoelectricsensor/schema.ts b/src/models/photoelectricsensor/schema.ts new file mode 100644 index 0000000..d59480b --- /dev/null +++ b/src/models/photoelectricsensor/schema.ts @@ -0,0 +1,70 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + width: positive.default(20), + depth: positive.default(35), + height: positive.default(55), + lensDiameter: positive.default(12), + cableDiameter: positive.default(5), + mountHoleDiameter: positive.default(4), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.lensDiameter >= p.width) + context.addIssue({ + code: "custom", + message: "Incompatible photoelectricsensor dimensions: 1", + }) + if (p.lensDiameter >= p.height * 0.3) + context.addIssue({ + code: "custom", + message: "Incompatible photoelectricsensor dimensions: 2", + }) + if (p.cableDiameter >= p.width / 2) + context.addIssue({ + code: "custom", + message: "Incompatible photoelectricsensor dimensions: 3", + }) + if (p.mountHoleDiameter >= Math.min(p.width, p.depth) / 3) + context.addIssue({ + code: "custom", + message: "Incompatible photoelectricsensor dimensions: 4", + }) +} +export const photoelectricSensorModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const photoelectricSensorModelDefinitionSchema = z + .object({ fn: z.literal("photoelectricsensor"), ...shape }) + .strict() + .superRefine(validate) +export type PhotoelectricSensorModelPropsInput = z.input< + typeof photoelectricSensorModelPropsSchema +> +export type PhotoelectricSensorModelProps = z.output< + typeof photoelectricSensorModelPropsSchema +> +export type PhotoelectricSensorModelDefinition = z.output< + typeof photoelectricSensorModelDefinitionSchema +> +/** Rectangular photoelectric sensor with two projecting optical lenses facing -Y, a top status indicator, cable tail and two transverse mounting holes. Optical axes are nominal and no sensing range is implied. All normalized lengths are millimeters. */ +export function getPhotoelectricSensorDimensions( + input: PhotoelectricSensorModelPropsInput = {}, +) { + const p = photoelectricSensorModelPropsSchema.parse(input) + return { + ...p, + size: [p.width, p.depth + 2.5, p.height + 11] as [number, number, number], + } +} diff --git a/src/models/pillowblockbearing/index.ts b/src/models/pillowblockbearing/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pillowblockbearing/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pillowblockbearing/parse-model-string.ts b/src/models/pillowblockbearing/parse-model-string.ts new file mode 100644 index 0000000..ce087ec --- /dev/null +++ b/src/models/pillowblockbearing/parse-model-string.ts @@ -0,0 +1,29 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pillowBlockBearingModelDefinitionSchema } from "./schema" +const aliases = { + bore: "boreDiameter", + bw: "baseWidth", + bd: "baseDepth", + bt: "baseThickness", + hd: "housingDiameter", + ch: "centerHeight", + ms: "mountSpacing", + mh: "mountHoleDiameter", + borediameter: "boreDiameter", + basewidth: "baseWidth", + basedepth: "baseDepth", + basethickness: "baseThickness", + housingdiameter: "housingDiameter", + centerheight: "centerHeight", + mountspacing: "mountSpacing", + mountholediameter: "mountHoleDiameter", +} as const +export function parsePillowBlockBearingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pillowblockbearing", + pillowBlockBearingModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/pillowblockbearing/register.ts b/src/models/pillowblockbearing/register.ts new file mode 100644 index 0000000..17db02f --- /dev/null +++ b/src/models/pillowblockbearing/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { pillowBlockBearingModelDefinitionSchema } from "./schema" +import { parsePillowBlockBearingModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "pillowblockbearing", + schema: pillowBlockBearingModelDefinitionSchema, + parse: parsePillowBlockBearingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pillowblockbearing/schema.ts b/src/models/pillowblockbearing/schema.ts new file mode 100644 index 0000000..6dc7d5c --- /dev/null +++ b/src/models/pillowblockbearing/schema.ts @@ -0,0 +1,97 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + boreDiameter: positiveLength.default(20), + baseWidth: positiveLength.default(80), + baseDepth: positiveLength.default(30), + baseThickness: positiveLength.default(8), + housingDiameter: positiveLength.default(46), + centerHeight: positiveLength.default(30), + mountSpacing: positiveLength.default(60), + mountHoleDiameter: positiveLength.default(6), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.housingDiameter <= p.boreDiameter + 12) + context.addIssue({ + code: "custom", + message: "Housing must leave space around the shaft for races and balls", + }) + if (p.centerHeight <= p.baseThickness + p.boreDiameter / 2) + context.addIssue({ + code: "custom", + message: "Shaft bore must clear the mounting foot", + }) + if (p.mountSpacing + p.mountHoleDiameter >= p.baseWidth) + context.addIssue({ + code: "custom", + message: "Mounting holes must remain within the foot", + }) + if (p.mountSpacing - p.mountHoleDiameter <= p.housingDiameter) + context.addIssue({ + code: "custom", + message: "Mounting holes must clear the bearing housing", + }) + if (p.baseDepth <= p.mountHoleDiameter) + context.addIssue({ + code: "custom", + message: "Mounting-hole diameter must be less than foot depth", + }) + if ( + (((p.housingDiameter - p.boreDiameter) * 0.62 - 6) * 0.85) / 2 >= + p.baseDepth + ) + context.addIssue({ + code: "custom", + message: "Ball diameter must fit within housing depth", + }) + if ( + (((p.housingDiameter - p.boreDiameter) * 0.62 - 6) * 0.85) / 2 >= + ((2 * p.boreDiameter + (p.housingDiameter - p.boreDiameter) * 0.62) / 2) * + Math.sin(Math.PI / 10) + ) + context.addIssue({ + code: "custom", + message: + "Adjacent bearing balls must have positive circumferential clearance", + }) +} +export const pillowBlockBearingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pillowBlockBearingModelDefinitionSchema = z + .object({ fn: z.literal("pillowblockbearing"), ...shape }) + .strict() + .superRefine(validate) +export type PillowBlockBearingModelPropsInput = z.input< + typeof pillowBlockBearingModelPropsSchema +> +export type PillowBlockBearingModelProps = z.output< + typeof pillowBlockBearingModelPropsSchema +> +export type PillowBlockBearingModelDefinition = z.output< + typeof pillowBlockBearingModelDefinitionSchema +> +/** Mounted radial bearing in a raised pillow housing. Shaft axis is Y; the mounting-foot bottom is Z=0. Open front race shows rolling elements. Clearance, cage and grease hardware are nominal; no load rating is implied. All lengths are normalized to millimeters. */ +export function getPillowBlockBearingDimensions( + input: PillowBlockBearingModelPropsInput = {}, +) { + const p = pillowBlockBearingModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.baseWidth, + p.baseDepth + 2, + p.centerHeight + p.housingDiameter / 2, + ] as [number, number, number], + bottomZ: 0, + topZ: p.centerHeight + p.housingDiameter / 2, + } +} diff --git a/src/models/pilotlight/index.ts b/src/models/pilotlight/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pilotlight/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pilotlight/parse-model-string.ts b/src/models/pilotlight/parse-model-string.ts new file mode 100644 index 0000000..b203637 --- /dev/null +++ b/src/models/pilotlight/parse-model-string.ts @@ -0,0 +1,17 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pilotLightModelDefinitionSchema } from "./schema" +const aliases = { + paneldiameter: "panelDiameter", + lensdiameter: "lensDiameter", + bodylength: "bodyLength", + lensheight: "lensHeight", +} as const +export function parsePilotLightModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pilotlight", + pilotLightModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/pilotlight/register.ts b/src/models/pilotlight/register.ts new file mode 100644 index 0000000..0eb60fd --- /dev/null +++ b/src/models/pilotlight/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { pilotLightModelDefinitionSchema } from "./schema" +import { parsePilotLightModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "pilotlight", + schema: pilotLightModelDefinitionSchema, + parse: parsePilotLightModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pilotlight/schema.ts b/src/models/pilotlight/schema.ts new file mode 100644 index 0000000..e0df1cf --- /dev/null +++ b/src/models/pilotlight/schema.ts @@ -0,0 +1,58 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + panelDiameter: positive.default(22), + lensDiameter: positive.default(28), + bodyLength: positive.default(28), + lensHeight: positive.default(8), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.lensDiameter <= p.panelDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible pilotlight dimensions: 1", + }) + if (p.lensHeight >= p.bodyLength) + context.addIssue({ + code: "custom", + message: "Incompatible pilotlight dimensions: 2", + }) +} +export const pilotLightModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pilotLightModelDefinitionSchema = z + .object({ fn: z.literal("pilotlight"), ...shape }) + .strict() + .superRefine(validate) +export type PilotLightModelPropsInput = z.input< + typeof pilotLightModelPropsSchema +> +export type PilotLightModelProps = z.output +export type PilotLightModelDefinition = z.output< + typeof pilotLightModelDefinitionSchema +> +/** Panel indicator light with raised lens, surrounding bezel, mounting barrel and two rear terminal tabs. Optical diffusion and illumination are not simulated; no electrical ratings or mounting thread are asserted. All normalized lengths are millimeters. */ +export function getPilotLightDimensions(input: PilotLightModelPropsInput = {}) { + const p = pilotLightModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.lensDiameter + 3, + p.lensDiameter + 3, + p.bodyLength + p.lensHeight + 6, + ] as [number, number, number], + } +} diff --git a/src/models/pipecap/index.ts b/src/models/pipecap/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pipecap/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pipecap/parse-model-string.ts b/src/models/pipecap/parse-model-string.ts new file mode 100644 index 0000000..1600bd7 --- /dev/null +++ b/src/models/pipecap/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pipeCapModelDefinitionSchema } from "./schema" +export function parsePipeCapModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pipecap", + pipeCapModelDefinitionSchema, + { + od: "outerDiameter", + socket: "socketDiameter", + l: "length", + floor: "floorThickness", + af: "hexAcrossFlats", + outerdiameter: "outerDiameter", + socketdiameter: "socketDiameter", + length: "length", + floorthickness: "floorThickness", + hexacrossflats: "hexAcrossFlats", + }, + ) +} diff --git a/src/models/pipecap/register.ts b/src/models/pipecap/register.ts new file mode 100644 index 0000000..b7e2d42 --- /dev/null +++ b/src/models/pipecap/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parsePipeCapModelParams } from "./parse-model-string" +import { pipeCapModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "pipecap", + schema: pipeCapModelDefinitionSchema, + parse: parsePipeCapModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pipecap/schema.ts b/src/models/pipecap/schema.ts new file mode 100644 index 0000000..20ff2a2 --- /dev/null +++ b/src/models/pipecap/schema.ts @@ -0,0 +1,59 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(30), + socketDiameter: positiveLength.default(20), + length: positiveLength.default(25), + floorThickness: positiveLength.default(4), + hexAcrossFlats: positiveLength.default(32), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.socketDiameter < p.outerDiameter)) + issue("Requires socketDiameter < outerDiameter") + if (!(p.outerDiameter < p.hexAcrossFlats)) + issue("Requires outerDiameter < hexAcrossFlats") + if (!(p.floorThickness < p.length)) issue("Requires floorThickness < length") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.outerDiameter - p.socketDiameter, + p.hexAcrossFlats - p.outerDiameter, + p.length - p.floorThickness, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const pipeCapModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pipeCapModelDefinitionSchema = z + .object({ fn: z.literal("pipecap"), ...shape }) + .strict() + .superRefine(validate) +export type PipeCapModelPropsInput = z.input +export type PipeCapModelProps = z.output +export type PipeCapModelDefinition = z.output< + typeof pipeCapModelDefinitionSchema +> +export function getPipeCapDimensions(input: PipeCapModelPropsInput = {}) { + return pipeCapModelPropsSchema.parse(input) +} diff --git a/src/models/pipecoupling/index.ts b/src/models/pipecoupling/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pipecoupling/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pipecoupling/parse-model-string.ts b/src/models/pipecoupling/parse-model-string.ts new file mode 100644 index 0000000..953b90c --- /dev/null +++ b/src/models/pipecoupling/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pipeCouplingModelDefinitionSchema } from "./schema" +export function parsePipeCouplingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pipecoupling", + pipeCouplingModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + socket: "socketDiameter", + l: "length", + depth: "socketDepth", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + socketdiameter: "socketDiameter", + length: "length", + socketdepth: "socketDepth", + }, + ) +} diff --git a/src/models/pipecoupling/register.ts b/src/models/pipecoupling/register.ts new file mode 100644 index 0000000..9b51041 --- /dev/null +++ b/src/models/pipecoupling/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parsePipeCouplingModelParams } from "./parse-model-string" +import { pipeCouplingModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "pipecoupling", + schema: pipeCouplingModelDefinitionSchema, + parse: parsePipeCouplingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pipecoupling/schema.ts b/src/models/pipecoupling/schema.ts new file mode 100644 index 0000000..3b9226a --- /dev/null +++ b/src/models/pipecoupling/schema.ts @@ -0,0 +1,66 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(30), + boreDiameter: positiveLength.default(16), + socketDiameter: positiveLength.default(20), + length: positiveLength.default(40), + socketDepth: positiveLength.default(14), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.socketDiameter)) + issue("Requires boreDiameter < socketDiameter") + if (!(p.socketDiameter < p.outerDiameter)) + issue("Requires socketDiameter < outerDiameter") + if (!(p.socketDepth * 2 < p.length)) + issue("Requires socketDepth * 2 < length") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.socketDiameter - p.boreDiameter, + p.outerDiameter - p.socketDiameter, + p.length - p.socketDepth * 2, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const pipeCouplingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pipeCouplingModelDefinitionSchema = z + .object({ fn: z.literal("pipecoupling"), ...shape }) + .strict() + .superRefine(validate) +export type PipeCouplingModelPropsInput = z.input< + typeof pipeCouplingModelPropsSchema +> +export type PipeCouplingModelProps = z.output< + typeof pipeCouplingModelPropsSchema +> +export type PipeCouplingModelDefinition = z.output< + typeof pipeCouplingModelDefinitionSchema +> +export function getPipeCouplingDimensions( + input: PipeCouplingModelPropsInput = {}, +) { + return pipeCouplingModelPropsSchema.parse(input) +} diff --git a/src/models/pipecross/index.ts b/src/models/pipecross/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pipecross/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pipecross/parse-model-string.ts b/src/models/pipecross/parse-model-string.ts new file mode 100644 index 0000000..d789d20 --- /dev/null +++ b/src/models/pipecross/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pipeCrossModelDefinitionSchema } from "./schema" +export function parsePipeCrossModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pipecross", + pipeCrossModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + span: "span", + socket: "socketDiameter", + socketl: "socketLength", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + socketdiameter: "socketDiameter", + socketlength: "socketLength", + }, + ) +} diff --git a/src/models/pipecross/register.ts b/src/models/pipecross/register.ts new file mode 100644 index 0000000..ad08957 --- /dev/null +++ b/src/models/pipecross/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parsePipeCrossModelParams } from "./parse-model-string" +import { pipeCrossModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "pipecross", + schema: pipeCrossModelDefinitionSchema, + parse: parsePipeCrossModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pipecross/schema.ts b/src/models/pipecross/schema.ts new file mode 100644 index 0000000..0bf3563 --- /dev/null +++ b/src/models/pipecross/schema.ts @@ -0,0 +1,62 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(24), + boreDiameter: positiveLength.default(16), + span: positiveLength.default(70), + socketDiameter: positiveLength.default(28), + socketLength: positiveLength.default(10), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.outerDiameter)) + issue("Requires boreDiameter < outerDiameter") + if (!(p.outerDiameter < p.socketDiameter)) + issue("Requires outerDiameter < socketDiameter") + if (!(p.socketLength * 2 < p.span)) issue("Requires socketLength * 2 < span") + if (!(p.outerDiameter * 2 < p.span)) + issue("Requires outerDiameter * 2 < span") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.outerDiameter - p.boreDiameter, + p.socketDiameter - p.outerDiameter, + p.span - p.socketLength * 2, + p.span - p.outerDiameter * 2, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const pipeCrossModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pipeCrossModelDefinitionSchema = z + .object({ fn: z.literal("pipecross"), ...shape }) + .strict() + .superRefine(validate) +export type PipeCrossModelPropsInput = z.input +export type PipeCrossModelProps = z.output +export type PipeCrossModelDefinition = z.output< + typeof pipeCrossModelDefinitionSchema +> +export function getPipeCrossDimensions(input: PipeCrossModelPropsInput = {}) { + return pipeCrossModelPropsSchema.parse(input) +} diff --git a/src/models/pipeelbow/index.ts b/src/models/pipeelbow/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pipeelbow/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pipeelbow/parse-model-string.ts b/src/models/pipeelbow/parse-model-string.ts new file mode 100644 index 0000000..2138932 --- /dev/null +++ b/src/models/pipeelbow/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pipeElbowModelDefinitionSchema } from "./schema" +export function parsePipeElbowModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pipeelbow", + pipeElbowModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + leg: "legLength", + socket: "socketDiameter", + socketl: "socketLength", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + leglength: "legLength", + socketdiameter: "socketDiameter", + socketlength: "socketLength", + }, + ) +} diff --git a/src/models/pipeelbow/register.ts b/src/models/pipeelbow/register.ts new file mode 100644 index 0000000..dd35e3d --- /dev/null +++ b/src/models/pipeelbow/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parsePipeElbowModelParams } from "./parse-model-string" +import { pipeElbowModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "pipeelbow", + schema: pipeElbowModelDefinitionSchema, + parse: parsePipeElbowModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pipeelbow/schema.ts b/src/models/pipeelbow/schema.ts new file mode 100644 index 0000000..d1b1661 --- /dev/null +++ b/src/models/pipeelbow/schema.ts @@ -0,0 +1,63 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(24), + boreDiameter: positiveLength.default(16), + legLength: positiveLength.default(36), + socketDiameter: positiveLength.default(28), + socketLength: positiveLength.default(10), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.outerDiameter)) + issue("Requires boreDiameter < outerDiameter") + if (!(p.outerDiameter < p.socketDiameter)) + issue("Requires outerDiameter < socketDiameter") + if (!(p.socketLength < p.legLength)) + issue("Requires socketLength < legLength") + if (!(p.outerDiameter < p.legLength)) + issue("Requires outerDiameter < legLength") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.outerDiameter - p.boreDiameter, + p.socketDiameter - p.outerDiameter, + p.legLength - p.socketLength, + p.legLength - p.outerDiameter, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const pipeElbowModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pipeElbowModelDefinitionSchema = z + .object({ fn: z.literal("pipeelbow"), ...shape }) + .strict() + .superRefine(validate) +export type PipeElbowModelPropsInput = z.input +export type PipeElbowModelProps = z.output +export type PipeElbowModelDefinition = z.output< + typeof pipeElbowModelDefinitionSchema +> +export function getPipeElbowDimensions(input: PipeElbowModelPropsInput = {}) { + return pipeElbowModelPropsSchema.parse(input) +} diff --git a/src/models/pipeflange/index.ts b/src/models/pipeflange/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pipeflange/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pipeflange/parse-model-string.ts b/src/models/pipeflange/parse-model-string.ts new file mode 100644 index 0000000..bdb2257 --- /dev/null +++ b/src/models/pipeflange/parse-model-string.ts @@ -0,0 +1,28 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pipeFlangeModelDefinitionSchema } from "./schema" +export function parsePipeFlangeModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pipeflange", + pipeFlangeModelDefinitionSchema, + { + flange: "flangeDiameter", + bore: "boreDiameter", + hub: "hubDiameter", + flanget: "flangeThickness", + hubl: "hubLength", + bc: "boltCircleDiameter", + hole: "boltHoleDiameter", + bolts: "boltCount", + flangediameter: "flangeDiameter", + borediameter: "boreDiameter", + hubdiameter: "hubDiameter", + flangethickness: "flangeThickness", + hublength: "hubLength", + boltcirclediameter: "boltCircleDiameter", + boltholediameter: "boltHoleDiameter", + boltcount: "boltCount", + }, + ) +} diff --git a/src/models/pipeflange/register.ts b/src/models/pipeflange/register.ts new file mode 100644 index 0000000..6edcc78 --- /dev/null +++ b/src/models/pipeflange/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parsePipeFlangeModelParams } from "./parse-model-string" +import { pipeFlangeModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "pipeflange", + schema: pipeFlangeModelDefinitionSchema, + parse: parsePipeFlangeModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pipeflange/schema.ts b/src/models/pipeflange/schema.ts new file mode 100644 index 0000000..3968f03 --- /dev/null +++ b/src/models/pipeflange/schema.ts @@ -0,0 +1,74 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + flangeDiameter: positiveLength.default(70), + boreDiameter: positiveLength.default(16), + hubDiameter: positiveLength.default(30), + flangeThickness: positiveLength.default(8), + hubLength: positiveLength.default(20), + boltCircleDiameter: positiveLength.default(50), + boltHoleDiameter: positiveLength.default(6), + boltCount: z.number().int().min(2).max(12).default(4), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.hubDiameter)) + issue("Requires boreDiameter < hubDiameter") + if (!(p.hubDiameter + p.boltHoleDiameter < p.boltCircleDiameter)) + issue("Requires hubDiameter + boltHoleDiameter < boltCircleDiameter") + if (!(p.boltCircleDiameter + p.boltHoleDiameter < p.flangeDiameter)) + issue("Requires boltCircleDiameter + boltHoleDiameter < flangeDiameter") + if ( + !( + p.boltHoleDiameter * 2 < + p.boltCircleDiameter * Math.sin(Math.PI / p.boltCount) + ) + ) + issue( + "Requires boltHoleDiameter * 2 < boltCircleDiameter * Math.sin(Math.PI / boltCount)", + ) + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.hubDiameter - p.boreDiameter, + p.boltCircleDiameter - (p.hubDiameter + p.boltHoleDiameter), + p.flangeDiameter - (p.boltCircleDiameter + p.boltHoleDiameter), + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const pipeFlangeModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pipeFlangeModelDefinitionSchema = z + .object({ fn: z.literal("pipeflange"), ...shape }) + .strict() + .superRefine(validate) +export type PipeFlangeModelPropsInput = z.input< + typeof pipeFlangeModelPropsSchema +> +export type PipeFlangeModelProps = z.output +export type PipeFlangeModelDefinition = z.output< + typeof pipeFlangeModelDefinitionSchema +> +export function getPipeFlangeDimensions(input: PipeFlangeModelPropsInput = {}) { + return pipeFlangeModelPropsSchema.parse(input) +} diff --git a/src/models/pipenipple/index.ts b/src/models/pipenipple/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pipenipple/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pipenipple/parse-model-string.ts b/src/models/pipenipple/parse-model-string.ts new file mode 100644 index 0000000..e2eb7ed --- /dev/null +++ b/src/models/pipenipple/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pipeNippleModelDefinitionSchema } from "./schema" +export function parsePipeNippleModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pipenipple", + pipeNippleModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + l: "length", + threadl: "threadLength", + shoulder: "shoulderDiameter", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + length: "length", + threadlength: "threadLength", + shoulderdiameter: "shoulderDiameter", + }, + ) +} diff --git a/src/models/pipenipple/register.ts b/src/models/pipenipple/register.ts new file mode 100644 index 0000000..e70a161 --- /dev/null +++ b/src/models/pipenipple/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parsePipeNippleModelParams } from "./parse-model-string" +import { pipeNippleModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "pipenipple", + schema: pipeNippleModelDefinitionSchema, + parse: parsePipeNippleModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pipenipple/schema.ts b/src/models/pipenipple/schema.ts new file mode 100644 index 0000000..5a920ab --- /dev/null +++ b/src/models/pipenipple/schema.ts @@ -0,0 +1,62 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(20), + boreDiameter: positiveLength.default(16), + length: positiveLength.default(50), + threadLength: positiveLength.default(12), + shoulderDiameter: positiveLength.default(22), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.outerDiameter)) + issue("Requires boreDiameter < outerDiameter") + if (!(p.outerDiameter < p.shoulderDiameter)) + issue("Requires outerDiameter < shoulderDiameter") + if (!(p.threadLength * 2 < p.length)) + issue("Requires threadLength * 2 < length") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.outerDiameter - p.boreDiameter, + p.shoulderDiameter - p.outerDiameter, + p.length - p.threadLength * 2, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const pipeNippleModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pipeNippleModelDefinitionSchema = z + .object({ fn: z.literal("pipenipple"), ...shape }) + .strict() + .superRefine(validate) +export type PipeNippleModelPropsInput = z.input< + typeof pipeNippleModelPropsSchema +> +export type PipeNippleModelProps = z.output +export type PipeNippleModelDefinition = z.output< + typeof pipeNippleModelDefinitionSchema +> +export function getPipeNippleDimensions(input: PipeNippleModelPropsInput = {}) { + return pipeNippleModelPropsSchema.parse(input) +} diff --git a/src/models/pipeplug/index.ts b/src/models/pipeplug/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pipeplug/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pipeplug/parse-model-string.ts b/src/models/pipeplug/parse-model-string.ts new file mode 100644 index 0000000..3009479 --- /dev/null +++ b/src/models/pipeplug/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pipePlugModelDefinitionSchema } from "./schema" +export function parsePipePlugModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pipeplug", + pipePlugModelDefinitionSchema, + { + d: "diameter", + l: "length", + af: "headAcrossFlats", + head: "headHeight", + diameter: "diameter", + length: "length", + headacrossflats: "headAcrossFlats", + headheight: "headHeight", + }, + ) +} diff --git a/src/models/pipeplug/register.ts b/src/models/pipeplug/register.ts new file mode 100644 index 0000000..cda5191 --- /dev/null +++ b/src/models/pipeplug/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parsePipePlugModelParams } from "./parse-model-string" +import { pipePlugModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "pipeplug", + schema: pipePlugModelDefinitionSchema, + parse: parsePipePlugModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pipeplug/schema.ts b/src/models/pipeplug/schema.ts new file mode 100644 index 0000000..0b08dd9 --- /dev/null +++ b/src/models/pipeplug/schema.ts @@ -0,0 +1,52 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + diameter: positiveLength.default(20), + length: positiveLength.default(20), + headAcrossFlats: positiveLength.default(26), + headHeight: positiveLength.default(8), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.diameter < p.headAcrossFlats)) + issue("Requires diameter < headAcrossFlats") + if (!(p.headHeight < p.length)) issue("Requires headHeight < length") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [p.headAcrossFlats - p.diameter, p.length - p.headHeight] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const pipePlugModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pipePlugModelDefinitionSchema = z + .object({ fn: z.literal("pipeplug"), ...shape }) + .strict() + .superRefine(validate) +export type PipePlugModelPropsInput = z.input +export type PipePlugModelProps = z.output +export type PipePlugModelDefinition = z.output< + typeof pipePlugModelDefinitionSchema +> +export function getPipePlugDimensions(input: PipePlugModelPropsInput = {}) { + return pipePlugModelPropsSchema.parse(input) +} diff --git a/src/models/pipetee/index.ts b/src/models/pipetee/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pipetee/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pipetee/parse-model-string.ts b/src/models/pipetee/parse-model-string.ts new file mode 100644 index 0000000..c3c5e2e --- /dev/null +++ b/src/models/pipetee/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pipeTeeModelDefinitionSchema } from "./schema" +export function parsePipeTeeModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pipetee", + pipeTeeModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + l: "length", + branch: "branchLength", + socket: "socketDiameter", + socketl: "socketLength", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + length: "length", + branchlength: "branchLength", + socketdiameter: "socketDiameter", + socketlength: "socketLength", + }, + ) +} diff --git a/src/models/pipetee/register.ts b/src/models/pipetee/register.ts new file mode 100644 index 0000000..852a9e7 --- /dev/null +++ b/src/models/pipetee/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parsePipeTeeModelParams } from "./parse-model-string" +import { pipeTeeModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "pipetee", + schema: pipeTeeModelDefinitionSchema, + parse: parsePipeTeeModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pipetee/schema.ts b/src/models/pipetee/schema.ts new file mode 100644 index 0000000..50f781c --- /dev/null +++ b/src/models/pipetee/schema.ts @@ -0,0 +1,70 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(24), + boreDiameter: positiveLength.default(16), + length: positiveLength.default(70), + branchLength: positiveLength.default(35), + socketDiameter: positiveLength.default(28), + socketLength: positiveLength.default(10), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.outerDiameter)) + issue("Requires boreDiameter < outerDiameter") + if (!(p.outerDiameter < p.socketDiameter)) + issue("Requires outerDiameter < socketDiameter") + if (!(p.socketLength * 2 < p.length)) + issue("Requires socketLength * 2 < length") + if (!(p.socketLength < p.branchLength)) + issue("Requires socketLength < branchLength") + if (!(p.outerDiameter < p.branchLength)) + issue("Requires outerDiameter < branchLength") + if (!(p.outerDiameter * 2 < p.length)) + issue("Requires outerDiameter * 2 < length") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.outerDiameter - p.boreDiameter, + p.socketDiameter - p.outerDiameter, + p.length - p.socketLength * 2, + p.branchLength - p.socketLength, + p.branchLength - p.outerDiameter, + p.length - p.outerDiameter * 2, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const pipeTeeModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pipeTeeModelDefinitionSchema = z + .object({ fn: z.literal("pipetee"), ...shape }) + .strict() + .superRefine(validate) +export type PipeTeeModelPropsInput = z.input +export type PipeTeeModelProps = z.output +export type PipeTeeModelDefinition = z.output< + typeof pipeTeeModelDefinitionSchema +> +export function getPipeTeeDimensions(input: PipeTeeModelPropsInput = {}) { + return pipeTeeModelPropsSchema.parse(input) +} diff --git a/src/models/pipeunion/index.ts b/src/models/pipeunion/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pipeunion/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pipeunion/parse-model-string.ts b/src/models/pipeunion/parse-model-string.ts new file mode 100644 index 0000000..995197a --- /dev/null +++ b/src/models/pipeunion/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pipeUnionModelDefinitionSchema } from "./schema" +export function parsePipeUnionModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pipeunion", + pipeUnionModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + l: "length", + af: "nutAcrossFlats", + nutl: "nutLength", + endod: "endDiameter", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + length: "length", + nutacrossflats: "nutAcrossFlats", + nutlength: "nutLength", + enddiameter: "endDiameter", + }, + ) +} diff --git a/src/models/pipeunion/register.ts b/src/models/pipeunion/register.ts new file mode 100644 index 0000000..9d65cca --- /dev/null +++ b/src/models/pipeunion/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parsePipeUnionModelParams } from "./parse-model-string" +import { pipeUnionModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "pipeunion", + schema: pipeUnionModelDefinitionSchema, + parse: parsePipeUnionModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pipeunion/schema.ts b/src/models/pipeunion/schema.ts new file mode 100644 index 0000000..d7708df --- /dev/null +++ b/src/models/pipeunion/schema.ts @@ -0,0 +1,64 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(30), + boreDiameter: positiveLength.default(16), + length: positiveLength.default(55), + nutAcrossFlats: positiveLength.default(36), + nutLength: positiveLength.default(20), + endDiameter: positiveLength.default(24), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.endDiameter)) + issue("Requires boreDiameter < endDiameter") + if (!(p.endDiameter < p.outerDiameter)) + issue("Requires endDiameter < outerDiameter") + if (!(p.outerDiameter < p.nutAcrossFlats)) + issue("Requires outerDiameter < nutAcrossFlats") + if (!(p.nutLength * 1.25 < p.length)) issue("Requires nutLength < length") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.endDiameter - p.boreDiameter, + p.outerDiameter - p.endDiameter, + p.nutAcrossFlats - p.outerDiameter, + p.length - p.nutLength, + p.length - p.nutLength * 1.25, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const pipeUnionModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pipeUnionModelDefinitionSchema = z + .object({ fn: z.literal("pipeunion"), ...shape }) + .strict() + .superRefine(validate) +export type PipeUnionModelPropsInput = z.input +export type PipeUnionModelProps = z.output +export type PipeUnionModelDefinition = z.output< + typeof pipeUnionModelDefinitionSchema +> +export function getPipeUnionDimensions(input: PipeUnionModelPropsInput = {}) { + return pipeUnionModelPropsSchema.parse(input) +} diff --git a/src/models/pneumaticcylinder/index.ts b/src/models/pneumaticcylinder/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pneumaticcylinder/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pneumaticcylinder/parse-model-string.ts b/src/models/pneumaticcylinder/parse-model-string.ts new file mode 100644 index 0000000..a2ced8f --- /dev/null +++ b/src/models/pneumaticcylinder/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pneumaticCylinderModelDefinitionSchema } from "./schema" +const aliases = { + borediameter: "boreDiameter", + bodylength: "bodyLength", + roddiameter: "rodDiameter", + stroke: "stroke", + extension: "extension", + endcaplength: "endCapLength", + portdiameter: "portDiameter", +} as const +export function parsePneumaticCylinderModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pneumaticcylinder", + pneumaticCylinderModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/pneumaticcylinder/register.ts b/src/models/pneumaticcylinder/register.ts new file mode 100644 index 0000000..56cc69f --- /dev/null +++ b/src/models/pneumaticcylinder/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { pneumaticCylinderModelDefinitionSchema } from "./schema" +import { parsePneumaticCylinderModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "pneumaticcylinder", + schema: pneumaticCylinderModelDefinitionSchema, + parse: parsePneumaticCylinderModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pneumaticcylinder/schema.ts b/src/models/pneumaticcylinder/schema.ts new file mode 100644 index 0000000..9c81045 --- /dev/null +++ b/src/models/pneumaticcylinder/schema.ts @@ -0,0 +1,75 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + boreDiameter: positive.default(32), + bodyLength: positive.default(80), + rodDiameter: positive.default(12), + stroke: positive.default(50), + extension: nonnegative.default(25), + endCapLength: positive.default(10), + portDiameter: positive.default(6), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.rodDiameter >= p.boreDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible pneumaticcylinder dimensions: 1", + }) + if (p.extension > p.stroke) + context.addIssue({ + code: "custom", + message: "Incompatible pneumaticcylinder dimensions: 2", + }) + if (2 * p.endCapLength >= p.bodyLength) + context.addIssue({ + code: "custom", + message: "Incompatible pneumaticcylinder dimensions: 3", + }) + if (p.portDiameter >= p.endCapLength) + context.addIssue({ + code: "custom", + message: "Incompatible pneumaticcylinder dimensions: 4", + }) +} +export const pneumaticCylinderModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pneumaticCylinderModelDefinitionSchema = z + .object({ fn: z.literal("pneumaticcylinder"), ...shape }) + .strict() + .superRefine(validate) +export type PneumaticCylinderModelPropsInput = z.input< + typeof pneumaticCylinderModelPropsSchema +> +export type PneumaticCylinderModelProps = z.output< + typeof pneumaticCylinderModelPropsSchema +> +export type PneumaticCylinderModelDefinition = z.output< + typeof pneumaticCylinderModelDefinitionSchema +> +/** Air cylinder with square end caps. The body is along +Z; extension is the visible rod displacement and cannot exceed stroke. End-cap screw holes and cross-body pneumatic ports are geometric openings. Bore and stroke describe nominal assembly sizing; piston, seals, threads and concealed internals are omitted. All normalized lengths are millimeters. */ +export function getPneumaticCylinderDimensions( + input: PneumaticCylinderModelPropsInput = {}, +) { + const p = pneumaticCylinderModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.boreDiameter + 12, + p.boreDiameter + 12, + p.bodyLength + p.extension, + ] as [number, number, number], + } +} diff --git a/src/models/pressuregauge/index.ts b/src/models/pressuregauge/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pressuregauge/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pressuregauge/parse-model-string.ts b/src/models/pressuregauge/parse-model-string.ts new file mode 100644 index 0000000..d75a8db --- /dev/null +++ b/src/models/pressuregauge/parse-model-string.ts @@ -0,0 +1,18 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pressureGaugeModelDefinitionSchema } from "./schema" +const aliases = { + diameter: "diameter", + depth: "depth", + stemdiameter: "stemDiameter", + stemlength: "stemLength", + d: "diameter", +} as const +export function parsePressureGaugeModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pressuregauge", + pressureGaugeModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/pressuregauge/register.ts b/src/models/pressuregauge/register.ts new file mode 100644 index 0000000..766c394 --- /dev/null +++ b/src/models/pressuregauge/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { pressureGaugeModelDefinitionSchema } from "./schema" +import { parsePressureGaugeModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "pressuregauge", + schema: pressureGaugeModelDefinitionSchema, + parse: parsePressureGaugeModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pressuregauge/schema.ts b/src/models/pressuregauge/schema.ts new file mode 100644 index 0000000..1bda5a6 --- /dev/null +++ b/src/models/pressuregauge/schema.ts @@ -0,0 +1,62 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + diameter: positive.default(60), + depth: positive.default(22), + stemDiameter: positive.default(10), + stemLength: positive.default(15), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.stemDiameter >= p.diameter / 3) + context.addIssue({ + code: "custom", + message: "Incompatible pressuregauge dimensions: 1", + }) + if (p.depth >= p.diameter) + context.addIssue({ + code: "custom", + message: "Incompatible pressuregauge dimensions: 2", + }) +} +export const pressureGaugeModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pressureGaugeModelDefinitionSchema = z + .object({ fn: z.literal("pressuregauge"), ...shape }) + .strict() + .superRefine(validate) +export type PressureGaugeModelPropsInput = z.input< + typeof pressureGaugeModelPropsSchema +> +export type PressureGaugeModelProps = z.output< + typeof pressureGaugeModelPropsSchema +> +export type PressureGaugeModelDefinition = z.output< + typeof pressureGaugeModelDefinitionSchema +> +/** Round pressure gauge facing -Y with a recessed dial, eleven major tick marks and a fixed nominal needle. The bottom stem is bored. Tick marks are decorative and intentionally have no numbers or calibration. All normalized lengths are millimeters. */ +export function getPressureGaugeDimensions( + input: PressureGaugeModelPropsInput = {}, +) { + const p = pressureGaugeModelPropsSchema.parse(input) + return { + ...p, + size: [p.diameter, p.depth + 1.5, p.stemLength + p.diameter] as [ + number, + number, + number, + ], + } +} diff --git a/src/models/pressureswitch/index.ts b/src/models/pressureswitch/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pressureswitch/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pressureswitch/parse-model-string.ts b/src/models/pressureswitch/parse-model-string.ts new file mode 100644 index 0000000..f4908ea --- /dev/null +++ b/src/models/pressureswitch/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pressureSwitchModelDefinitionSchema } from "./schema" +const aliases = { + width: "width", + depth: "depth", + height: "height", + portdiameter: "portDiameter", + portlength: "portLength", + w: "width", + h: "height", +} as const +export function parsePressureSwitchModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pressureswitch", + pressureSwitchModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/pressureswitch/register.ts b/src/models/pressureswitch/register.ts new file mode 100644 index 0000000..48522d1 --- /dev/null +++ b/src/models/pressureswitch/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { pressureSwitchModelDefinitionSchema } from "./schema" +import { parsePressureSwitchModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "pressureswitch", + schema: pressureSwitchModelDefinitionSchema, + parse: parsePressureSwitchModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pressureswitch/schema.ts b/src/models/pressureswitch/schema.ts new file mode 100644 index 0000000..0a0c90c --- /dev/null +++ b/src/models/pressureswitch/schema.ts @@ -0,0 +1,63 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + width: positive.default(32), + depth: positive.default(28), + height: positive.default(38), + portDiameter: positive.default(8), + portLength: positive.default(10), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.portDiameter >= Math.min(p.width, p.depth) / 2) + context.addIssue({ + code: "custom", + message: "Incompatible pressureswitch dimensions: 1", + }) + if (p.portLength >= p.height) + context.addIssue({ + code: "custom", + message: "Incompatible pressureswitch dimensions: 2", + }) +} +export const pressureSwitchModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pressureSwitchModelDefinitionSchema = z + .object({ fn: z.literal("pressureswitch"), ...shape }) + .strict() + .superRefine(validate) +export type PressureSwitchModelPropsInput = z.input< + typeof pressureSwitchModelPropsSchema +> +export type PressureSwitchModelProps = z.output< + typeof pressureSwitchModelPropsSchema +> +export type PressureSwitchModelDefinition = z.output< + typeof pressureSwitchModelDefinitionSchema +> +/** Mechanical pressure switch enclosure with a bottom pressure port, top adjustment screw and bored cable gland on +X. The switch mechanism and pressure calibration are omitted. All normalized lengths are millimeters. */ +export function getPressureSwitchDimensions( + input: PressureSwitchModelPropsInput = {}, +) { + const p = pressureSwitchModelPropsSchema.parse(input) + return { + ...p, + size: [p.width + 9, p.depth, p.portLength + p.height + 6] as [ + number, + number, + number, + ], + } +} diff --git a/src/models/pushbuttonoperator/index.ts b/src/models/pushbuttonoperator/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pushbuttonoperator/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pushbuttonoperator/parse-model-string.ts b/src/models/pushbuttonoperator/parse-model-string.ts new file mode 100644 index 0000000..e452d43 --- /dev/null +++ b/src/models/pushbuttonoperator/parse-model-string.ts @@ -0,0 +1,18 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pushbuttonOperatorModelDefinitionSchema } from "./schema" +const aliases = { + paneldiameter: "panelDiameter", + bezeldiameter: "bezelDiameter", + buttondiameter: "buttonDiameter", + bodylength: "bodyLength", + buttonheight: "buttonHeight", +} as const +export function parsePushbuttonOperatorModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pushbuttonoperator", + pushbuttonOperatorModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/pushbuttonoperator/register.ts b/src/models/pushbuttonoperator/register.ts new file mode 100644 index 0000000..1ea18d7 --- /dev/null +++ b/src/models/pushbuttonoperator/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { pushbuttonOperatorModelDefinitionSchema } from "./schema" +import { parsePushbuttonOperatorModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "pushbuttonoperator", + schema: pushbuttonOperatorModelDefinitionSchema, + parse: parsePushbuttonOperatorModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pushbuttonoperator/schema.ts b/src/models/pushbuttonoperator/schema.ts new file mode 100644 index 0000000..23b03a1 --- /dev/null +++ b/src/models/pushbuttonoperator/schema.ts @@ -0,0 +1,68 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + panelDiameter: positive.default(22), + bezelDiameter: positive.default(30), + buttonDiameter: positive.default(22), + bodyLength: positive.default(35), + buttonHeight: positive.default(8), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.panelDiameter >= p.bezelDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible pushbuttonoperator dimensions: 1", + }) + if (p.buttonDiameter >= p.bezelDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible pushbuttonoperator dimensions: 2", + }) + if (p.bodyLength <= p.buttonHeight) + context.addIssue({ + code: "custom", + message: "Incompatible pushbuttonoperator dimensions: 3", + }) +} +export const pushbuttonOperatorModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pushbuttonOperatorModelDefinitionSchema = z + .object({ fn: z.literal("pushbuttonoperator"), ...shape }) + .strict() + .superRefine(validate) +export type PushbuttonOperatorModelPropsInput = z.input< + typeof pushbuttonOperatorModelPropsSchema +> +export type PushbuttonOperatorModelProps = z.output< + typeof pushbuttonOperatorModelPropsSchema +> +export type PushbuttonOperatorModelDefinition = z.output< + typeof pushbuttonOperatorModelDefinitionSchema +> +/** Panel pushbutton operator with nominal mounting barrel, bezel, circular plunger and two rear terminal tabs. The panel is at Z=bodyLength, the actuator faces +Z, and terminals project below Z=0. No contact rating is implied. All normalized lengths are millimeters. */ +export function getPushbuttonOperatorDimensions( + input: PushbuttonOperatorModelPropsInput = {}, +) { + const p = pushbuttonOperatorModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.bezelDiameter, + p.bezelDiameter, + p.bodyLength + p.buttonHeight + 7, + ] as [number, number, number], + } +} diff --git a/src/models/pushconnectfitting/index.ts b/src/models/pushconnectfitting/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/pushconnectfitting/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/pushconnectfitting/parse-model-string.ts b/src/models/pushconnectfitting/parse-model-string.ts new file mode 100644 index 0000000..cdcdb75 --- /dev/null +++ b/src/models/pushconnectfitting/parse-model-string.ts @@ -0,0 +1,26 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { pushConnectFittingModelDefinitionSchema } from "./schema" +export function parsePushConnectFittingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "pushconnectfitting", + pushConnectFittingModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + l: "length", + tube: "tubeDiameter", + collet: "colletDiameter", + colletl: "colletLength", + af: "hexAcrossFlats", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + length: "length", + tubediameter: "tubeDiameter", + colletdiameter: "colletDiameter", + colletlength: "colletLength", + hexacrossflats: "hexAcrossFlats", + }, + ) +} diff --git a/src/models/pushconnectfitting/register.ts b/src/models/pushconnectfitting/register.ts new file mode 100644 index 0000000..3401b46 --- /dev/null +++ b/src/models/pushconnectfitting/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parsePushConnectFittingModelParams } from "./parse-model-string" +import { pushConnectFittingModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "pushconnectfitting", + schema: pushConnectFittingModelDefinitionSchema, + parse: parsePushConnectFittingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/pushconnectfitting/schema.ts b/src/models/pushconnectfitting/schema.ts new file mode 100644 index 0000000..191cbf6 --- /dev/null +++ b/src/models/pushconnectfitting/schema.ts @@ -0,0 +1,74 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(26), + boreDiameter: positiveLength.default(8), + length: positiveLength.default(40), + tubeDiameter: positiveLength.default(12), + colletDiameter: positiveLength.default(20), + colletLength: positiveLength.default(6), + hexAcrossFlats: positiveLength.default(28), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.tubeDiameter)) + issue("Requires boreDiameter < tubeDiameter") + if (!(p.tubeDiameter < p.colletDiameter)) + issue("Requires tubeDiameter < colletDiameter") + if (!(p.colletDiameter < p.outerDiameter)) + issue("Requires colletDiameter < outerDiameter") + if (!(p.outerDiameter < p.hexAcrossFlats)) + issue("Requires outerDiameter < hexAcrossFlats") + if (!(p.colletLength * 2 < p.length)) + issue("Requires colletLength * 2 < length") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.tubeDiameter - p.boreDiameter, + p.colletDiameter - p.tubeDiameter, + p.outerDiameter - p.colletDiameter, + p.hexAcrossFlats - p.outerDiameter, + p.length - p.colletLength * 2, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const pushConnectFittingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const pushConnectFittingModelDefinitionSchema = z + .object({ fn: z.literal("pushconnectfitting"), ...shape }) + .strict() + .superRefine(validate) +export type PushConnectFittingModelPropsInput = z.input< + typeof pushConnectFittingModelPropsSchema +> +export type PushConnectFittingModelProps = z.output< + typeof pushConnectFittingModelPropsSchema +> +export type PushConnectFittingModelDefinition = z.output< + typeof pushConnectFittingModelDefinitionSchema +> +export function getPushConnectFittingDimensions( + input: PushConnectFittingModelPropsInput = {}, +) { + return pushConnectFittingModelPropsSchema.parse(input) +} diff --git a/src/models/reducingbushing/index.ts b/src/models/reducingbushing/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/reducingbushing/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/reducingbushing/parse-model-string.ts b/src/models/reducingbushing/parse-model-string.ts new file mode 100644 index 0000000..42dcfc6 --- /dev/null +++ b/src/models/reducingbushing/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { reducingBushingModelDefinitionSchema } from "./schema" +export function parseReducingBushingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "reducingbushing", + reducingBushingModelDefinitionSchema, + { + od: "outerDiameter", + largebore: "largeBoreDiameter", + smallbore: "smallBoreDiameter", + l: "length", + af: "hexAcrossFlats", + hexl: "hexLength", + outerdiameter: "outerDiameter", + largeborediameter: "largeBoreDiameter", + smallborediameter: "smallBoreDiameter", + length: "length", + hexacrossflats: "hexAcrossFlats", + hexlength: "hexLength", + }, + ) +} diff --git a/src/models/reducingbushing/register.ts b/src/models/reducingbushing/register.ts new file mode 100644 index 0000000..93d7a7f --- /dev/null +++ b/src/models/reducingbushing/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parseReducingBushingModelParams } from "./parse-model-string" +import { reducingBushingModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "reducingbushing", + schema: reducingBushingModelDefinitionSchema, + parse: parseReducingBushingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/reducingbushing/schema.ts b/src/models/reducingbushing/schema.ts new file mode 100644 index 0000000..dbfb3a2 --- /dev/null +++ b/src/models/reducingbushing/schema.ts @@ -0,0 +1,69 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(26), + largeBoreDiameter: positiveLength.default(18), + smallBoreDiameter: positiveLength.default(10), + length: positiveLength.default(30), + hexAcrossFlats: positiveLength.default(30), + hexLength: positiveLength.default(10), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.smallBoreDiameter < p.largeBoreDiameter)) + issue("Requires smallBoreDiameter < largeBoreDiameter") + if (!(p.largeBoreDiameter < p.outerDiameter)) + issue("Requires largeBoreDiameter < outerDiameter") + if (!(p.outerDiameter < p.hexAcrossFlats)) + issue("Requires outerDiameter < hexAcrossFlats") + if (!(p.hexLength < p.length)) issue("Requires hexLength < length") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [ + p.largeBoreDiameter - p.smallBoreDiameter, + p.outerDiameter - p.largeBoreDiameter, + p.hexAcrossFlats - p.outerDiameter, + p.length - p.hexLength, + ] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const reducingBushingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const reducingBushingModelDefinitionSchema = z + .object({ fn: z.literal("reducingbushing"), ...shape }) + .strict() + .superRefine(validate) +export type ReducingBushingModelPropsInput = z.input< + typeof reducingBushingModelPropsSchema +> +export type ReducingBushingModelProps = z.output< + typeof reducingBushingModelPropsSchema +> +export type ReducingBushingModelDefinition = z.output< + typeof reducingBushingModelDefinitionSchema +> +export function getReducingBushingDimensions( + input: ReducingBushingModelPropsInput = {}, +) { + return reducingBushingModelPropsSchema.parse(input) +} diff --git a/src/models/retainingring/index.ts b/src/models/retainingring/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/retainingring/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/retainingring/parse-model-string.ts b/src/models/retainingring/parse-model-string.ts new file mode 100644 index 0000000..ad37101 --- /dev/null +++ b/src/models/retainingring/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { retainingRingModelDefinitionSchema } from "./schema" +export function parseRetainingRingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "retainingring", + retainingRingModelDefinitionSchema, + { + id: "innerDiameter", + innerdiameter: "innerDiameter", + od: "outerDiameter", + outerdiameter: "outerDiameter", + t: "thickness", + thickness: "thickness", + g: "gapWidth", + gapwidth: "gapWidth", + ld: "lugDiameter", + lugdiameter: "lugDiameter", + hol: "holeDiameter", + holediameter: "holeDiameter", + }, + ) +} diff --git a/src/models/retainingring/register.ts b/src/models/retainingring/register.ts new file mode 100644 index 0000000..c1e7056 --- /dev/null +++ b/src/models/retainingring/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { retainingRingModelDefinitionSchema } from "./schema" +import { parseRetainingRingModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "retainingring", + schema: retainingRingModelDefinitionSchema, + parse: parseRetainingRingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/retainingring/schema.ts b/src/models/retainingring/schema.ts new file mode 100644 index 0000000..739a9c3 --- /dev/null +++ b/src/models/retainingring/schema.ts @@ -0,0 +1,78 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + innerDiameter: positiveLength.default(16), + outerDiameter: positiveLength.default(20), + thickness: positiveLength.default(1.2), + gapWidth: positiveLength.default(5), + lugDiameter: positiveLength.default(4), + holeDiameter: positiveLength.default(1.5), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![ + (p.outerDiameter - p.innerDiameter) / 2, + (p.lugDiameter - p.holeDiameter) / 2, + (p.gapWidth - p.lugDiameter) / 2, + ].every((gap) => gap >= extent * 0.0001) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if ( + !( + p.innerDiameter < p.outerDiameter && + p.gapWidth < p.innerDiameter && + p.gapWidth > p.lugDiameter && + p.holeDiameter < p.lugDiameter && + p.lugDiameter < p.innerDiameter / 2 + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Invalid retainingring dimensions: mating features must fit within the body", + }) +} +export const retainingRingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const retainingRingModelDefinitionSchema = z + .object({ fn: z.literal("retainingring"), ...shape }) + .strict() + .superRefine(validate) +export type RetainingRingModelPropsInput = z.input< + typeof retainingRingModelPropsSchema +> +export type RetainingRingModelProps = z.output< + typeof retainingRingModelPropsSchema +> +export type RetainingRingModelDefinition = z.output< + typeof retainingRingModelDefinitionSchema +> +export function getRetainingRingDimensions( + input: RetainingRingModelPropsInput = {}, +) { + return retainingRingModelPropsSchema.parse(input) +} diff --git a/src/models/rigidpipe/index.ts b/src/models/rigidpipe/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/rigidpipe/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/rigidpipe/parse-model-string.ts b/src/models/rigidpipe/parse-model-string.ts new file mode 100644 index 0000000..03f6305 --- /dev/null +++ b/src/models/rigidpipe/parse-model-string.ts @@ -0,0 +1,18 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { rigidPipeModelDefinitionSchema } from "./schema" +export function parseRigidPipeModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "rigidpipe", + rigidPipeModelDefinitionSchema, + { + od: "outerDiameter", + bore: "boreDiameter", + l: "length", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + length: "length", + }, + ) +} diff --git a/src/models/rigidpipe/register.ts b/src/models/rigidpipe/register.ts new file mode 100644 index 0000000..6c2c053 --- /dev/null +++ b/src/models/rigidpipe/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { parseRigidPipeModelParams } from "./parse-model-string" +import { rigidPipeModelDefinitionSchema } from "./schema" +export const model = defineModel({ + name: "rigidpipe", + schema: rigidPipeModelDefinitionSchema, + parse: parseRigidPipeModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/rigidpipe/schema.ts b/src/models/rigidpipe/schema.ts new file mode 100644 index 0000000..906df3e --- /dev/null +++ b/src/models/rigidpipe/schema.ts @@ -0,0 +1,50 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(20), + boreDiameter: positiveLength.default(16), + length: positiveLength.default(100), +} +type Resolved = z.output> +function validate(p: Resolved, ctx: z.RefinementCtx) { + const issue = (message: string) => ctx.addIssue({ code: "custom", message }) + if (!(p.boreDiameter < p.outerDiameter)) + issue("Requires boreDiameter < outerDiameter") + const dimensions = Object.keys(shape) + .filter((key) => !key.endsWith("Count")) + .map((key) => p[key as keyof Resolved]) + const minimumGap = Math.max(...dimensions) * 1e-4 + const clearances = [p.outerDiameter - p.boreDiameter] + if (clearances.some((gap) => gap < minimumGap)) + issue( + "Material and clearance dimensions exceed nominal geometry resolution", + ) + if (Math.max(...dimensions) > 100000 || Math.min(...dimensions) < 0.001) + issue( + "Dimensions must remain within nominal geometry limits (0.001 to 100000 mm)", + ) + if (Math.min(...dimensions) / Math.max(...dimensions) < 1e-5) + issue("Dimensions exceed nominal geometry resolution") +} +/** Custom nominal millimeter dimensions; smooth thread envelopes do not certify a pipe standard. */ +export const rigidPipeModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const rigidPipeModelDefinitionSchema = z + .object({ fn: z.literal("rigidpipe"), ...shape }) + .strict() + .superRefine(validate) +export type RigidPipeModelPropsInput = z.input +export type RigidPipeModelProps = z.output +export type RigidPipeModelDefinition = z.output< + typeof rigidPipeModelDefinitionSchema +> +export function getRigidPipeDimensions(input: RigidPipeModelPropsInput = {}) { + return rigidPipeModelPropsSchema.parse(input) +} diff --git a/src/models/rivetnut/index.ts b/src/models/rivetnut/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/rivetnut/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/rivetnut/parse-model-string.ts b/src/models/rivetnut/parse-model-string.ts new file mode 100644 index 0000000..7c1c90e --- /dev/null +++ b/src/models/rivetnut/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { rivetNutModelDefinitionSchema } from "./schema" +export function parseRivetNutModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "rivetnut", + rivetNutModelDefinitionSchema, + { + id: "boreDiameter", + borediameter: "boreDiameter", + d: "bodyDiameter", + bodydiameter: "bodyDiameter", + l: "bodyLength", + bodylength: "bodyLength", + fd: "flangeDiameter", + flangediameter: "flangeDiameter", + ft: "flangeThickness", + flangethickness: "flangeThickness", + }, + ) +} diff --git a/src/models/rivetnut/register.ts b/src/models/rivetnut/register.ts new file mode 100644 index 0000000..a2a35c5 --- /dev/null +++ b/src/models/rivetnut/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { rivetNutModelDefinitionSchema } from "./schema" +import { parseRivetNutModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "rivetnut", + schema: rivetNutModelDefinitionSchema, + parse: parseRivetNutModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/rivetnut/schema.ts b/src/models/rivetnut/schema.ts new file mode 100644 index 0000000..517d4d6 --- /dev/null +++ b/src/models/rivetnut/schema.ts @@ -0,0 +1,68 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + boreDiameter: positiveLength.default(6), + bodyDiameter: positiveLength.default(9), + bodyLength: positiveLength.default(14), + flangeDiameter: positiveLength.default(12), + flangeThickness: positiveLength.default(1.5), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![ + (p.bodyDiameter - p.boreDiameter - p.bodyDiameter * 0.045) / 2, + (p.flangeDiameter - p.bodyDiameter) / 2, + ].every((gap) => gap >= extent * 0.0001) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if ( + !( + p.boreDiameter < p.bodyDiameter && + p.bodyDiameter < p.flangeDiameter && + p.flangeThickness < p.bodyLength + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Invalid rivetnut dimensions: mating features must fit within the body", + }) +} +export const rivetNutModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const rivetNutModelDefinitionSchema = z + .object({ fn: z.literal("rivetnut"), ...shape }) + .strict() + .superRefine(validate) +export type RivetNutModelPropsInput = z.input +export type RivetNutModelProps = z.output +export type RivetNutModelDefinition = z.output< + typeof rivetNutModelDefinitionSchema +> +export function getRivetNutDimensions(input: RivetNutModelPropsInput = {}) { + return rivetNutModelPropsSchema.parse(input) +} diff --git a/src/models/rodend/index.ts b/src/models/rodend/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/rodend/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/rodend/parse-model-string.ts b/src/models/rodend/parse-model-string.ts new file mode 100644 index 0000000..350a2ff --- /dev/null +++ b/src/models/rodend/parse-model-string.ts @@ -0,0 +1,23 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { rodEndModelDefinitionSchema } from "./schema" +const aliases = { + hd: "headDiameter", + bore: "boreDiameter", + w: "headWidth", + stem: "stemDiameter", + sl: "stemLength", + headdiameter: "headDiameter", + borediameter: "boreDiameter", + headwidth: "headWidth", + stemdiameter: "stemDiameter", + stemlength: "stemLength", +} as const +export function parseRodEndModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "rodend", + rodEndModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/rodend/register.ts b/src/models/rodend/register.ts new file mode 100644 index 0000000..1ed50ef --- /dev/null +++ b/src/models/rodend/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { rodEndModelDefinitionSchema } from "./schema" +import { parseRodEndModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "rodend", + schema: rodEndModelDefinitionSchema, + parse: parseRodEndModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/rodend/schema.ts b/src/models/rodend/schema.ts new file mode 100644 index 0000000..0a03673 --- /dev/null +++ b/src/models/rodend/schema.ts @@ -0,0 +1,76 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + headDiameter: positiveLength.default(28), + boreDiameter: positiveLength.default(12), + headWidth: positiveLength.default(12), + stemDiameter: positiveLength.default(10), + stemLength: positiveLength.default(30), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.headDiameter <= p.boreDiameter * 1.7) + context.addIssue({ + code: "custom", + message: + "The head must leave material around its spherical insert and bore", + }) + if (p.headWidth >= p.headDiameter * 0.65) + context.addIssue({ + code: "custom", + message: "Head width must be less than 65 percent of head diameter", + }) + if (p.headWidth <= p.boreDiameter * 0.35) + context.addIssue({ + code: "custom", + message: "Head width must expose a useful inner spherical ring", + }) + if (p.stemDiameter >= p.headWidth) + context.addIssue({ + code: "custom", + message: "The stem must fit within the head width", + }) + if (p.stemDiameter >= p.headDiameter / 2) + context.addIssue({ + code: "custom", + message: "The stem diameter must be smaller than the head radius", + }) + if ( + Math.sqrt((p.headDiameter * 0.7) ** 2 - p.headWidth ** 2) <= p.boreDiameter + ) + context.addIssue({ + code: "custom", + message: "Spherical insert must surround the bore at both end faces", + }) +} +export const rodEndModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const rodEndModelDefinitionSchema = z + .object({ fn: z.literal("rodend"), ...shape }) + .strict() + .superRefine(validate) +export type RodEndModelPropsInput = z.input +export type RodEndModelProps = z.output +export type RodEndModelDefinition = z.output +/** Rod-end eye with a curved spherical inner insert and smooth male-stem envelope pointing toward -Y. Eye axis Z, bottom Z=0. Thread major diameter only, no threads, articulation limit or load rating. All lengths are normalized to millimeters. */ +export function getRodEndDimensions(input: RodEndModelPropsInput = {}) { + const p = rodEndModelPropsSchema.parse(input) + return { + ...p, + size: [p.headDiameter, p.headDiameter + p.stemLength, p.headWidth] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.headWidth, + } +} diff --git a/src/models/rollerchain/index.ts b/src/models/rollerchain/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/rollerchain/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/rollerchain/parse-model-string.ts b/src/models/rollerchain/parse-model-string.ts new file mode 100644 index 0000000..d2b80cf --- /dev/null +++ b/src/models/rollerchain/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { rollerChainModelDefinitionSchema } from "./schema" +const aliases = { + pitch: "pitch", + links: "linkCount", + roller: "rollerDiameter", + w: "width", + pin: "pinDiameter", + plate: "plateThickness", + linkcount: "linkCount", + rollerdiameter: "rollerDiameter", + width: "width", + pindiameter: "pinDiameter", + platethickness: "plateThickness", +} as const +export function parseRollerChainModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "rollerchain", + rollerChainModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/rollerchain/register.ts b/src/models/rollerchain/register.ts new file mode 100644 index 0000000..d8727cc --- /dev/null +++ b/src/models/rollerchain/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { rollerChainModelDefinitionSchema } from "./schema" +import { parseRollerChainModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "rollerchain", + schema: rollerChainModelDefinitionSchema, + parse: parseRollerChainModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/rollerchain/schema.ts b/src/models/rollerchain/schema.ts new file mode 100644 index 0000000..7fa2444 --- /dev/null +++ b/src/models/rollerchain/schema.ts @@ -0,0 +1,71 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + pitch: positiveLength.default(12.7), + linkCount: z.number().int().min(2).max(64).default(6), + rollerDiameter: positiveLength.default(7.9), + width: positiveLength.default(8), + pinDiameter: positiveLength.default(4), + plateThickness: positiveLength.default(1.2), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.pitch <= p.rollerDiameter * 1.15) + context.addIssue({ + code: "custom", + message: "Pitch must leave positive space between adjacent rollers", + }) + if (p.rollerDiameter <= p.pinDiameter) + context.addIssue({ + code: "custom", + message: "Roller diameter must exceed pin diameter", + }) + if (p.width <= p.plateThickness * 5) + context.addIssue({ + code: "custom", + message: + "Width must leave room for both pairs of plates and a free roller", + }) + if (p.pinDiameter <= p.plateThickness) + context.addIssue({ + code: "custom", + message: "Pin diameter must exceed plate thickness", + }) +} +export const rollerChainModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const rollerChainModelDefinitionSchema = z + .object({ fn: z.literal("rollerchain"), ...shape }) + .strict() + .superRefine(validate) +export type RollerChainModelPropsInput = z.input< + typeof rollerChainModelPropsSchema +> +export type RollerChainModelProps = z.output +export type RollerChainModelDefinition = z.output< + typeof rollerChainModelDefinitionSchema +> +/** Straight roller-chain segment along X with alternating inner/outer side plates, discrete rollers and projecting rivet pins. Roller axes Z. Nominal pitch/width placement assembly; not a named chain standard or flexible/meshing simulation. All lengths are normalized to millimeters. */ +export function getRollerChainDimensions( + input: RollerChainModelPropsInput = {}, +) { + const p = rollerChainModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.pitch * p.linkCount + p.rollerDiameter * 1.25, + p.rollerDiameter * 1.25, + p.width + 2 * p.plateThickness, + ] as [number, number, number], + bottomZ: -p.plateThickness, + topZ: p.width + p.plateThickness, + } +} diff --git a/src/models/roundbar/index.ts b/src/models/roundbar/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/roundbar/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/roundbar/parse-model-string.ts b/src/models/roundbar/parse-model-string.ts new file mode 100644 index 0000000..12a2419 --- /dev/null +++ b/src/models/roundbar/parse-model-string.ts @@ -0,0 +1,18 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { roundBarModelDefinitionSchema } from "./schema" +const aliases = { + diameter: "diameter", + length: "length", + chamfer: "chamfer", + l: "length", + d: "diameter", +} as const +export function parseRoundBarModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "roundbar", + roundBarModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/roundbar/register.ts b/src/models/roundbar/register.ts new file mode 100644 index 0000000..4103a7e --- /dev/null +++ b/src/models/roundbar/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { roundBarModelDefinitionSchema } from "./schema" +import { parseRoundBarModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "roundbar", + schema: roundBarModelDefinitionSchema, + parse: parseRoundBarModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/roundbar/schema.ts b/src/models/roundbar/schema.ts new file mode 100644 index 0000000..f6bc03a --- /dev/null +++ b/src/models/roundbar/schema.ts @@ -0,0 +1,43 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + diameter: positiveLength.default(20), + length: positiveLength.default(100), + chamfer: positiveLength.default(1), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.chamfer < p.diameter / 3)) + context.addIssue({ + code: "custom", + message: "Invalid roundbar dimensions: chamfer < diameter/3", + }) + if (!(2 * p.chamfer < p.length)) + context.addIssue({ + code: "custom", + message: "Invalid roundbar dimensions: 2*chamfer < length", + }) +} +export const roundBarModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const roundBarModelDefinitionSchema = z + .object({ fn: z.literal("roundbar"), ...shape }) + .strict() + .superRefine(validate) +export type RoundBarModelPropsInput = z.input +export type RoundBarModelProps = z.output +export type RoundBarModelDefinition = z.output< + typeof roundBarModelDefinitionSchema +> +/** Round stock along +Z with equal end chamfers; bottom Z=0. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getRoundBarDimensions(input: RoundBarModelPropsInput = {}) { + return roundBarModelPropsSchema.parse(input) +} diff --git a/src/models/rubberbumper/index.ts b/src/models/rubberbumper/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/rubberbumper/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/rubberbumper/parse-model-string.ts b/src/models/rubberbumper/parse-model-string.ts new file mode 100644 index 0000000..2100d42 --- /dev/null +++ b/src/models/rubberbumper/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { rubberBumperModelDefinitionSchema } from "./schema" +const aliases = { + diameter: "diameter", + height: "height", + holediameter: "holeDiameter", + counterborediameter: "counterboreDiameter", + counterboredepth: "counterboreDepth", + h: "height", + d: "diameter", +} as const +export function parseRubberBumperModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "rubberbumper", + rubberBumperModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/rubberbumper/register.ts b/src/models/rubberbumper/register.ts new file mode 100644 index 0000000..4cd8f4c --- /dev/null +++ b/src/models/rubberbumper/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { rubberBumperModelDefinitionSchema } from "./schema" +import { parseRubberBumperModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "rubberbumper", + schema: rubberBumperModelDefinitionSchema, + parse: parseRubberBumperModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/rubberbumper/schema.ts b/src/models/rubberbumper/schema.ts new file mode 100644 index 0000000..371266f --- /dev/null +++ b/src/models/rubberbumper/schema.ts @@ -0,0 +1,63 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + diameter: positiveLength.default(30), + height: positiveLength.default(15), + holeDiameter: positiveLength.default(6), + counterboreDiameter: positiveLength.default(13), + counterboreDepth: positiveLength.default(5), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.holeDiameter < p.counterboreDiameter)) + context.addIssue({ + code: "custom", + message: + "Invalid rubberbumper dimensions: holeDiameter < counterboreDiameter", + }) + if (!(p.counterboreDiameter < p.diameter * 0.8)) + context.addIssue({ + code: "custom", + message: + "Invalid rubberbumper dimensions: counterboreDiameter < diameter*.8", + }) + if (!(p.counterboreDepth < p.height)) + context.addIssue({ + code: "custom", + message: "Invalid rubberbumper dimensions: counterboreDepth < height", + }) + if (!(p.height < p.diameter)) + context.addIssue({ + code: "custom", + message: "Invalid rubberbumper dimensions: height < diameter", + }) +} +export const rubberBumperModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const rubberBumperModelDefinitionSchema = z + .object({ fn: z.literal("rubberbumper"), ...shape }) + .strict() + .superRefine(validate) +export type RubberBumperModelPropsInput = z.input< + typeof rubberBumperModelPropsSchema +> +export type RubberBumperModelProps = z.output< + typeof rubberBumperModelPropsSchema +> +export type RubberBumperModelDefinition = z.output< + typeof rubberBumperModelDefinitionSchema +> +/** Cylindrical bumper with axial through bore and top counterbore, axis Z. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getRubberBumperDimensions( + input: RubberBumperModelPropsInput = {}, +) { + return rubberBumperModelPropsSchema.parse(input) +} diff --git a/src/models/selectoroperator/index.ts b/src/models/selectoroperator/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/selectoroperator/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/selectoroperator/parse-model-string.ts b/src/models/selectoroperator/parse-model-string.ts new file mode 100644 index 0000000..4e32421 --- /dev/null +++ b/src/models/selectoroperator/parse-model-string.ts @@ -0,0 +1,19 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { selectorOperatorModelDefinitionSchema } from "./schema" +const aliases = { + paneldiameter: "panelDiameter", + bezeldiameter: "bezelDiameter", + bodylength: "bodyLength", + knobwidth: "knobWidth", + knobdepth: "knobDepth", + knobheight: "knobHeight", +} as const +export function parseSelectorOperatorModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "selectoroperator", + selectorOperatorModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/selectoroperator/register.ts b/src/models/selectoroperator/register.ts new file mode 100644 index 0000000..517627d --- /dev/null +++ b/src/models/selectoroperator/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { selectorOperatorModelDefinitionSchema } from "./schema" +import { parseSelectorOperatorModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "selectoroperator", + schema: selectorOperatorModelDefinitionSchema, + parse: parseSelectorOperatorModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/selectoroperator/schema.ts b/src/models/selectoroperator/schema.ts new file mode 100644 index 0000000..fb0ac20 --- /dev/null +++ b/src/models/selectoroperator/schema.ts @@ -0,0 +1,74 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + panelDiameter: positive.default(22), + bezelDiameter: positive.default(30), + bodyLength: positive.default(35), + knobWidth: positive.default(28), + knobDepth: positive.default(10), + knobHeight: positive.default(15), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.panelDiameter >= p.bezelDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible selectoroperator dimensions: 1", + }) + if (p.knobDepth >= p.knobWidth) + context.addIssue({ + code: "custom", + message: "Incompatible selectoroperator dimensions: 2", + }) + if (p.knobWidth > p.bezelDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible selectoroperator dimensions: 3", + }) + if (p.knobHeight >= p.bodyLength) + context.addIssue({ + code: "custom", + message: "Incompatible selectoroperator dimensions: 4", + }) +} +export const selectorOperatorModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const selectorOperatorModelDefinitionSchema = z + .object({ fn: z.literal("selectoroperator"), ...shape }) + .strict() + .superRefine(validate) +export type SelectorOperatorModelPropsInput = z.input< + typeof selectorOperatorModelPropsSchema +> +export type SelectorOperatorModelProps = z.output< + typeof selectorOperatorModelPropsSchema +> +export type SelectorOperatorModelDefinition = z.output< + typeof selectorOperatorModelDefinitionSchema +> +/** Panel selector switch operator with mounting barrel, bezel, raised bar grip and two rear terminals. The grip is aligned with X at its nominal position; detents and switching contacts are omitted. All normalized lengths are millimeters. */ +export function getSelectorOperatorDimensions( + input: SelectorOperatorModelPropsInput = {}, +) { + const p = selectorOperatorModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.bezelDiameter, + p.bezelDiameter, + p.bodyLength + 3 + p.knobHeight + 7, + ] as [number, number, number], + } +} diff --git a/src/models/shaftsupport/index.ts b/src/models/shaftsupport/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/shaftsupport/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/shaftsupport/parse-model-string.ts b/src/models/shaftsupport/parse-model-string.ts new file mode 100644 index 0000000..0feeff1 --- /dev/null +++ b/src/models/shaftsupport/parse-model-string.ts @@ -0,0 +1,37 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { shaftSupportModelDefinitionSchema } from "./schema" +const aliases = { + bore: "boreDiameter", + bw: "baseWidth", + bd: "baseDepth", + bt: "baseThickness", + hw: "headWidth", + hd: "headDepth", + hh: "headHeight", + ch: "centerHeight", + ms: "mountSpacing", + mh: "mountHoleDiameter", + slit: "slitWidth", + clamp: "clampHoleDiameter", + borediameter: "boreDiameter", + basewidth: "baseWidth", + basedepth: "baseDepth", + basethickness: "baseThickness", + headwidth: "headWidth", + headdepth: "headDepth", + headheight: "headHeight", + centerheight: "centerHeight", + mountspacing: "mountSpacing", + mountholediameter: "mountHoleDiameter", + slitwidth: "slitWidth", + clampholediameter: "clampHoleDiameter", +} as const +export function parseShaftSupportModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "shaftsupport", + shaftSupportModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/shaftsupport/register.ts b/src/models/shaftsupport/register.ts new file mode 100644 index 0000000..b3fdee7 --- /dev/null +++ b/src/models/shaftsupport/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { shaftSupportModelDefinitionSchema } from "./schema" +import { parseShaftSupportModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "shaftsupport", + schema: shaftSupportModelDefinitionSchema, + parse: parseShaftSupportModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/shaftsupport/schema.ts b/src/models/shaftsupport/schema.ts new file mode 100644 index 0000000..e15db04 --- /dev/null +++ b/src/models/shaftsupport/schema.ts @@ -0,0 +1,106 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + boreDiameter: positiveLength.default(16), + baseWidth: positiveLength.default(55), + baseDepth: positiveLength.default(32), + baseThickness: positiveLength.default(7), + headWidth: positiveLength.default(26), + headDepth: positiveLength.default(20), + headHeight: positiveLength.default(34), + centerHeight: positiveLength.default(26), + mountSpacing: positiveLength.default(40), + mountHoleDiameter: positiveLength.default(5), + slitWidth: positiveLength.default(1.5), + clampHoleDiameter: positiveLength.default(4), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.headWidth <= p.boreDiameter) + context.addIssue({ + code: "custom", + message: "Support head must leave material around the shaft bore", + }) + if (p.centerHeight - p.boreDiameter / 2 <= p.baseThickness) + context.addIssue({ + code: "custom", + message: "Shaft bore must clear the mounting foot", + }) + if ( + p.centerHeight + p.boreDiameter / 2 + p.clampHoleDiameter >= + p.baseThickness + p.headHeight + ) + context.addIssue({ + code: "custom", + message: "Head must leave upper material for the clamping screw bore", + }) + if (p.mountSpacing + p.mountHoleDiameter >= p.baseWidth) + context.addIssue({ + code: "custom", + message: "Mounting holes must remain within the mounting foot", + }) + if (p.mountSpacing - p.mountHoleDiameter <= p.headWidth) + context.addIssue({ + code: "custom", + message: "Mounting holes must clear the support head", + }) + if (p.slitWidth >= p.boreDiameter) + context.addIssue({ + code: "custom", + message: "Split slit must be narrower than the shaft bore", + }) + if (p.headDepth >= p.baseDepth) + context.addIssue({ + code: "custom", + message: "Head depth must be less than mounting-foot depth", + }) + if (p.mountHoleDiameter >= p.baseDepth) + context.addIssue({ + code: "custom", + message: "Mounting-hole diameter must be less than mounting-foot depth", + }) + if (p.clampHoleDiameter >= p.headDepth) + context.addIssue({ + code: "custom", + message: "Clamping-screw diameter must be less than head depth", + }) +} +export const shaftSupportModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const shaftSupportModelDefinitionSchema = z + .object({ fn: z.literal("shaftsupport"), ...shape }) + .strict() + .superRefine(validate) +export type ShaftSupportModelPropsInput = z.input< + typeof shaftSupportModelPropsSchema +> +export type ShaftSupportModelProps = z.output< + typeof shaftSupportModelPropsSchema +> +export type ShaftSupportModelDefinition = z.output< + typeof shaftSupportModelDefinitionSchema +> +/** Split shaft support with a horizontal Y shaft bore, top clamping slit, transverse X clamp-screw bore and two vertical foot mounting bores. Bottom Z=0. No thread, clamp hardware or load rating is specified. All lengths are normalized to millimeters. */ +export function getShaftSupportDimensions( + input: ShaftSupportModelPropsInput = {}, +) { + const p = shaftSupportModelPropsSchema.parse(input) + return { + ...p, + size: [p.baseWidth, p.baseDepth, p.baseThickness + p.headHeight] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.baseThickness + p.headHeight, + } +} diff --git a/src/models/shoulderbolt/index.ts b/src/models/shoulderbolt/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/shoulderbolt/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/shoulderbolt/parse-model-string.ts b/src/models/shoulderbolt/parse-model-string.ts new file mode 100644 index 0000000..3c71aed --- /dev/null +++ b/src/models/shoulderbolt/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { shoulderBoltModelDefinitionSchema } from "./schema" +export function parseShoulderBoltModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "shoulderbolt", + shoulderBoltModelDefinitionSchema, + { + sd: "shoulderDiameter", + shoulderdiameter: "shoulderDiameter", + sl: "shoulderLength", + shoulderlength: "shoulderLength", + td: "threadDiameter", + threaddiameter: "threadDiameter", + tl: "threadLength", + threadlength: "threadLength", + hd: "headDiameter", + headdiameter: "headDiameter", + hh: "headHeight", + headheight: "headHeight", + }, + ) +} diff --git a/src/models/shoulderbolt/register.ts b/src/models/shoulderbolt/register.ts new file mode 100644 index 0000000..647d539 --- /dev/null +++ b/src/models/shoulderbolt/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { shoulderBoltModelDefinitionSchema } from "./schema" +import { parseShoulderBoltModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "shoulderbolt", + schema: shoulderBoltModelDefinitionSchema, + parse: parseShoulderBoltModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/shoulderbolt/schema.ts b/src/models/shoulderbolt/schema.ts new file mode 100644 index 0000000..f0d5117 --- /dev/null +++ b/src/models/shoulderbolt/schema.ts @@ -0,0 +1,75 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + shoulderDiameter: positiveLength.default(8), + shoulderLength: positiveLength.default(20), + threadDiameter: positiveLength.default(6), + threadLength: positiveLength.default(10), + headDiameter: positiveLength.default(13), + headHeight: positiveLength.default(5), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![ + (p.shoulderDiameter - p.threadDiameter) / 2, + (p.headDiameter - p.shoulderDiameter) / 2, + ].every((gap) => gap >= extent * 0.0001) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if ( + !( + p.threadDiameter < p.shoulderDiameter && + p.shoulderDiameter < p.headDiameter && + p.headHeight < p.headDiameter + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Invalid shoulderbolt dimensions: mating features must fit within the body", + }) +} +export const shoulderBoltModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const shoulderBoltModelDefinitionSchema = z + .object({ fn: z.literal("shoulderbolt"), ...shape }) + .strict() + .superRefine(validate) +export type ShoulderBoltModelPropsInput = z.input< + typeof shoulderBoltModelPropsSchema +> +export type ShoulderBoltModelProps = z.output< + typeof shoulderBoltModelPropsSchema +> +export type ShoulderBoltModelDefinition = z.output< + typeof shoulderBoltModelDefinitionSchema +> +export function getShoulderBoltDimensions( + input: ShoulderBoltModelPropsInput = {}, +) { + return shoulderBoltModelPropsSchema.parse(input) +} diff --git a/src/models/solenoidvalve/index.ts b/src/models/solenoidvalve/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/solenoidvalve/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/solenoidvalve/parse-model-string.ts b/src/models/solenoidvalve/parse-model-string.ts new file mode 100644 index 0000000..dc5093e --- /dev/null +++ b/src/models/solenoidvalve/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { solenoidValveModelDefinitionSchema } from "./schema" +const aliases = { + length: "length", + width: "width", + height: "height", + coilwidth: "coilWidth", + coilheight: "coilHeight", + portdiameter: "portDiameter", + w: "width", + h: "height", + l: "length", +} as const +export function parseSolenoidValveModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "solenoidvalve", + solenoidValveModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/solenoidvalve/register.ts b/src/models/solenoidvalve/register.ts new file mode 100644 index 0000000..ac35193 --- /dev/null +++ b/src/models/solenoidvalve/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { solenoidValveModelDefinitionSchema } from "./schema" +import { parseSolenoidValveModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "solenoidvalve", + schema: solenoidValveModelDefinitionSchema, + parse: parseSolenoidValveModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/solenoidvalve/schema.ts b/src/models/solenoidvalve/schema.ts new file mode 100644 index 0000000..67baea0 --- /dev/null +++ b/src/models/solenoidvalve/schema.ts @@ -0,0 +1,69 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + length: positive.default(70), + width: positive.default(28), + height: positive.default(25), + coilWidth: positive.default(24), + coilHeight: positive.default(30), + portDiameter: positive.default(6), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.coilWidth >= p.length / 2) + context.addIssue({ + code: "custom", + message: "Incompatible solenoidvalve dimensions: 1", + }) + if (p.coilWidth > p.width) + context.addIssue({ + code: "custom", + message: "Incompatible solenoidvalve dimensions: 2", + }) + if (p.portDiameter >= Math.min(p.width, p.height) / 3) + context.addIssue({ + code: "custom", + message: "Incompatible solenoidvalve dimensions: 3", + }) +} +export const solenoidValveModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const solenoidValveModelDefinitionSchema = z + .object({ fn: z.literal("solenoidvalve"), ...shape }) + .strict() + .superRefine(validate) +export type SolenoidValveModelPropsInput = z.input< + typeof solenoidValveModelPropsSchema +> +export type SolenoidValveModelProps = z.output< + typeof solenoidValveModelPropsSchema +> +export type SolenoidValveModelDefinition = z.output< + typeof solenoidValveModelDefinitionSchema +> +/** Generic directional air solenoid valve: long valve body, three cross ports, two mounting holes, coil and electrical connector envelope. The internal spool and port switching paths are omitted; the model is not a pneumatic schematic. All normalized lengths are millimeters. */ +export function getSolenoidValveDimensions( + input: SolenoidValveModelPropsInput = {}, +) { + const p = solenoidValveModelPropsSchema.parse(input) + return { + ...p, + size: [p.length + 5, p.width, p.height + p.coilHeight + 6] as [ + number, + number, + number, + ], + } +} diff --git a/src/models/solidrivet/index.ts b/src/models/solidrivet/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/solidrivet/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/solidrivet/parse-model-string.ts b/src/models/solidrivet/parse-model-string.ts new file mode 100644 index 0000000..d916e85 --- /dev/null +++ b/src/models/solidrivet/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { solidRivetModelDefinitionSchema } from "./schema" +export function parseSolidRivetModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "solidrivet", + solidRivetModelDefinitionSchema, + { + d: "shaftDiameter", + shaftdiameter: "shaftDiameter", + l: "shaftLength", + shaftlength: "shaftLength", + hd: "headDiameter", + headdiameter: "headDiameter", + hh: "headHeight", + headheight: "headHeight", + }, + ) +} diff --git a/src/models/solidrivet/register.ts b/src/models/solidrivet/register.ts new file mode 100644 index 0000000..8888679 --- /dev/null +++ b/src/models/solidrivet/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { solidRivetModelDefinitionSchema } from "./schema" +import { parseSolidRivetModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "solidrivet", + schema: solidRivetModelDefinitionSchema, + parse: parseSolidRivetModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/solidrivet/schema.ts b/src/models/solidrivet/schema.ts new file mode 100644 index 0000000..273b79e --- /dev/null +++ b/src/models/solidrivet/schema.ts @@ -0,0 +1,62 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + shaftDiameter: positiveLength.default(4), + shaftLength: positiveLength.default(12), + headDiameter: positiveLength.default(8), + headHeight: positiveLength.default(2.5), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![(p.headDiameter - p.shaftDiameter) / 2].every( + (gap) => gap >= extent * 0.0001, + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if (!(p.shaftDiameter < p.headDiameter && p.headHeight < p.headDiameter / 2)) + ctx.addIssue({ + code: "custom", + message: + "Invalid solidrivet dimensions: mating features must fit within the body", + }) +} +export const solidRivetModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const solidRivetModelDefinitionSchema = z + .object({ fn: z.literal("solidrivet"), ...shape }) + .strict() + .superRefine(validate) +export type SolidRivetModelPropsInput = z.input< + typeof solidRivetModelPropsSchema +> +export type SolidRivetModelProps = z.output +export type SolidRivetModelDefinition = z.output< + typeof solidRivetModelDefinitionSchema +> +export function getSolidRivetDimensions(input: SolidRivetModelPropsInput = {}) { + return solidRivetModelPropsSchema.parse(input) +} diff --git a/src/models/sphericalbearing/index.ts b/src/models/sphericalbearing/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/sphericalbearing/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/sphericalbearing/parse-model-string.ts b/src/models/sphericalbearing/parse-model-string.ts new file mode 100644 index 0000000..2d63945 --- /dev/null +++ b/src/models/sphericalbearing/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { sphericalBearingModelDefinitionSchema } from "./schema" +const aliases = { + od: "outerDiameter", + bore: "boreDiameter", + w: "width", + ball: "ballDiameter", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + width: "width", + balldiameter: "ballDiameter", +} as const +export function parseSphericalBearingModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "sphericalbearing", + sphericalBearingModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/sphericalbearing/register.ts b/src/models/sphericalbearing/register.ts new file mode 100644 index 0000000..6f278ff --- /dev/null +++ b/src/models/sphericalbearing/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { sphericalBearingModelDefinitionSchema } from "./schema" +import { parseSphericalBearingModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "sphericalbearing", + schema: sphericalBearingModelDefinitionSchema, + parse: parseSphericalBearingModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/sphericalbearing/schema.ts b/src/models/sphericalbearing/schema.ts new file mode 100644 index 0000000..012be8f --- /dev/null +++ b/src/models/sphericalbearing/schema.ts @@ -0,0 +1,75 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(30), + boreDiameter: positiveLength.default(12), + width: positiveLength.default(14), + ballDiameter: positiveLength.default(22), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.outerDiameter <= p.ballDiameter) + context.addIssue({ + code: "custom", + message: "Outer race must exceed the spherical inner diameter", + }) + if (p.ballDiameter <= p.boreDiameter) + context.addIssue({ + code: "custom", + message: "Spherical inner diameter must exceed the shaft bore", + }) + if (p.width >= p.ballDiameter) + context.addIssue({ + code: "custom", + message: "Bearing width must be less than the sphere diameter", + }) + if (p.width <= p.boreDiameter * 0.3) + context.addIssue({ + code: "custom", + message: "Width must leave a useful spherical inner ring", + }) + if (Math.sqrt(p.ballDiameter ** 2 - p.width ** 2) <= p.boreDiameter) + context.addIssue({ + code: "custom", + message: "The sphere section at both end faces must surround the bore", + }) +} +export const sphericalBearingModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const sphericalBearingModelDefinitionSchema = z + .object({ fn: z.literal("sphericalbearing"), ...shape }) + .strict() + .superRefine(validate) +export type SphericalBearingModelPropsInput = z.input< + typeof sphericalBearingModelPropsSchema +> +export type SphericalBearingModelProps = z.output< + typeof sphericalBearingModelPropsSchema +> +export type SphericalBearingModelDefinition = z.output< + typeof sphericalBearingModelDefinitionSchema +> +/** Spherical plain bearing assembly: cylindrical outer race with spherical pocket and truncated spherical inner ring with an axial bore. Axis Z, bottom Z=0. Outer race is recessed to 72% of inner-ring width to expose the spherical shoulders. Small nominal spherical clearance; articulation and tolerances are not modeled. All lengths are normalized to millimeters. */ +export function getSphericalBearingDimensions( + input: SphericalBearingModelPropsInput = {}, +) { + const p = sphericalBearingModelPropsSchema.parse(input) + return { + ...p, + size: [p.outerDiameter, p.outerDiameter, p.width] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.width, + } +} diff --git a/src/models/splitringhanger/index.ts b/src/models/splitringhanger/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/splitringhanger/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/splitringhanger/parse-model-string.ts b/src/models/splitringhanger/parse-model-string.ts new file mode 100644 index 0000000..a68a6a7 --- /dev/null +++ b/src/models/splitringhanger/parse-model-string.ts @@ -0,0 +1,19 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { splitRingHangerModelDefinitionSchema } from "./schema" +const aliases = { + pipediameter: "pipeDiameter", + bandthickness: "bandThickness", + bandwidth: "bandWidth", + gap: "gap", + lugwidth: "lugWidth", + holediameter: "holeDiameter", +} as const +export function parseSplitRingHangerModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "splitringhanger", + splitRingHangerModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/splitringhanger/register.ts b/src/models/splitringhanger/register.ts new file mode 100644 index 0000000..19f6b38 --- /dev/null +++ b/src/models/splitringhanger/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { splitRingHangerModelDefinitionSchema } from "./schema" +import { parseSplitRingHangerModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "splitringhanger", + schema: splitRingHangerModelDefinitionSchema, + parse: parseSplitRingHangerModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/splitringhanger/schema.ts b/src/models/splitringhanger/schema.ts new file mode 100644 index 0000000..189a3ca --- /dev/null +++ b/src/models/splitringhanger/schema.ts @@ -0,0 +1,63 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + pipeDiameter: positiveLength.default(32), + bandThickness: positiveLength.default(3), + bandWidth: positiveLength.default(15), + gap: positiveLength.default(2), + lugWidth: positiveLength.default(14), + holeDiameter: positiveLength.default(5), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.bandThickness < p.pipeDiameter / 4)) + context.addIssue({ + code: "custom", + message: + "Invalid splitringhanger dimensions: bandThickness < pipeDiameter/4", + }) + if (!(p.gap < p.bandThickness)) + context.addIssue({ + code: "custom", + message: "Invalid splitringhanger dimensions: gap < bandThickness", + }) + if (!(p.lugWidth > 2 * p.holeDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid splitringhanger dimensions: lugWidth > 2*holeDiameter", + }) + if (!(p.bandWidth > 2 * p.holeDiameter)) + context.addIssue({ + code: "custom", + message: "Invalid splitringhanger dimensions: bandWidth > 2*holeDiameter", + }) +} +export const splitRingHangerModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const splitRingHangerModelDefinitionSchema = z + .object({ fn: z.literal("splitringhanger"), ...shape }) + .strict() + .superRefine(validate) +export type SplitRingHangerModelPropsInput = z.input< + typeof splitRingHangerModelPropsSchema +> +export type SplitRingHangerModelProps = z.output< + typeof splitRingHangerModelPropsSchema +> +export type SplitRingHangerModelDefinition = z.output< + typeof splitRingHangerModelDefinitionSchema +> +/** Nominal two-piece pipe hanger: pipe axis Y, horizontal split, lug bolt holes and top mounting boss. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getSplitRingHangerDimensions( + input: SplitRingHangerModelPropsInput = {}, +) { + return splitRingHangerModelPropsSchema.parse(input) +} diff --git a/src/models/springpin/index.ts b/src/models/springpin/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/springpin/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/springpin/parse-model-string.ts b/src/models/springpin/parse-model-string.ts new file mode 100644 index 0000000..de3603a --- /dev/null +++ b/src/models/springpin/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { springPinModelDefinitionSchema } from "./schema" +export function parseSpringPinModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "springpin", + springPinModelDefinitionSchema, + { + od: "outerDiameter", + outerdiameter: "outerDiameter", + l: "length", + length: "length", + wt: "wallThickness", + wallthickness: "wallThickness", + g: "gapWidth", + gapwidth: "gapWidth", + }, + ) +} diff --git a/src/models/springpin/register.ts b/src/models/springpin/register.ts new file mode 100644 index 0000000..fb0456a --- /dev/null +++ b/src/models/springpin/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { springPinModelDefinitionSchema } from "./schema" +import { parseSpringPinModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "springpin", + schema: springPinModelDefinitionSchema, + parse: parseSpringPinModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/springpin/schema.ts b/src/models/springpin/schema.ts new file mode 100644 index 0000000..84ed117 --- /dev/null +++ b/src/models/springpin/schema.ts @@ -0,0 +1,66 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + outerDiameter: positiveLength.default(6), + length: positiveLength.default(20), + wallThickness: positiveLength.default(1), + gapWidth: positiveLength.default(1), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![ + p.wallThickness, + (p.outerDiameter - 2 * p.wallThickness - p.gapWidth) / 2, + ].every((gap) => gap >= extent * 0.0001) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if ( + !( + 2 * p.wallThickness < p.outerDiameter && + p.gapWidth < p.outerDiameter - 2 * p.wallThickness + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Invalid springpin dimensions: mating features must fit within the body", + }) +} +export const springPinModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const springPinModelDefinitionSchema = z + .object({ fn: z.literal("springpin"), ...shape }) + .strict() + .superRefine(validate) +export type SpringPinModelPropsInput = z.input +export type SpringPinModelProps = z.output +export type SpringPinModelDefinition = z.output< + typeof springPinModelDefinitionSchema +> +export function getSpringPinDimensions(input: SpringPinModelPropsInput = {}) { + return springPinModelPropsSchema.parse(input) +} diff --git a/src/models/sprocket/index.ts b/src/models/sprocket/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/sprocket/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/sprocket/parse-model-string.ts b/src/models/sprocket/parse-model-string.ts new file mode 100644 index 0000000..2aa38c8 --- /dev/null +++ b/src/models/sprocket/parse-model-string.ts @@ -0,0 +1,27 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { sprocketModelDefinitionSchema } from "./schema" +const aliases = { + od: "outerDiameter", + root: "rootDiameter", + w: "width", + bore: "boreDiameter", + teeth: "toothCount", + hub: "hubDiameter", + hh: "hubHeight", + outerdiameter: "outerDiameter", + rootdiameter: "rootDiameter", + width: "width", + borediameter: "boreDiameter", + toothcount: "toothCount", + hubdiameter: "hubDiameter", + hubheight: "hubHeight", +} as const +export function parseSprocketModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "sprocket", + sprocketModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/sprocket/register.ts b/src/models/sprocket/register.ts new file mode 100644 index 0000000..460de17 --- /dev/null +++ b/src/models/sprocket/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { sprocketModelDefinitionSchema } from "./schema" +import { parseSprocketModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "sprocket", + schema: sprocketModelDefinitionSchema, + parse: parseSprocketModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/sprocket/schema.ts b/src/models/sprocket/schema.ts new file mode 100644 index 0000000..f8406a4 --- /dev/null +++ b/src/models/sprocket/schema.ts @@ -0,0 +1,76 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(60), + rootDiameter: positiveLength.default(48), + width: positiveLength.default(8), + boreDiameter: positiveLength.default(12), + toothCount: z.number().int().min(2).max(64).default(16), + hubDiameter: positiveLength.default(26), + hubHeight: positiveLength.default(10), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.outerDiameter <= p.rootDiameter) + context.addIssue({ + code: "custom", + message: "Outside diameter must exceed the tooth-root diameter", + }) + if (p.rootDiameter <= p.hubDiameter) + context.addIssue({ + code: "custom", + message: "Tooth-root diameter must exceed the hub diameter", + }) + if (p.hubDiameter <= p.boreDiameter) + context.addIssue({ + code: "custom", + message: "Hub must leave material around the shaft bore", + }) + if (p.outerDiameter - p.rootDiameter >= p.rootDiameter / 2) + context.addIssue({ + code: "custom", + message: "Tooth height must be less than half the root radius", + }) + const pocketRadius = (p.outerDiameter - p.rootDiameter) * 0.3 + if ( + pocketRadius >= + (p.rootDiameter / 2 + pocketRadius) * Math.sin(Math.PI / p.toothCount) + ) + context.addIssue({ + code: "custom", + message: "Adjacent roller pockets must leave a positive tooth bridge", + }) +} +export const sprocketModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const sprocketModelDefinitionSchema = z + .object({ fn: z.literal("sprocket"), ...shape }) + .strict() + .superRefine(validate) +export type SprocketModelPropsInput = z.input +export type SprocketModelProps = z.output +export type SprocketModelDefinition = z.output< + typeof sprocketModelDefinitionSchema +> +/** Chain-sprocket envelope with circular roller pockets between radial teeth, hub shoulder and through bore. Axis Z, bottom Z=0. Tooth/pocket outline is nominal, not a chain pitch standard, conjugate roller seating profile or cutting specification. All lengths are normalized to millimeters. */ +export function getSprocketDimensions(input: SprocketModelPropsInput = {}) { + const p = sprocketModelPropsSchema.parse(input) + return { + ...p, + size: [p.outerDiameter, p.outerDiameter, p.width + p.hubHeight] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.width + p.hubHeight, + } +} diff --git a/src/models/squarekey/index.ts b/src/models/squarekey/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/squarekey/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/squarekey/parse-model-string.ts b/src/models/squarekey/parse-model-string.ts new file mode 100644 index 0000000..6768703 --- /dev/null +++ b/src/models/squarekey/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { squareKeyModelDefinitionSchema } from "./schema" +const aliases = { + width: "width", + height: "height", + length: "length", + chamfer: "chamfer", + w: "width", + h: "height", + l: "length", +} as const +export function parseSquareKeyModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "squarekey", + squareKeyModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/squarekey/register.ts b/src/models/squarekey/register.ts new file mode 100644 index 0000000..af80c89 --- /dev/null +++ b/src/models/squarekey/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { squareKeyModelDefinitionSchema } from "./schema" +import { parseSquareKeyModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "squarekey", + schema: squareKeyModelDefinitionSchema, + parse: parseSquareKeyModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/squarekey/schema.ts b/src/models/squarekey/schema.ts new file mode 100644 index 0000000..215baef --- /dev/null +++ b/src/models/squarekey/schema.ts @@ -0,0 +1,45 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + width: positiveLength.default(8), + height: positiveLength.default(8), + length: positiveLength.default(40), + chamfer: positiveLength.default(1), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.length > p.width)) + context.addIssue({ + code: "custom", + message: "Invalid squarekey dimensions: length > width", + }) + if (!(p.chamfer < Math.min(p.width, p.length) / 3)) + context.addIssue({ + code: "custom", + message: + "Invalid squarekey dimensions: chamfer < Math.min(width,length)/3", + }) +} +export const squareKeyModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const squareKeyModelDefinitionSchema = z + .object({ fn: z.literal("squarekey"), ...shape }) + .strict() + .superRefine(validate) +export type SquareKeyModelPropsInput = z.input +export type SquareKeyModelProps = z.output +export type SquareKeyModelDefinition = z.output< + typeof squareKeyModelDefinitionSchema +> +/** Parallel shaft key, length X, width Y, height Z; clipped plan corners, bottom Z=0. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getSquareKeyDimensions(input: SquareKeyModelPropsInput = {}) { + return squareKeyModelPropsSchema.parse(input) +} diff --git a/src/models/starknob/index.ts b/src/models/starknob/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/starknob/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/starknob/parse-model-string.ts b/src/models/starknob/parse-model-string.ts new file mode 100644 index 0000000..bb5f21a --- /dev/null +++ b/src/models/starknob/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { starKnobModelDefinitionSchema } from "./schema" +const aliases = { + diameter: "diameter", + height: "height", + borediameter: "boreDiameter", + lobes: "lobes", + hubdiameter: "hubDiameter", + hubheight: "hubHeight", + h: "height", + d: "diameter", + bore: "boreDiameter", +} as const +export function parseStarKnobModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "starknob", + starKnobModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/starknob/register.ts b/src/models/starknob/register.ts new file mode 100644 index 0000000..7aa7e9d --- /dev/null +++ b/src/models/starknob/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { starKnobModelDefinitionSchema } from "./schema" +import { parseStarKnobModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "starknob", + schema: starKnobModelDefinitionSchema, + parse: parseStarKnobModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/starknob/schema.ts b/src/models/starknob/schema.ts new file mode 100644 index 0000000..3dd1314 --- /dev/null +++ b/src/models/starknob/schema.ts @@ -0,0 +1,51 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + diameter: positiveLength.default(40), + height: positiveLength.default(16), + boreDiameter: positiveLength.default(6), + lobes: z.number().int().min(3).max(10).default(5), + hubDiameter: positiveLength.default(18), + hubHeight: positiveLength.default(10), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.boreDiameter < p.hubDiameter * 0.8)) + context.addIssue({ + code: "custom", + message: "Invalid starknob dimensions: boreDiameter < hubDiameter*.8", + }) + if (!(p.hubDiameter < p.diameter * 0.6)) + context.addIssue({ + code: "custom", + message: "Invalid starknob dimensions: hubDiameter < diameter*.6", + }) + if (!(p.height < p.diameter)) + context.addIssue({ + code: "custom", + message: "Invalid starknob dimensions: height < diameter", + }) +} +export const starKnobModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const starKnobModelDefinitionSchema = z + .object({ fn: z.literal("starknob"), ...shape }) + .strict() + .superRefine(validate) +export type StarKnobModelPropsInput = z.input +export type StarKnobModelProps = z.output +export type StarKnobModelDefinition = z.output< + typeof starKnobModelDefinitionSchema +> +/** Lobed star knob with bottom hub and axial through bore, axis Z. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getStarKnobDimensions(input: StarKnobModelPropsInput = {}) { + return starKnobModelPropsSchema.parse(input) +} diff --git a/src/models/taperpin/index.ts b/src/models/taperpin/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/taperpin/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/taperpin/parse-model-string.ts b/src/models/taperpin/parse-model-string.ts new file mode 100644 index 0000000..41897bd --- /dev/null +++ b/src/models/taperpin/parse-model-string.ts @@ -0,0 +1,18 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { taperPinModelDefinitionSchema } from "./schema" +export function parseTaperPinModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "taperpin", + taperPinModelDefinitionSchema, + { + ld: "largeDiameter", + largediameter: "largeDiameter", + sd: "smallDiameter", + smalldiameter: "smallDiameter", + l: "length", + length: "length", + }, + ) +} diff --git a/src/models/taperpin/register.ts b/src/models/taperpin/register.ts new file mode 100644 index 0000000..ec406ea --- /dev/null +++ b/src/models/taperpin/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { taperPinModelDefinitionSchema } from "./schema" +import { parseTaperPinModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "taperpin", + schema: taperPinModelDefinitionSchema, + parse: parseTaperPinModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/taperpin/schema.ts b/src/models/taperpin/schema.ts new file mode 100644 index 0000000..850d10d --- /dev/null +++ b/src/models/taperpin/schema.ts @@ -0,0 +1,59 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + largeDiameter: positiveLength.default(6), + smallDiameter: positiveLength.default(5), + length: positiveLength.default(30), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![(p.largeDiameter - p.smallDiameter) / 2].every( + (gap) => gap >= extent * 0.0001, + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if (!(p.smallDiameter < p.largeDiameter)) + ctx.addIssue({ + code: "custom", + message: + "Invalid taperpin dimensions: mating features must fit within the body", + }) +} +export const taperPinModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const taperPinModelDefinitionSchema = z + .object({ fn: z.literal("taperpin"), ...shape }) + .strict() + .superRefine(validate) +export type TaperPinModelPropsInput = z.input +export type TaperPinModelProps = z.output +export type TaperPinModelDefinition = z.output< + typeof taperPinModelDefinitionSchema +> +export function getTaperPinDimensions(input: TaperPinModelPropsInput = {}) { + return taperPinModelPropsSchema.parse(input) +} diff --git a/src/models/teebar/index.ts b/src/models/teebar/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/teebar/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/teebar/parse-model-string.ts b/src/models/teebar/parse-model-string.ts new file mode 100644 index 0000000..ca74f62 --- /dev/null +++ b/src/models/teebar/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { teeBarModelDefinitionSchema } from "./schema" +const aliases = { + width: "width", + height: "height", + webthickness: "webThickness", + flangethickness: "flangeThickness", + length: "length", + w: "width", + h: "height", + l: "length", +} as const +export function parseTeeBarModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "teebar", + teeBarModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/teebar/register.ts b/src/models/teebar/register.ts new file mode 100644 index 0000000..080891a --- /dev/null +++ b/src/models/teebar/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { teeBarModelDefinitionSchema } from "./schema" +import { parseTeeBarModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "teebar", + schema: teeBarModelDefinitionSchema, + parse: parseTeeBarModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/teebar/schema.ts b/src/models/teebar/schema.ts new file mode 100644 index 0000000..e27911d --- /dev/null +++ b/src/models/teebar/schema.ts @@ -0,0 +1,43 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + width: positiveLength.default(40), + height: positiveLength.default(40), + webThickness: positiveLength.default(4), + flangeThickness: positiveLength.default(6), + length: positiveLength.default(100), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.webThickness < p.width)) + context.addIssue({ + code: "custom", + message: "Invalid teebar dimensions: webThickness < width", + }) + if (!(p.flangeThickness < p.height)) + context.addIssue({ + code: "custom", + message: "Invalid teebar dimensions: flangeThickness < height", + }) +} +export const teeBarModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const teeBarModelDefinitionSchema = z + .object({ fn: z.literal("teebar"), ...shape }) + .strict() + .superRefine(validate) +export type TeeBarModelPropsInput = z.input +export type TeeBarModelProps = z.output +export type TeeBarModelDefinition = z.output +/** T-section stock, centered XY envelope, flange at maximum Y, length +Z. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getTeeBarDimensions(input: TeeBarModelPropsInput = {}) { + return teeBarModelPropsSchema.parse(input) +} diff --git a/src/models/terminalblock/index.ts b/src/models/terminalblock/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/terminalblock/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/terminalblock/parse-model-string.ts b/src/models/terminalblock/parse-model-string.ts new file mode 100644 index 0000000..e846da9 --- /dev/null +++ b/src/models/terminalblock/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { terminalBlockModelDefinitionSchema } from "./schema" +const aliases = { + width: "width", + depth: "depth", + height: "height", + wirediameter: "wireDiameter", + screwdiameter: "screwDiameter", + w: "width", + h: "height", +} as const +export function parseTerminalBlockModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "terminalblock", + terminalBlockModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/terminalblock/register.ts b/src/models/terminalblock/register.ts new file mode 100644 index 0000000..105139e --- /dev/null +++ b/src/models/terminalblock/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { terminalBlockModelDefinitionSchema } from "./schema" +import { parseTerminalBlockModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "terminalblock", + schema: terminalBlockModelDefinitionSchema, + parse: parseTerminalBlockModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/terminalblock/schema.ts b/src/models/terminalblock/schema.ts new file mode 100644 index 0000000..6580954 --- /dev/null +++ b/src/models/terminalblock/schema.ts @@ -0,0 +1,69 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + width: positive.default(6), + depth: positive.default(40), + height: positive.default(35), + wireDiameter: positive.default(3), + screwDiameter: positive.default(4), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.wireDiameter >= p.width) + context.addIssue({ + code: "custom", + message: "Incompatible terminalblock dimensions: 1", + }) + if (p.screwDiameter >= p.width) + context.addIssue({ + code: "custom", + message: "Incompatible terminalblock dimensions: 2", + }) + if (p.wireDiameter >= p.height / 3) + context.addIssue({ + code: "custom", + message: "Incompatible terminalblock dimensions: 3", + }) + if (p.screwDiameter >= p.depth / 4) + context.addIssue({ + code: "custom", + message: "Incompatible terminalblock dimensions: 4", + }) +} +export const terminalBlockModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const terminalBlockModelDefinitionSchema = z + .object({ fn: z.literal("terminalblock"), ...shape }) + .strict() + .superRefine(validate) +export type TerminalBlockModelPropsInput = z.input< + typeof terminalBlockModelPropsSchema +> +export type TerminalBlockModelProps = z.output< + typeof terminalBlockModelPropsSchema +> +export type TerminalBlockModelDefinition = z.output< + typeof terminalBlockModelDefinitionSchema +> +/** Single DIN terminal block with two wire-entry bores, paired slotted screw heads, center separator recess and underside rail clip recess. The insulating body is an envelope; screw thread, conductive cage and rail latching mechanics are omitted. All normalized lengths are millimeters. */ +export function getTerminalBlockDimensions( + input: TerminalBlockModelPropsInput = {}, +) { + const p = terminalBlockModelPropsSchema.parse(input) + return { + ...p, + size: [p.width, p.depth, p.height + 5] as [number, number, number], + } +} diff --git a/src/models/toggleclamp/index.ts b/src/models/toggleclamp/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/toggleclamp/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/toggleclamp/parse-model-string.ts b/src/models/toggleclamp/parse-model-string.ts new file mode 100644 index 0000000..855943d --- /dev/null +++ b/src/models/toggleclamp/parse-model-string.ts @@ -0,0 +1,22 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { toggleClampModelDefinitionSchema } from "./schema" +const aliases = { + length: "length", + width: "width", + basethickness: "baseThickness", + handleheight: "handleHeight", + armlength: "armLength", + paddiameter: "padDiameter", + holediameter: "holeDiameter", + w: "width", + l: "length", +} as const +export function parseToggleClampModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "toggleclamp", + toggleClampModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/toggleclamp/register.ts b/src/models/toggleclamp/register.ts new file mode 100644 index 0000000..244afe2 --- /dev/null +++ b/src/models/toggleclamp/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { toggleClampModelDefinitionSchema } from "./schema" +import { parseToggleClampModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "toggleclamp", + schema: toggleClampModelDefinitionSchema, + parse: parseToggleClampModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/toggleclamp/schema.ts b/src/models/toggleclamp/schema.ts new file mode 100644 index 0000000..50f2e97 --- /dev/null +++ b/src/models/toggleclamp/schema.ts @@ -0,0 +1,76 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + length: positiveLength.default(110), + width: positiveLength.default(35), + baseThickness: positiveLength.default(5), + handleHeight: positiveLength.default(55), + armLength: positiveLength.default(75), + padDiameter: positiveLength.default(14), + holeDiameter: positiveLength.default(5), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.length > 2 * p.width)) + context.addIssue({ + code: "custom", + message: "Invalid toggleclamp dimensions: length > 2*width", + }) + if (!(p.handleHeight > p.width)) + context.addIssue({ + code: "custom", + message: "Invalid toggleclamp dimensions: handleHeight > width", + }) + if (!(p.armLength > p.width)) + context.addIssue({ + code: "custom", + message: "Invalid toggleclamp dimensions: armLength > width", + }) + if (!(p.armLength < 2 * p.length)) + context.addIssue({ + code: "custom", + message: "Invalid toggleclamp dimensions: armLength < 2*length", + }) + if (!(p.padDiameter < p.width / 2)) + context.addIssue({ + code: "custom", + message: "Invalid toggleclamp dimensions: padDiameter < width/2", + }) + if (!(p.holeDiameter < p.width / 4)) + context.addIssue({ + code: "custom", + message: "Invalid toggleclamp dimensions: holeDiameter < width/4", + }) + if (!(p.baseThickness < p.width / 4)) + context.addIssue({ + code: "custom", + message: "Invalid toggleclamp dimensions: baseThickness < width/4", + }) +} +export const toggleClampModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const toggleClampModelDefinitionSchema = z + .object({ fn: z.literal("toggleclamp"), ...shape }) + .strict() + .superRefine(validate) +export type ToggleClampModelPropsInput = z.input< + typeof toggleClampModelPropsSchema +> +export type ToggleClampModelProps = z.output +export type ToggleClampModelDefinition = z.output< + typeof toggleClampModelDefinitionSchema +> +/** Nominal closed hold-down toggle clamp, long axis X, handle upright, bottom Z=0. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getToggleClampDimensions( + input: ToggleClampModelPropsInput = {}, +) { + return toggleClampModelPropsSchema.parse(input) +} diff --git a/src/models/toothedwasher/index.ts b/src/models/toothedwasher/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/toothedwasher/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/toothedwasher/parse-model-string.ts b/src/models/toothedwasher/parse-model-string.ts new file mode 100644 index 0000000..8f4ba81 --- /dev/null +++ b/src/models/toothedwasher/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { toothedWasherModelDefinitionSchema } from "./schema" +export function parseToothedWasherModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "toothedwasher", + toothedWasherModelDefinitionSchema, + { + id: "innerDiameter", + innerdiameter: "innerDiameter", + od: "outerDiameter", + outerdiameter: "outerDiameter", + t: "thickness", + thickness: "thickness", + td: "toothDepth", + toothdepth: "toothDepth", + }, + ) +} diff --git a/src/models/toothedwasher/register.ts b/src/models/toothedwasher/register.ts new file mode 100644 index 0000000..c56a139 --- /dev/null +++ b/src/models/toothedwasher/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { toothedWasherModelDefinitionSchema } from "./schema" +import { parseToothedWasherModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "toothedwasher", + schema: toothedWasherModelDefinitionSchema, + parse: parseToothedWasherModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/toothedwasher/schema.ts b/src/models/toothedwasher/schema.ts new file mode 100644 index 0000000..efe3952 --- /dev/null +++ b/src/models/toothedwasher/schema.ts @@ -0,0 +1,66 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + innerDiameter: positiveLength.default(6.4), + outerDiameter: positiveLength.default(13), + thickness: positiveLength.default(0.8), + toothDepth: positiveLength.default(1.5), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![(p.outerDiameter - p.innerDiameter - 2 * p.toothDepth) / 2].every( + (gap) => gap >= extent * 0.0001, + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if (!(p.innerDiameter < p.outerDiameter - 2 * p.toothDepth)) + ctx.addIssue({ + code: "custom", + message: + "Invalid toothedwasher dimensions: mating features must fit within the body", + }) +} +export const toothedWasherModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const toothedWasherModelDefinitionSchema = z + .object({ fn: z.literal("toothedwasher"), ...shape }) + .strict() + .superRefine(validate) +export type ToothedWasherModelPropsInput = z.input< + typeof toothedWasherModelPropsSchema +> +export type ToothedWasherModelProps = z.output< + typeof toothedWasherModelPropsSchema +> +export type ToothedWasherModelDefinition = z.output< + typeof toothedWasherModelDefinitionSchema +> +export function getToothedWasherDimensions( + input: ToothedWasherModelPropsInput = {}, +) { + return toothedWasherModelPropsSchema.parse(input) +} diff --git a/src/models/tslotnut/index.ts b/src/models/tslotnut/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/tslotnut/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/tslotnut/parse-model-string.ts b/src/models/tslotnut/parse-model-string.ts new file mode 100644 index 0000000..429a40b --- /dev/null +++ b/src/models/tslotnut/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { tSlotNutModelDefinitionSchema } from "./schema" +export function parseTSlotNutModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "tslotnut", + tSlotNutModelDefinitionSchema, + { + id: "boreDiameter", + borediameter: "boreDiameter", + w: "baseWidth", + basewidth: "baseWidth", + l: "length", + length: "length", + bh: "baseHeight", + baseheight: "baseHeight", + nw: "neckWidth", + neckwidth: "neckWidth", + nh: "neckHeight", + neckheight: "neckHeight", + }, + ) +} diff --git a/src/models/tslotnut/register.ts b/src/models/tslotnut/register.ts new file mode 100644 index 0000000..a12b1ca --- /dev/null +++ b/src/models/tslotnut/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { tSlotNutModelDefinitionSchema } from "./schema" +import { parseTSlotNutModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "tslotnut", + schema: tSlotNutModelDefinitionSchema, + parse: parseTSlotNutModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/tslotnut/schema.ts b/src/models/tslotnut/schema.ts new file mode 100644 index 0000000..f4883c0 --- /dev/null +++ b/src/models/tslotnut/schema.ts @@ -0,0 +1,70 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + boreDiameter: positiveLength.default(6), + baseWidth: positiveLength.default(16), + length: positiveLength.default(20), + baseHeight: positiveLength.default(4), + neckWidth: positiveLength.default(10), + neckHeight: positiveLength.default(4), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![ + (p.neckWidth - p.boreDiameter) / 2, + (p.length - p.boreDiameter) / 2, + (p.baseWidth - p.neckWidth) / 2, + ].every((gap) => gap >= extent * 0.0001) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if ( + !( + p.boreDiameter < p.neckWidth && + p.neckWidth < p.baseWidth && + p.boreDiameter < p.length + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Invalid tslotnut dimensions: mating features must fit within the body", + }) +} +export const tSlotNutModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const tSlotNutModelDefinitionSchema = z + .object({ fn: z.literal("tslotnut"), ...shape }) + .strict() + .superRefine(validate) +export type TSlotNutModelPropsInput = z.input +export type TSlotNutModelProps = z.output +export type TSlotNutModelDefinition = z.output< + typeof tSlotNutModelDefinitionSchema +> +export function getTSlotNutDimensions(input: TSlotNutModelPropsInput = {}) { + return tSlotNutModelPropsSchema.parse(input) +} diff --git a/src/models/ubolt/index.ts b/src/models/ubolt/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/ubolt/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/ubolt/parse-model-string.ts b/src/models/ubolt/parse-model-string.ts new file mode 100644 index 0000000..5657883 --- /dev/null +++ b/src/models/ubolt/parse-model-string.ts @@ -0,0 +1,13 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { uBoltModelDefinitionSchema } from "./schema" +export function parseUBoltModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams(raw, "ubolt", uBoltModelDefinitionSchema, { + d: "rodDiameter", + roddiameter: "rodDiameter", + s: "legSpacing", + legspacing: "legSpacing", + l: "legLength", + leglength: "legLength", + }) +} diff --git a/src/models/ubolt/register.ts b/src/models/ubolt/register.ts new file mode 100644 index 0000000..8d0dd79 --- /dev/null +++ b/src/models/ubolt/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { uBoltModelDefinitionSchema } from "./schema" +import { parseUBoltModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "ubolt", + schema: uBoltModelDefinitionSchema, + parse: parseUBoltModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/ubolt/schema.ts b/src/models/ubolt/schema.ts new file mode 100644 index 0000000..8383a43 --- /dev/null +++ b/src/models/ubolt/schema.ts @@ -0,0 +1,53 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + rodDiameter: positiveLength.default(6), + legSpacing: positiveLength.default(30), + legLength: positiveLength.default(35), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if (![p.legSpacing - p.rodDiameter].every((gap) => gap >= extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if (!(p.rodDiameter < p.legSpacing)) + ctx.addIssue({ + code: "custom", + message: + "Invalid ubolt dimensions: mating features must fit within the body", + }) +} +export const uBoltModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const uBoltModelDefinitionSchema = z + .object({ fn: z.literal("ubolt"), ...shape }) + .strict() + .superRefine(validate) +export type UBoltModelPropsInput = z.input +export type UBoltModelProps = z.output +export type UBoltModelDefinition = z.output +export function getUBoltDimensions(input: UBoltModelPropsInput = {}) { + return uBoltModelPropsSchema.parse(input) +} diff --git a/src/models/universaljoint/index.ts b/src/models/universaljoint/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/universaljoint/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/universaljoint/parse-model-string.ts b/src/models/universaljoint/parse-model-string.ts new file mode 100644 index 0000000..a319722 --- /dev/null +++ b/src/models/universaljoint/parse-model-string.ts @@ -0,0 +1,26 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { universalJointModelDefinitionSchema } from "./schema" +const aliases = { + od: "outerDiameter", + l: "length", + bore: "boreDiameter", + hubl: "hubLength", + armt: "armThickness", + crossd: "crossDiameter", + clearance: "clearance", + outerdiameter: "outerDiameter", + length: "length", + borediameter: "boreDiameter", + hublength: "hubLength", + armthickness: "armThickness", + crossdiameter: "crossDiameter", +} as const +export function parseUniversalJointModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "universaljoint", + universalJointModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/universaljoint/register.ts b/src/models/universaljoint/register.ts new file mode 100644 index 0000000..2204e7b --- /dev/null +++ b/src/models/universaljoint/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { universalJointModelDefinitionSchema } from "./schema" +import { parseUniversalJointModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "universaljoint", + schema: universalJointModelDefinitionSchema, + parse: parseUniversalJointModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/universaljoint/schema.ts b/src/models/universaljoint/schema.ts new file mode 100644 index 0000000..ca0de89 --- /dev/null +++ b/src/models/universaljoint/schema.ts @@ -0,0 +1,96 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (v) => v > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(30), + length: positiveLength.default(60), + boreDiameter: positiveLength.default(8), + hubLength: positiveLength.default(14), + armThickness: positiveLength.default(5), + crossDiameter: positiveLength.default(6), + clearance: industrialLengthSchema + .refine((v) => v >= 0, "Clearance cannot be negative") + .default(0.2), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.armThickness * 3 >= p.outerDiameter) + context.addIssue({ + code: "custom", + message: "Fork arms must leave room between opposing ears", + }) + if (p.boreDiameter >= p.outerDiameter - 2 * p.armThickness) + context.addIssue({ + code: "custom", + message: "Shaft bore must leave solid material behind each fork", + }) + if (p.hubLength >= p.length / 2 - p.crossDiameter) + context.addIssue({ + code: "custom", + message: "Hub lengths must leave fork arms around the center cross", + }) + if (p.crossDiameter * 1.8 >= p.outerDiameter - 2 * p.armThickness) + context.addIssue({ + code: "custom", + message: "The perpendicular forks must clear one another", + }) + if (p.crossDiameter / 2 + p.clearance >= (p.crossDiameter * 1.8) / 2) + context.addIssue({ + code: "custom", + message: "Cross clearance must leave material around its journal holes", + }) + const bounds = [ + p.outerDiameter, + p.length, + p.boreDiameter, + p.hubLength, + p.armThickness, + p.crossDiameter, + ] + if (!Number.isFinite(bounds.reduce((sum, value) => sum + value, 0))) + context.addIssue({ + code: "custom", + message: "Combined dimensions must remain finite", + }) +} +export const universalJointModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const universalJointModelDefinitionSchema = z + .object({ fn: z.literal("universaljoint"), ...shape }) + .strict() + .superRefine(validate) +export type UniversalJointModelPropsInput = z.input< + typeof universalJointModelPropsSchema +> +export type UniversalJointModelProps = z.output< + typeof universalJointModelPropsSchema +> +export type UniversalJointModelDefinition = z.output< + typeof universalJointModelDefinitionSchema +> +/** Straight pose of a universal joint with two orthogonal forks, transverse journal holes, and a solid four-arm center cross. Both shaft axes are Z; the center cross lies at Z=length/2. All lengths normalize to millimeters. */ +export function getUniversalJointDimensions( + input: UniversalJointModelPropsInput = {}, +) { + const p = universalJointModelPropsSchema.parse(input) + return { + ...p, + size: [p.outerDiameter, p.outerDiameter, p.length] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.length, + crossZ: p.length / 2, + crossSpan: p.outerDiameter - p.armThickness, + earWidth: p.crossDiameter * 1.8, + } +} diff --git a/src/models/vacuumcup/index.ts b/src/models/vacuumcup/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/vacuumcup/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/vacuumcup/parse-model-string.ts b/src/models/vacuumcup/parse-model-string.ts new file mode 100644 index 0000000..72a44f4 --- /dev/null +++ b/src/models/vacuumcup/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { vacuumCupModelDefinitionSchema } from "./schema" +const aliases = { + diameter: "diameter", + height: "height", + neckdiameter: "neckDiameter", + necklength: "neckLength", + wallthickness: "wallThickness", + borediameter: "boreDiameter", + d: "diameter", + h: "height", +} as const +export function parseVacuumCupModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "vacuumcup", + vacuumCupModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/vacuumcup/register.ts b/src/models/vacuumcup/register.ts new file mode 100644 index 0000000..2b3ed7c --- /dev/null +++ b/src/models/vacuumcup/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { vacuumCupModelDefinitionSchema } from "./schema" +import { parseVacuumCupModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "vacuumcup", + schema: vacuumCupModelDefinitionSchema, + parse: parseVacuumCupModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/vacuumcup/schema.ts b/src/models/vacuumcup/schema.ts new file mode 100644 index 0000000..7c4514e --- /dev/null +++ b/src/models/vacuumcup/schema.ts @@ -0,0 +1,68 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + diameter: positive.default(35), + height: positive.default(22), + neckDiameter: positive.default(12), + neckLength: positive.default(12), + wallThickness: positive.default(2), + boreDiameter: positive.default(6), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.neckDiameter >= p.diameter) + context.addIssue({ + code: "custom", + message: "Incompatible vacuumcup dimensions: 1", + }) + if (2 * p.wallThickness >= p.neckDiameter - p.boreDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible vacuumcup dimensions: 2", + }) + if (p.boreDiameter >= p.neckDiameter) + context.addIssue({ + code: "custom", + message: "Incompatible vacuumcup dimensions: 3", + }) + if (p.wallThickness >= p.height / 2) + context.addIssue({ + code: "custom", + message: "Incompatible vacuumcup dimensions: 4", + }) +} +export const vacuumCupModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const vacuumCupModelDefinitionSchema = z + .object({ fn: z.literal("vacuumcup"), ...shape }) + .strict() + .superRefine(validate) +export type VacuumCupModelPropsInput = z.input +export type VacuumCupModelProps = z.output +export type VacuumCupModelDefinition = z.output< + typeof vacuumCupModelDefinitionSchema +> +/** Tapered vacuum suction cup with a hollow bell, sealing lip and through-bored connection neck. Cup lip rests on Z=0; no flexible deformation, thread form or vacuum rating is modeled. All normalized lengths are millimeters. */ +export function getVacuumCupDimensions(input: VacuumCupModelPropsInput = {}) { + const p = vacuumCupModelPropsSchema.parse(input) + return { + ...p, + size: [ + p.diameter + 2 * p.wallThickness, + p.diameter + 2 * p.wallThickness, + p.height + p.neckLength, + ] as [number, number, number], + } +} diff --git a/src/models/vbeltsheave/index.ts b/src/models/vbeltsheave/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/vbeltsheave/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/vbeltsheave/parse-model-string.ts b/src/models/vbeltsheave/parse-model-string.ts new file mode 100644 index 0000000..9c6cbf3 --- /dev/null +++ b/src/models/vbeltsheave/parse-model-string.ts @@ -0,0 +1,29 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { vBeltSheaveModelDefinitionSchema } from "./schema" +const aliases = { + od: "outerDiameter", + bore: "boreDiameter", + w: "width", + grooves: "grooveCount", + depth: "grooveDepth", + gw: "grooveWidth", + hub: "hubDiameter", + hh: "hubHeight", + outerdiameter: "outerDiameter", + borediameter: "boreDiameter", + width: "width", + groovecount: "grooveCount", + groovedepth: "grooveDepth", + groovewidth: "grooveWidth", + hubdiameter: "hubDiameter", + hubheight: "hubHeight", +} as const +export function parseVBeltSheaveModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "vbeltsheave", + vBeltSheaveModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/vbeltsheave/register.ts b/src/models/vbeltsheave/register.ts new file mode 100644 index 0000000..7df3a7f --- /dev/null +++ b/src/models/vbeltsheave/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { vBeltSheaveModelDefinitionSchema } from "./schema" +import { parseVBeltSheaveModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "vbeltsheave", + schema: vBeltSheaveModelDefinitionSchema, + parse: parseVBeltSheaveModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/vbeltsheave/schema.ts b/src/models/vbeltsheave/schema.ts new file mode 100644 index 0000000..24b38d9 --- /dev/null +++ b/src/models/vbeltsheave/schema.ts @@ -0,0 +1,67 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(60), + boreDiameter: positiveLength.default(12), + width: positiveLength.default(24), + grooveCount: z.number().int().min(2).max(64).default(2), + grooveDepth: positiveLength.default(8), + grooveWidth: positiveLength.default(9), + hubDiameter: positiveLength.default(26), + hubHeight: positiveLength.default(8), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.grooveCount * p.grooveWidth >= p.width) + context.addIssue({ + code: "custom", + message: "Grooves must leave positive-width outer lands", + }) + if (p.outerDiameter / 2 - p.grooveDepth <= p.hubDiameter / 2) + context.addIssue({ + code: "custom", + message: "Groove roots must leave radial material outside the hub", + }) + if (p.hubDiameter <= p.boreDiameter) + context.addIssue({ + code: "custom", + message: "Hub must leave material around the shaft bore", + }) +} +export const vBeltSheaveModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const vBeltSheaveModelDefinitionSchema = z + .object({ fn: z.literal("vbeltsheave"), ...shape }) + .strict() + .superRefine(validate) +export type VBeltSheaveModelPropsInput = z.input< + typeof vBeltSheaveModelPropsSchema +> +export type VBeltSheaveModelProps = z.output +export type VBeltSheaveModelDefinition = z.output< + typeof vBeltSheaveModelDefinitionSchema +> +/** Multi-groove V-belt sheave with sloped circumferential groove flanks, small root land, hub and through bore. Axis Z, bottom Z=0. Nominal groove envelope; belt section standard, taper bushing and keyway are not specified. All lengths are normalized to millimeters. */ +export function getVBeltSheaveDimensions( + input: VBeltSheaveModelPropsInput = {}, +) { + const p = vBeltSheaveModelPropsSchema.parse(input) + return { + ...p, + size: [p.outerDiameter, p.outerDiameter, p.width + p.hubHeight] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.width + p.hubHeight, + } +} diff --git a/src/models/vgroovewheel/index.ts b/src/models/vgroovewheel/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/vgroovewheel/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/vgroovewheel/parse-model-string.ts b/src/models/vgroovewheel/parse-model-string.ts new file mode 100644 index 0000000..68a0848 --- /dev/null +++ b/src/models/vgroovewheel/parse-model-string.ts @@ -0,0 +1,21 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { vGrooveWheelModelDefinitionSchema } from "./schema" +const aliases = { + od: "outerDiameter", + root: "rootDiameter", + bore: "boreDiameter", + w: "width", + outerdiameter: "outerDiameter", + rootdiameter: "rootDiameter", + borediameter: "boreDiameter", + width: "width", +} as const +export function parseVGrooveWheelModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "vgroovewheel", + vGrooveWheelModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/vgroovewheel/register.ts b/src/models/vgroovewheel/register.ts new file mode 100644 index 0000000..ed6132e --- /dev/null +++ b/src/models/vgroovewheel/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { vGrooveWheelModelDefinitionSchema } from "./schema" +import { parseVGrooveWheelModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "vgroovewheel", + schema: vGrooveWheelModelDefinitionSchema, + parse: parseVGrooveWheelModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/vgroovewheel/schema.ts b/src/models/vgroovewheel/schema.ts new file mode 100644 index 0000000..9714df0 --- /dev/null +++ b/src/models/vgroovewheel/schema.ts @@ -0,0 +1,60 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const shape = { + outerDiameter: positiveLength.default(40), + rootDiameter: positiveLength.default(28), + boreDiameter: positiveLength.default(8), + width: positiveLength.default(12), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (p.rootDiameter <= p.boreDiameter) + context.addIssue({ + code: "custom", + message: "Groove root must leave material around the bore", + }) + if (p.outerDiameter <= p.rootDiameter) + context.addIssue({ + code: "custom", + message: "Outside diameter must exceed the groove root diameter", + }) +} +export const vGrooveWheelModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const vGrooveWheelModelDefinitionSchema = z + .object({ fn: z.literal("vgroovewheel"), ...shape }) + .strict() + .superRefine(validate) +export type VGrooveWheelModelPropsInput = z.input< + typeof vGrooveWheelModelPropsSchema +> +export type VGrooveWheelModelProps = z.output< + typeof vGrooveWheelModelPropsSchema +> +export type VGrooveWheelModelDefinition = z.output< + typeof vGrooveWheelModelDefinitionSchema +> +/** Symmetric V-groove track wheel with straight axial bore. Z axis, bottom Z=0. Groove flanks run from 20%/80% of the width to the central root; this nominal track envelope does not specify a bearing or rail standard. All lengths are normalized to millimeters. */ +export function getVGrooveWheelDimensions( + input: VGrooveWheelModelPropsInput = {}, +) { + const p = vGrooveWheelModelPropsSchema.parse(input) + return { + ...p, + size: [p.outerDiameter, p.outerDiameter, p.width] as [ + number, + number, + number, + ], + bottomZ: 0, + topZ: p.width, + } +} diff --git a/src/models/wavewasher/index.ts b/src/models/wavewasher/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/wavewasher/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/wavewasher/parse-model-string.ts b/src/models/wavewasher/parse-model-string.ts new file mode 100644 index 0000000..5a003aa --- /dev/null +++ b/src/models/wavewasher/parse-model-string.ts @@ -0,0 +1,20 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { waveWasherModelDefinitionSchema } from "./schema" +export function parseWaveWasherModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "wavewasher", + waveWasherModelDefinitionSchema, + { + id: "innerDiameter", + innerdiameter: "innerDiameter", + od: "outerDiameter", + outerdiameter: "outerDiameter", + t: "thickness", + thickness: "thickness", + wh: "waveHeight", + waveheight: "waveHeight", + }, + ) +} diff --git a/src/models/wavewasher/register.ts b/src/models/wavewasher/register.ts new file mode 100644 index 0000000..900be85 --- /dev/null +++ b/src/models/wavewasher/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { waveWasherModelDefinitionSchema } from "./schema" +import { parseWaveWasherModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "wavewasher", + schema: waveWasherModelDefinitionSchema, + parse: parseWaveWasherModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/wavewasher/schema.ts b/src/models/wavewasher/schema.ts new file mode 100644 index 0000000..2d6088a --- /dev/null +++ b/src/models/wavewasher/schema.ts @@ -0,0 +1,62 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + innerDiameter: positiveLength.default(10), + outerDiameter: positiveLength.default(16), + thickness: positiveLength.default(0.6), + waveHeight: positiveLength.default(1.5), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![(p.outerDiameter - p.innerDiameter) / 2].every( + (gap) => gap >= extent * 0.0001, + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if (!(p.innerDiameter < p.outerDiameter)) + ctx.addIssue({ + code: "custom", + message: + "Invalid wavewasher dimensions: mating features must fit within the body", + }) +} +export const waveWasherModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const waveWasherModelDefinitionSchema = z + .object({ fn: z.literal("wavewasher"), ...shape }) + .strict() + .superRefine(validate) +export type WaveWasherModelPropsInput = z.input< + typeof waveWasherModelPropsSchema +> +export type WaveWasherModelProps = z.output +export type WaveWasherModelDefinition = z.output< + typeof waveWasherModelDefinitionSchema +> +export function getWaveWasherDimensions(input: WaveWasherModelPropsInput = {}) { + return waveWasherModelPropsSchema.parse(input) +} diff --git a/src/models/wingnut/index.ts b/src/models/wingnut/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/wingnut/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/wingnut/parse-model-string.ts b/src/models/wingnut/parse-model-string.ts new file mode 100644 index 0000000..56b8fe2 --- /dev/null +++ b/src/models/wingnut/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { wingNutModelDefinitionSchema } from "./schema" +export function parseWingNutModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "wingnut", + wingNutModelDefinitionSchema, + { + id: "boreDiameter", + borediameter: "boreDiameter", + af: "acrossFlats", + acrossflats: "acrossFlats", + h: "bodyHeight", + bodyheight: "bodyHeight", + s: "wingSpan", + wingspan: "wingSpan", + wh: "wingHeight", + wingheight: "wingHeight", + wt: "wingThickness", + wingthickness: "wingThickness", + }, + ) +} diff --git a/src/models/wingnut/register.ts b/src/models/wingnut/register.ts new file mode 100644 index 0000000..52d4d45 --- /dev/null +++ b/src/models/wingnut/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { wingNutModelDefinitionSchema } from "./schema" +import { parseWingNutModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "wingnut", + schema: wingNutModelDefinitionSchema, + parse: parseWingNutModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/wingnut/schema.ts b/src/models/wingnut/schema.ts new file mode 100644 index 0000000..d1cce6a --- /dev/null +++ b/src/models/wingnut/schema.ts @@ -0,0 +1,70 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" + +/** Nominal visual dimensions in millimeters; threads use smooth envelopes. */ +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.0001 && value <= 10000, + "Length must be in [0.0001, 10000] mm", +) +const shape = { + boreDiameter: positiveLength.default(6), + acrossFlats: positiveLength.default(10), + bodyHeight: positiveLength.default(5), + wingSpan: positiveLength.default(28), + wingHeight: positiveLength.default(10), + wingThickness: positiveLength.default(3), +} +const validate = ( + p: z.output>, + ctx: z.RefinementCtx, +) => { + const extent = Math.max( + ...Object.values(p).filter((value) => typeof value === "number"), + ) + if ( + ![ + (p.acrossFlats - p.boreDiameter) / 2, + (p.wingSpan - p.acrossFlats) / 2, + ].every((gap) => gap >= extent * 0.0001) + ) + ctx.addIssue({ + code: "custom", + message: + "Material and clearance gaps must exceed 0.0001 times the model envelope", + }) + if (Object.values(p).some((value) => value < extent * 0.0001)) + ctx.addIssue({ + code: "custom", + message: + "Dimensions are too small relative to the model envelope for stable geometry", + }) + if ( + !( + p.boreDiameter < p.acrossFlats && + p.acrossFlats < p.wingSpan && + p.wingThickness < p.acrossFlats && + p.bodyHeight < p.wingHeight + ) + ) + ctx.addIssue({ + code: "custom", + message: + "Invalid wingnut dimensions: mating features must fit within the body", + }) +} +export const wingNutModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const wingNutModelDefinitionSchema = z + .object({ fn: z.literal("wingnut"), ...shape }) + .strict() + .superRefine(validate) +export type WingNutModelPropsInput = z.input +export type WingNutModelProps = z.output +export type WingNutModelDefinition = z.output< + typeof wingNutModelDefinitionSchema +> +export function getWingNutDimensions(input: WingNutModelPropsInput = {}) { + return wingNutModelPropsSchema.parse(input) +} diff --git a/src/models/wireduct/index.ts b/src/models/wireduct/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/wireduct/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/wireduct/parse-model-string.ts b/src/models/wireduct/parse-model-string.ts new file mode 100644 index 0000000..f22cf99 --- /dev/null +++ b/src/models/wireduct/parse-model-string.ts @@ -0,0 +1,24 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { wireDuctModelDefinitionSchema } from "./schema" +const aliases = { + width: "width", + height: "height", + length: "length", + thickness: "thickness", + slotwidth: "slotWidth", + slotpitch: "slotPitch", + holediameter: "holeDiameter", + w: "width", + h: "height", + l: "length", + t: "thickness", +} as const +export function parseWireDuctModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "wireduct", + wireDuctModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/wireduct/register.ts b/src/models/wireduct/register.ts new file mode 100644 index 0000000..085d2b9 --- /dev/null +++ b/src/models/wireduct/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { wireDuctModelDefinitionSchema } from "./schema" +import { parseWireDuctModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "wireduct", + schema: wireDuctModelDefinitionSchema, + parse: parseWireDuctModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/wireduct/schema.ts b/src/models/wireduct/schema.ts new file mode 100644 index 0000000..4d40c86 --- /dev/null +++ b/src/models/wireduct/schema.ts @@ -0,0 +1,70 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positive = industrialLengthSchema.refine( + (value) => value > 0, + "Length must be positive", +) +const nonnegative = industrialLengthSchema.refine( + (value) => value >= 0, + "Length must be nonnegative", +) +const shape = { + width: positive.default(40), + height: positive.default(40), + length: positive.default(100), + thickness: positive.default(2), + slotWidth: positive.default(4), + slotPitch: positive.default(8), + holeDiameter: positive.default(5), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (2 * p.thickness >= p.width || 4 * p.thickness >= p.height) + context.addIssue({ + code: "custom", + message: "Incompatible wireduct dimensions: 1", + }) + if (p.slotWidth >= p.slotPitch) + context.addIssue({ + code: "custom", + message: "Incompatible wireduct dimensions: 2", + }) + if (p.slotPitch >= p.length) + context.addIssue({ + code: "custom", + message: "Incompatible wireduct dimensions: 3", + }) + if (p.holeDiameter >= p.width - 2 * p.thickness) + context.addIssue({ + code: "custom", + message: "Incompatible wireduct dimensions: 4", + }) + if (p.length / p.slotPitch > 512 || p.length > 12800) + context.addIssue({ + code: "custom", + message: "Too many repeated rail or duct features; maximum 512", + }) +} +export const wireDuctModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const wireDuctModelDefinitionSchema = z + .object({ fn: z.literal("wireduct"), ...shape }) + .strict() + .superRefine(validate) +export type WireDuctModelPropsInput = z.input +export type WireDuctModelProps = z.output +export type WireDuctModelDefinition = z.output< + typeof wireDuctModelDefinitionSchema +> +/** Open slotted wire duct with a continuous floor, two comb sidewalls and repeated bottom mounting holes. Length runs along X. The removable lid and flexible sidewall deformation are omitted. All normalized lengths are millimeters. */ +export function getWireDuctDimensions(input: WireDuctModelPropsInput = {}) { + const p = wireDuctModelPropsSchema.parse(input) + return { + ...p, + size: [p.length, p.width, p.height] as [number, number, number], + } +} diff --git a/src/models/woodruffkey/index.ts b/src/models/woodruffkey/index.ts new file mode 100644 index 0000000..06861aa --- /dev/null +++ b/src/models/woodruffkey/index.ts @@ -0,0 +1 @@ +export * from "./schema" diff --git a/src/models/woodruffkey/parse-model-string.ts b/src/models/woodruffkey/parse-model-string.ts new file mode 100644 index 0000000..e4522fa --- /dev/null +++ b/src/models/woodruffkey/parse-model-string.ts @@ -0,0 +1,19 @@ +import type { RawModelprinterParams } from "../../parse-model-string" +import { parseIndustrialModelParams } from "../../industrial-parameter-utils" +import { woodruffKeyModelDefinitionSchema } from "./schema" +const aliases = { + diameter: "diameter", + thickness: "thickness", + height: "height", + h: "height", + d: "diameter", + t: "thickness", +} as const +export function parseWoodruffKeyModelParams(raw: RawModelprinterParams) { + return parseIndustrialModelParams( + raw, + "woodruffkey", + woodruffKeyModelDefinitionSchema, + aliases, + ) +} diff --git a/src/models/woodruffkey/register.ts b/src/models/woodruffkey/register.ts new file mode 100644 index 0000000..f1d176e --- /dev/null +++ b/src/models/woodruffkey/register.ts @@ -0,0 +1,11 @@ +import { defineModel, type ModelRegistry } from "../../model-registry" +import { woodruffKeyModelDefinitionSchema } from "./schema" +import { parseWoodruffKeyModelParams } from "./parse-model-string" +export const model = defineModel({ + name: "woodruffkey", + schema: woodruffKeyModelDefinitionSchema, + parse: parseWoodruffKeyModelParams, +}) +export function register(registry: ModelRegistry): void { + registry.register(model) +} diff --git a/src/models/woodruffkey/schema.ts b/src/models/woodruffkey/schema.ts new file mode 100644 index 0000000..850cb47 --- /dev/null +++ b/src/models/woodruffkey/schema.ts @@ -0,0 +1,47 @@ +import { z } from "zod" +import { industrialLengthSchema } from "../../industrial-parameter-utils" +const positiveLength = industrialLengthSchema.refine( + (value) => value >= 0.01, + "Length must be at least 0.01 mm", +) +const shape = { + diameter: positiveLength.default(30), + thickness: positiveLength.default(6), + height: positiveLength.default(12), +} +function validate( + p: z.output>, + context: z.RefinementCtx, +) { + if (!(p.height <= p.diameter / 2)) + context.addIssue({ + code: "custom", + message: "Invalid woodruffkey dimensions: height <= diameter/2", + }) + if (!(p.thickness < p.diameter / 2)) + context.addIssue({ + code: "custom", + message: "Invalid woodruffkey dimensions: thickness < diameter/2", + }) +} +export const woodruffKeyModelPropsSchema = z + .object(shape) + .strict() + .superRefine(validate) +export const woodruffKeyModelDefinitionSchema = z + .object({ fn: z.literal("woodruffkey"), ...shape }) + .strict() + .superRefine(validate) +export type WoodruffKeyModelPropsInput = z.input< + typeof woodruffKeyModelPropsSchema +> +export type WoodruffKeyModelProps = z.output +export type WoodruffKeyModelDefinition = z.output< + typeof woodruffKeyModelDefinitionSchema +> +/** Circular segment key in XZ, thickness along Y, flat top at Z=height, bottom Z=0. All lengths normalize to millimeters. Nominal geometry; no vendor, fit or standard compliance is implied. */ +export function getWoodruffKeyDimensions( + input: WoodruffKeyModelPropsInput = {}, +) { + return woodruffKeyModelPropsSchema.parse(input) +} diff --git a/tests/coupling-contracts.test.ts b/tests/coupling-contracts.test.ts new file mode 100644 index 0000000..0a0f74d --- /dev/null +++ b/tests/coupling-contracts.test.ts @@ -0,0 +1,191 @@ +import { expect, test } from "bun:test" +import { mp, modelDefinitionSchema } from "../src" +import { + jawCouplingModelPropsSchema, + getJawCouplingDimensions, +} from "../src/models/jawcoupling" +import { + beamCouplingModelPropsSchema, + getBeamCouplingDimensions, +} from "../src/models/beamcoupling" +import { + oldhamCouplingModelPropsSchema, + getOldhamCouplingDimensions, +} from "../src/models/oldhamcoupling" +import { + universalJointModelPropsSchema, + getUniversalJointDimensions, +} from "../src/models/universaljoint" +import { + bevelGearModelPropsSchema, + getBevelGearDimensions, +} from "../src/models/bevelgear" + +const contracts = [ + { + name: "jawcoupling", + source: "jawcoupling_od30mm_l40mm_bore8mm_hubl12mm_jawd12mm_jawcount3", + schema: jawCouplingModelPropsSchema, + invalid: [ + "bore21mm", + "hubl20mm", + "jawd8mm", + "jawd17mm", + "jawcount2", + "jawcount3.5", + "jawcount3mm", + ], + }, + { + name: "beamcoupling", + source: + "beamcoupling_od25mm_l30mm_bore8mm_hubl7mm_slotw1mm_slots6_bridgeangle60", + schema: beamCouplingModelPropsSchema, + invalid: [ + "bore25mm", + "hubl15mm", + "slotw3mm", + "slots1", + "slots6.5", + "slots6mm", + "bridgeangle121", + "bridgeangle60mm", + ], + }, + { + name: "oldhamcoupling", + source: + "oldhamcoupling_od30mm_l36mm_bore8mm_hubl12mm_slotw7mm_slotd3mm_clearance0.2mm", + schema: oldhamCouplingModelPropsSchema, + invalid: [ + "bore18mm", + "hubl18mm", + "slotw21mm", + "slotw0.4mm", + "slotd12mm", + "slotd0.4mm", + "clearance-1mm", + ], + }, + { + name: "universaljoint", + source: + "universaljoint_od30mm_l60mm_bore8mm_hubl14mm_armt5mm_crossd6mm_clearance0.2mm", + schema: universalJointModelPropsSchema, + invalid: [ + "armt10mm", + "bore20mm", + "hubl24mm", + "crossd12mm", + "clearance2.4mm", + "clearance2.5mm", + ], + }, + { + name: "bevelgear", + source: + "bevelgear_od40mm_td24mm_h14mm_bore8mm_depth3mm_hubd20mm_hubh8mm_teeth16", + schema: bevelGearModelPropsSchema, + invalid: [ + "td40mm", + "depth8mm", + "hubd25mm", + "hubd8mm", + "teeth5", + "teeth16.5", + "teeth16mm", + ], + }, +] as const +for (const entry of contracts) { + test(`${entry.name} defaults, full string, typed schema and registered definition agree`, () => { + const definition = mp.string(entry.source).json() + expect(definition.fn).toBe(entry.name) + const { fn, ...props } = definition + expect(props).toEqual(entry.schema.parse({})) + expect(mp.string(entry.name).json()).toEqual(definition) + expect(modelDefinitionSchema.parse(definition)).toEqual(definition) + expect(mp.getModelNames()).toContain(entry.name) + expect(mp.string(mp.string(entry.source).params().string).json()).toEqual( + definition, + ) + }) + test(`${entry.name} unit conversion and aliases preserve the normalized contract`, () => { + const d = mp.string(entry.name).json() + expect( + mp + .string( + `${entry.name.toUpperCase()}_OUTERDIAMETER${entry.schema.parse({}).outerDiameter / 10}CM_BORE0.8CM`, + ) + .json(), + ).toEqual(d) + expect( + entry.schema.parse({ boreDiameter: "0.31496062992125984in" }) + .boreDiameter, + ).toBeCloseTo(8) + expect( + entry.schema.parse({ + outerDiameter: entry.schema.parse({}).outerDiameter / 1000 + "m", + }), + ).toEqual(entry.schema.parse({})) + }) + test(`${entry.name} rejects duplicate aliases, unknown tokens, malformed lengths and impossible geometry`, () => { + for (const token of [ + "od0mm", + "od-1mm", + "od30mm_outerdiameter30mm", + "od30mmjunk", + "od1.2.3mm", + "od", + "typo1mm", + ...entry.invalid, + ]) + expect(() => mp.string(`${entry.name}_${token}`).json()).toThrow() + expect(() => mp.string(`${entry.name}30`).json()).toThrow() + for (const input of [ + { outerDiameter: Infinity }, + { boreDiameter: NaN }, + { outerDiameter: "oops" }, + { unknownProperty: 1 }, + { outerDiameter: 1.7e308, length: 1.7e308 }, + ]) + expect(() => entry.schema.parse(input)).toThrow() + }) +} + +test("coupling datums and mechanical feature dimensions use millimeters", () => { + expect(getJawCouplingDimensions({})).toMatchObject({ + size: [30, 30, 40], + bottomZ: 0, + topZ: 40, + centralLength: 16, + jawInnerRadius: 9.5, + }) + expect(getBeamCouplingDimensions({})).toMatchObject({ + size: [25, 25, 30], + bottomZ: 0, + topZ: 30, + flexureLength: 16, + slotPitch: 16 / 7, + }) + expect(getOldhamCouplingDimensions({})).toMatchObject({ + size: [30, 30, 36], + diskThickness: 11.6, + diskDiameter: 26.4, + tongueWidth: 6.6, + tongueHeight: 2.8, + }) + expect(getUniversalJointDimensions({})).toMatchObject({ + size: [30, 30, 60], + crossZ: 30, + crossSpan: 25, + earWidth: 10.8, + }) + expect(getBevelGearDimensions({})).toMatchObject({ + size: [40, 40, 22], + topZ: 22, + pitchAngle: 22.5, + rootDiameter: 34, + topRootDiameter: 18, + }) +}) diff --git a/tests/fixtures/machine-hardware-models.ts b/tests/fixtures/machine-hardware-models.ts new file mode 100644 index 0000000..a45361a --- /dev/null +++ b/tests/fixtures/machine-hardware-models.ts @@ -0,0 +1,524 @@ +import { + hingeModelPropsSchema as schema0, + hingeModelDefinitionSchema as definition0, +} from "../../src/models/hinge" +import { + register as register0, + model as model0, +} from "../../src/models/hinge/register" +import { + barrelHingeModelPropsSchema as schema1, + barrelHingeModelDefinitionSchema as definition1, +} from "../../src/models/barrelhinge" +import { + register as register1, + model as model1, +} from "../../src/models/barrelhinge/register" +import { + drawLatchModelPropsSchema as schema2, + drawLatchModelDefinitionSchema as definition2, +} from "../../src/models/drawlatch" +import { + register as register2, + model as model2, +} from "../../src/models/drawlatch/register" +import { + toggleClampModelPropsSchema as schema3, + toggleClampModelDefinitionSchema as definition3, +} from "../../src/models/toggleclamp" +import { + register as register3, + model as model3, +} from "../../src/models/toggleclamp/register" +import { + levelingFootModelPropsSchema as schema4, + levelingFootModelDefinitionSchema as definition4, +} from "../../src/models/levelingfoot" +import { + register as register4, + model as model4, +} from "../../src/models/levelingfoot/register" +import { + casterModelPropsSchema as schema5, + casterModelDefinitionSchema as definition5, +} from "../../src/models/caster" +import { + register as register5, + model as model5, +} from "../../src/models/caster/register" +import { + rubberBumperModelPropsSchema as schema6, + rubberBumperModelDefinitionSchema as definition6, +} from "../../src/models/rubberbumper" +import { + register as register6, + model as model6, +} from "../../src/models/rubberbumper/register" +import { + bridgeHandleModelPropsSchema as schema7, + bridgeHandleModelDefinitionSchema as definition7, +} from "../../src/models/bridgehandle" +import { + register as register7, + model as model7, +} from "../../src/models/bridgehandle/register" +import { + handwheelModelPropsSchema as schema8, + handwheelModelDefinitionSchema as definition8, +} from "../../src/models/handwheel" +import { + register as register8, + model as model8, +} from "../../src/models/handwheel/register" +import { + starKnobModelPropsSchema as schema9, + starKnobModelDefinitionSchema as definition9, +} from "../../src/models/starknob" +import { + register as register9, + model as model9, +} from "../../src/models/starknob/register" +import { + leverHandleModelPropsSchema as schema10, + leverHandleModelDefinitionSchema as definition10, +} from "../../src/models/leverhandle" +import { + register as register10, + model as model10, +} from "../../src/models/leverhandle/register" +import { + squareKeyModelPropsSchema as schema11, + squareKeyModelDefinitionSchema as definition11, +} from "../../src/models/squarekey" +import { + register as register11, + model as model11, +} from "../../src/models/squarekey/register" +import { + woodruffKeyModelPropsSchema as schema12, + woodruffKeyModelDefinitionSchema as definition12, +} from "../../src/models/woodruffkey" +import { + register as register12, + model as model12, +} from "../../src/models/woodruffkey/register" +import { + iBeamModelPropsSchema as schema13, + iBeamModelDefinitionSchema as definition13, +} from "../../src/models/ibeam" +import { + register as register13, + model as model13, +} from "../../src/models/ibeam/register" +import { + teeBarModelPropsSchema as schema14, + teeBarModelDefinitionSchema as definition14, +} from "../../src/models/teebar" +import { + register as register14, + model as model14, +} from "../../src/models/teebar/register" +import { + roundBarModelPropsSchema as schema15, + roundBarModelDefinitionSchema as definition15, +} from "../../src/models/roundbar" +import { + register as register15, + model as model15, +} from "../../src/models/roundbar/register" +import { + splitRingHangerModelPropsSchema as schema16, + splitRingHangerModelDefinitionSchema as definition16, +} from "../../src/models/splitringhanger" +import { + register as register16, + model as model16, +} from "../../src/models/splitringhanger/register" +import { + detentPinModelPropsSchema as schema17, + detentPinModelDefinitionSchema as definition17, +} from "../../src/models/detentpin" +import { + register as register17, + model as model17, +} from "../../src/models/detentpin/register" +import { + expandedMetalModelPropsSchema as schema18, + expandedMetalModelDefinitionSchema as definition18, +} from "../../src/models/expandedmetal" +import { + register as register18, + model as model18, +} from "../../src/models/expandedmetal/register" +import { + coolingFanModelPropsSchema as schema19, + coolingFanModelDefinitionSchema as definition19, +} from "../../src/models/coolingfan" +import { + register as register19, + model as model19, +} from "../../src/models/coolingfan/register" +export const machineHardwareContracts = [ + { + name: "hinge", + schema: schema0, + definition: definition0, + register: register0, + parse: model0.parse, + defaults: { + width: 60, + length: 60, + leafThickness: 3, + barrelDiameter: 10, + pinDiameter: 4, + holeDiameter: 5, + }, + source: + "hinge_width60mm_length60mm_leafthickness3mm_barreldiameter10mm_pindiameter4mm_holediameter5mm", + first: "width", + invalid: { pinDiameter: 15 }, + }, + { + name: "barrelhinge", + schema: schema1, + definition: definition1, + register: register1, + parse: model1.parse, + defaults: { + height: 60, + barrelDiameter: 14, + pinDiameter: 6, + leafWidth: 20, + leafThickness: 4, + holeDiameter: 5, + }, + source: + "barrelhinge_height60mm_barreldiameter14mm_pindiameter6mm_leafwidth20mm_leafthickness4mm_holediameter5mm", + first: "height", + invalid: { pinDiameter: 15 }, + }, + { + name: "drawlatch", + schema: schema2, + definition: definition2, + register: register2, + parse: model2.parse, + defaults: { + length: 100, + width: 25, + baseThickness: 4, + handleHeight: 18, + wireDiameter: 4, + holeDiameter: 4, + }, + source: + "drawlatch_length100mm_width25mm_basethickness4mm_handleheight18mm_wirediameter4mm_holediameter4mm", + first: "length", + invalid: { wireDiameter: 10 }, + }, + { + name: "toggleclamp", + schema: schema3, + definition: definition3, + register: register3, + parse: model3.parse, + defaults: { + length: 110, + width: 35, + baseThickness: 5, + handleHeight: 55, + armLength: 75, + padDiameter: 14, + holeDiameter: 5, + }, + source: + "toggleclamp_length110mm_width35mm_basethickness5mm_handleheight55mm_armlength75mm_paddiameter14mm_holediameter5mm", + first: "length", + invalid: { padDiameter: 30 }, + }, + { + name: "levelingfoot", + schema: schema4, + definition: definition4, + register: register4, + parse: model4.parse, + defaults: { + baseDiameter: 50, + baseHeight: 12, + stemDiameter: 12, + stemLength: 55, + hexDiameter: 24, + hexHeight: 8, + }, + source: + "levelingfoot_basediameter50mm_baseheight12mm_stemdiameter12mm_stemlength55mm_hexdiameter24mm_hexheight8mm", + first: "baseDiameter", + invalid: { stemDiameter: 40 }, + }, + { + name: "caster", + schema: schema5, + definition: definition5, + register: register5, + parse: model5.parse, + defaults: { + wheelDiameter: 75, + wheelWidth: 24, + axleDiameter: 10, + forkThickness: 5, + plateWidth: 50, + plateLength: 65, + plateThickness: 5, + overallHeight: 110, + holeDiameter: 5, + }, + source: + "caster_wheeldiameter75mm_wheelwidth24mm_axlediameter10mm_forkthickness5mm_platewidth50mm_platelength65mm_platethickness5mm_overallheight110mm_holediameter5mm", + first: "wheelDiameter", + invalid: { overallHeight: 70 }, + }, + { + name: "rubberbumper", + schema: schema6, + definition: definition6, + register: register6, + parse: model6.parse, + defaults: { + diameter: 30, + height: 15, + holeDiameter: 6, + counterboreDiameter: 13, + counterboreDepth: 5, + }, + source: + "rubberbumper_diameter30mm_height15mm_holediameter6mm_counterborediameter13mm_counterboredepth5mm", + first: "diameter", + invalid: { counterboreDepth: 16 }, + }, + { + name: "bridgehandle", + schema: schema7, + definition: definition7, + register: register7, + parse: model7.parse, + defaults: { + length: 120, + gripDiameter: 15, + height: 40, + mountingDiameter: 24, + mountingHeight: 5, + holeDiameter: 5, + }, + source: + "bridgehandle_length120mm_gripdiameter15mm_height40mm_mountingdiameter24mm_mountingheight5mm_holediameter5mm", + first: "length", + invalid: { height: 10 }, + }, + { + name: "handwheel", + schema: schema8, + definition: definition8, + register: register8, + parse: model8.parse, + defaults: { + outerDiameter: 100, + rimWidth: 12, + hubDiameter: 28, + hubHeight: 25, + boreDiameter: 12, + spokes: 4, + }, + source: + "handwheel_outerdiameter100mm_rimwidth12mm_hubdiameter28mm_hubheight25mm_borediameter12mm_spokes4", + first: "outerDiameter", + invalid: { boreDiameter: 30 }, + }, + { + name: "starknob", + schema: schema9, + definition: definition9, + register: register9, + parse: model9.parse, + defaults: { + diameter: 40, + height: 16, + boreDiameter: 6, + lobes: 5, + hubDiameter: 18, + hubHeight: 10, + }, + source: + "starknob_diameter40mm_height16mm_borediameter6mm_lobes5_hubdiameter18mm_hubheight10mm", + first: "diameter", + invalid: { boreDiameter: 20 }, + }, + { + name: "leverhandle", + schema: schema10, + definition: definition10, + register: register10, + parse: model10.parse, + defaults: { + length: 90, + width: 18, + height: 16, + hubDiameter: 28, + hubHeight: 25, + boreDiameter: 8, + }, + source: + "leverhandle_length90mm_width18mm_height16mm_hubdiameter28mm_hubheight25mm_borediameter8mm", + first: "length", + invalid: { boreDiameter: 25 }, + }, + { + name: "squarekey", + schema: schema11, + definition: definition11, + register: register11, + parse: model11.parse, + defaults: { width: 8, height: 8, length: 40, chamfer: 1 }, + source: "squarekey_width8mm_height8mm_length40mm_chamfer1mm", + first: "width", + invalid: { chamfer: 4 }, + }, + { + name: "woodruffkey", + schema: schema12, + definition: definition12, + register: register12, + parse: model12.parse, + defaults: { diameter: 30, thickness: 6, height: 12 }, + source: "woodruffkey_diameter30mm_thickness6mm_height12mm", + first: "diameter", + invalid: { height: 20 }, + }, + { + name: "ibeam", + schema: schema13, + definition: definition13, + register: register13, + parse: model13.parse, + defaults: { + width: 40, + height: 60, + webThickness: 4, + flangeThickness: 6, + length: 100, + }, + source: + "ibeam_width40mm_height60mm_webthickness4mm_flangethickness6mm_length100mm", + first: "width", + invalid: { flangeThickness: 40 }, + }, + { + name: "teebar", + schema: schema14, + definition: definition14, + register: register14, + parse: model14.parse, + defaults: { + width: 40, + height: 40, + webThickness: 4, + flangeThickness: 6, + length: 100, + }, + source: + "teebar_width40mm_height40mm_webthickness4mm_flangethickness6mm_length100mm", + first: "width", + invalid: { flangeThickness: 40 }, + }, + { + name: "roundbar", + schema: schema15, + definition: definition15, + register: register15, + parse: model15.parse, + defaults: { diameter: 20, length: 100, chamfer: 1 }, + source: "roundbar_diameter20mm_length100mm_chamfer1mm", + first: "diameter", + invalid: { chamfer: 20 }, + }, + { + name: "splitringhanger", + schema: schema16, + definition: definition16, + register: register16, + parse: model16.parse, + defaults: { + pipeDiameter: 32, + bandThickness: 3, + bandWidth: 15, + gap: 2, + lugWidth: 14, + holeDiameter: 5, + }, + source: + "splitringhanger_pipediameter32mm_bandthickness3mm_bandwidth15mm_gap2mm_lugwidth14mm_holediameter5mm", + first: "pipeDiameter", + invalid: { gap: 5 }, + }, + { + name: "detentpin", + schema: schema17, + definition: definition17, + register: register17, + parse: model17.parse, + defaults: { + shaftDiameter: 8, + shaftLength: 50, + headDiameter: 18, + headHeight: 6, + ballDiameter: 3, + ballOffset: 4, + ringDiameter: 24, + ringWire: 2, + }, + source: + "detentpin_shaftdiameter8mm_shaftlength50mm_headdiameter18mm_headheight6mm_balldiameter3mm_balloffset4mm_ringdiameter24mm_ringwire2mm", + first: "shaftDiameter", + invalid: { ringWire: 8 }, + }, + { + name: "expandedmetal", + schema: schema18, + definition: definition18, + register: register18, + parse: model18.parse, + defaults: { + width: 100, + length: 100, + thickness: 2, + diamondWidth: 15, + diamondLength: 30, + strandWidth: 2, + frameWidth: 3, + }, + source: + "expandedmetal_width100mm_length100mm_thickness2mm_diamondwidth15mm_diamondlength30mm_strandwidth2mm_framewidth3mm", + first: "width", + invalid: { diamondWidth: 1 }, + }, + { + name: "coolingfan", + schema: schema19, + definition: definition19, + register: register19, + parse: model19.parse, + defaults: { + size: 80, + thickness: 25, + hubDiameter: 28, + bladeCount: 7, + bladeWidth: 12, + bladeThickness: 3, + mountingHoleDiameter: 4, + frameThickness: 6, + }, + source: + "coolingfan_size80mm_thickness25mm_hubdiameter28mm_bladecount7_bladewidth12mm_bladethickness3mm_mountingholediameter4mm_framethickness6mm", + first: "size", + invalid: { frameThickness: 30 }, + }, +] diff --git a/tests/industrial-automation-contracts.test.ts b/tests/industrial-automation-contracts.test.ts new file mode 100644 index 0000000..06702d0 --- /dev/null +++ b/tests/industrial-automation-contracts.test.ts @@ -0,0 +1,1661 @@ +import { expect, test } from "bun:test" +import { mp } from "../src" +import { + pneumaticCylinderModelPropsSchema, + pneumaticCylinderModelDefinitionSchema, + getPneumaticCylinderDimensions, +} from "../src/models/pneumaticcylinder" +import { + cylinderMountModelPropsSchema, + cylinderMountModelDefinitionSchema, + getCylinderMountDimensions, +} from "../src/models/cylindermount" +import { + airFilterModelPropsSchema, + airFilterModelDefinitionSchema, + getAirFilterDimensions, +} from "../src/models/airfilter" +import { + airRegulatorModelPropsSchema, + airRegulatorModelDefinitionSchema, + getAirRegulatorDimensions, +} from "../src/models/airregulator" +import { + airLubricatorModelPropsSchema, + airLubricatorModelDefinitionSchema, + getAirLubricatorDimensions, +} from "../src/models/airlubricator" +import { + solenoidValveModelPropsSchema, + solenoidValveModelDefinitionSchema, + getSolenoidValveDimensions, +} from "../src/models/solenoidvalve" +import { + pressureSwitchModelPropsSchema, + pressureSwitchModelDefinitionSchema, + getPressureSwitchDimensions, +} from "../src/models/pressureswitch" +import { + pressureGaugeModelPropsSchema, + pressureGaugeModelDefinitionSchema, + getPressureGaugeDimensions, +} from "../src/models/pressuregauge" +import { + flowMeterModelPropsSchema, + flowMeterModelDefinitionSchema, + getFlowMeterDimensions, +} from "../src/models/flowmeter" +import { + vacuumCupModelPropsSchema, + vacuumCupModelDefinitionSchema, + getVacuumCupDimensions, +} from "../src/models/vacuumcup" +import { + inductiveProximitySensorModelPropsSchema, + inductiveProximitySensorModelDefinitionSchema, + getInductiveProximitySensorDimensions, +} from "../src/models/inductiveproximitysensor" +import { + photoelectricSensorModelPropsSchema, + photoelectricSensorModelDefinitionSchema, + getPhotoelectricSensorDimensions, +} from "../src/models/photoelectricsensor" +import { + limitSwitchModelPropsSchema, + limitSwitchModelDefinitionSchema, + getLimitSwitchDimensions, +} from "../src/models/limitswitch" +import { + pushbuttonOperatorModelPropsSchema, + pushbuttonOperatorModelDefinitionSchema, + getPushbuttonOperatorDimensions, +} from "../src/models/pushbuttonoperator" +import { + emergencyStopSwitchModelPropsSchema, + emergencyStopSwitchModelDefinitionSchema, + getEmergencyStopSwitchDimensions, +} from "../src/models/emergencystopswitch" +import { + selectorOperatorModelPropsSchema, + selectorOperatorModelDefinitionSchema, + getSelectorOperatorDimensions, +} from "../src/models/selectoroperator" +import { + pilotLightModelPropsSchema, + pilotLightModelDefinitionSchema, + getPilotLightDimensions, +} from "../src/models/pilotlight" +import { + dinRailModelPropsSchema, + dinRailModelDefinitionSchema, + getDinRailDimensions, +} from "../src/models/dinrail" +import { + terminalBlockModelPropsSchema, + terminalBlockModelDefinitionSchema, + getTerminalBlockDimensions, +} from "../src/models/terminalblock" +import { + wireDuctModelPropsSchema, + wireDuctModelDefinitionSchema, + getWireDuctDimensions, +} from "../src/models/wireduct" +test("pneumaticcylinder: strict typed schema, units and registry contract", () => { + const expected = { + fn: "pneumaticcylinder", + ...{ + boreDiameter: 32, + bodyLength: 80, + rodDiameter: 12, + stroke: 50, + extension: 25, + endCapLength: 10, + portDiameter: 6, + }, + } + expect( + mp + .string( + "pneumaticcylinder_borediameter32mm_bodylength80mm_roddiameter12mm_stroke50mm_extension25mm_endcaplength10mm_portdiameter6mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "PNEUMATICCYLINDER_BOREDIAMETER32MM_BODYLENGTH80MM_RODDIAMETER12MM_STROKE50MM_EXTENSION25MM_ENDCAPLENGTH10MM_PORTDIAMETER6MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("pneumaticcylinder").json()).toEqual(expected) + expect( + pneumaticCylinderModelDefinitionSchema.parse(expected), + ).toEqual(expected) + const { fn, ...props } = expected + expect( + pneumaticCylinderModelPropsSchema.parse({ + ...props, + boreDiameter: "3.2cm", + }), + ).toEqual(props) + expect( + getPneumaticCylinderDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_borediameter1mm", + "_borediameter1e2", + "_borediameter1mmjunk", + "_borediameter", + "__", + ]) + expect(() => + mp + .string( + "pneumaticcylinder_borediameter32mm_bodylength80mm_roddiameter12mm_stroke50mm_extension25mm_endcaplength10mm_portdiameter6mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + pneumaticCylinderModelPropsSchema.parse({ + ...props, + boreDiameter: value, + }), + ).toThrow() + expect(() => + pneumaticCylinderModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + pneumaticCylinderModelPropsSchema.parse({ ...props, ...{ extension: 51 } }), + ).toThrow() +}) + +test("cylindermount: strict typed schema, units and registry contract", () => { + const expected = { + fn: "cylindermount", + ...{ + width: 50, + depth: 25, + height: 45, + thickness: 5, + boreDiameter: 20, + holeDiameter: 5, + }, + } + expect( + mp + .string( + "cylindermount_width50mm_depth25mm_height45mm_thickness5mm_borediameter20mm_holediameter5mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "CYLINDERMOUNT_WIDTH50MM_DEPTH25MM_HEIGHT45MM_THICKNESS5MM_BOREDIAMETER20MM_HOLEDIAMETER5MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("cylindermount").json()).toEqual(expected) + expect(cylinderMountModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + cylinderMountModelPropsSchema.parse({ ...props, width: "5.0cm" }), + ).toEqual(props) + expect( + getCylinderMountDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_width1mm", + "_width1e2", + "_width1mmjunk", + "_width", + "__", + ]) + expect(() => + mp + .string( + "cylindermount_width50mm_depth25mm_height45mm_thickness5mm_borediameter20mm_holediameter5mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + cylinderMountModelPropsSchema.parse({ ...props, width: value }), + ).toThrow() + expect(() => + cylinderMountModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + cylinderMountModelPropsSchema.parse({ ...props, ...{ boreDiameter: 42 } }), + ).toThrow() +}) + +test("airfilter: strict typed schema, units and registry contract", () => { + const expected = { + fn: "airfilter", + ...{ + bodyWidth: 40, + bodyDepth: 40, + bodyHeight: 30, + bowlDiameter: 28, + bowlLength: 50, + portDiameter: 8, + drainLength: 8, + }, + } + expect( + mp + .string( + "airfilter_bodywidth40mm_bodydepth40mm_bodyheight30mm_bowldiameter28mm_bowllength50mm_portdiameter8mm_drainlength8mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "AIRFILTER_BODYWIDTH40MM_BODYDEPTH40MM_BODYHEIGHT30MM_BOWLDIAMETER28MM_BOWLLENGTH50MM_PORTDIAMETER8MM_DRAINLENGTH8MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("airfilter").json()).toEqual(expected) + expect(airFilterModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + airFilterModelPropsSchema.parse({ ...props, bodyWidth: "4.0cm" }), + ).toEqual(props) + expect( + getAirFilterDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_bodywidth1mm", + "_bodywidth1e2", + "_bodywidth1mmjunk", + "_bodywidth", + "__", + ]) + expect(() => + mp + .string( + "airfilter_bodywidth40mm_bodydepth40mm_bodyheight30mm_bowldiameter28mm_bowllength50mm_portdiameter8mm_drainlength8mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + airFilterModelPropsSchema.parse({ ...props, bodyWidth: value }), + ).toThrow() + expect(() => + airFilterModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + airFilterModelPropsSchema.parse({ ...props, ...{ bowlDiameter: 45 } }), + ).toThrow() +}) + +test("airregulator: strict typed schema, units and registry contract", () => { + const expected = { + fn: "airregulator", + ...{ + bodyWidth: 40, + bodyDepth: 36, + bodyHeight: 30, + knobDiameter: 28, + knobHeight: 20, + portDiameter: 8, + gaugeDiameter: 20, + }, + } + expect( + mp + .string( + "airregulator_bodywidth40mm_bodydepth36mm_bodyheight30mm_knobdiameter28mm_knobheight20mm_portdiameter8mm_gaugediameter20mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "AIRREGULATOR_BODYWIDTH40MM_BODYDEPTH36MM_BODYHEIGHT30MM_KNOBDIAMETER28MM_KNOBHEIGHT20MM_PORTDIAMETER8MM_GAUGEDIAMETER20MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("airregulator").json()).toEqual(expected) + expect(airRegulatorModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + airRegulatorModelPropsSchema.parse({ ...props, bodyWidth: "4.0cm" }), + ).toEqual(props) + expect( + getAirRegulatorDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_bodywidth1mm", + "_bodywidth1e2", + "_bodywidth1mmjunk", + "_bodywidth", + "__", + ]) + expect(() => + mp + .string( + "airregulator_bodywidth40mm_bodydepth36mm_bodyheight30mm_knobdiameter28mm_knobheight20mm_portdiameter8mm_gaugediameter20mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + airRegulatorModelPropsSchema.parse({ ...props, bodyWidth: value }), + ).toThrow() + expect(() => + airRegulatorModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + airRegulatorModelPropsSchema.parse({ ...props, ...{ knobDiameter: 38 } }), + ).toThrow() +}) + +test("airlubricator: strict typed schema, units and registry contract", () => { + const expected = { + fn: "airlubricator", + ...{ + bodyWidth: 40, + bodyDepth: 40, + bodyHeight: 30, + bowlDiameter: 28, + bowlLength: 50, + portDiameter: 8, + fillDiameter: 12, + fillHeight: 8, + }, + } + expect( + mp + .string( + "airlubricator_bodywidth40mm_bodydepth40mm_bodyheight30mm_bowldiameter28mm_bowllength50mm_portdiameter8mm_filldiameter12mm_fillheight8mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "AIRLUBRICATOR_BODYWIDTH40MM_BODYDEPTH40MM_BODYHEIGHT30MM_BOWLDIAMETER28MM_BOWLLENGTH50MM_PORTDIAMETER8MM_FILLDIAMETER12MM_FILLHEIGHT8MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("airlubricator").json()).toEqual(expected) + expect(airLubricatorModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + airLubricatorModelPropsSchema.parse({ ...props, bodyWidth: "4.0cm" }), + ).toEqual(props) + expect( + getAirLubricatorDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_bodywidth1mm", + "_bodywidth1e2", + "_bodywidth1mmjunk", + "_bodywidth", + "__", + ]) + expect(() => + mp + .string( + "airlubricator_bodywidth40mm_bodydepth40mm_bodyheight30mm_bowldiameter28mm_bowllength50mm_portdiameter8mm_filldiameter12mm_fillheight8mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + airLubricatorModelPropsSchema.parse({ ...props, bodyWidth: value }), + ).toThrow() + expect(() => + airLubricatorModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + airLubricatorModelPropsSchema.parse({ ...props, ...{ fillDiameter: 25 } }), + ).toThrow() +}) + +test("solenoidvalve: strict typed schema, units and registry contract", () => { + const expected = { + fn: "solenoidvalve", + ...{ + length: 70, + width: 28, + height: 25, + coilWidth: 24, + coilHeight: 30, + portDiameter: 6, + }, + } + expect( + mp + .string( + "solenoidvalve_length70mm_width28mm_height25mm_coilwidth24mm_coilheight30mm_portdiameter6mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "SOLENOIDVALVE_LENGTH70MM_WIDTH28MM_HEIGHT25MM_COILWIDTH24MM_COILHEIGHT30MM_PORTDIAMETER6MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("solenoidvalve").json()).toEqual(expected) + expect(solenoidValveModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + solenoidValveModelPropsSchema.parse({ ...props, length: "7.0cm" }), + ).toEqual(props) + expect( + getSolenoidValveDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_length1mm", + "_length1e2", + "_length1mmjunk", + "_length", + "__", + ]) + expect(() => + mp + .string( + "solenoidvalve_length70mm_width28mm_height25mm_coilwidth24mm_coilheight30mm_portdiameter6mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + solenoidValveModelPropsSchema.parse({ ...props, length: value }), + ).toThrow() + expect(() => + solenoidValveModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + solenoidValveModelPropsSchema.parse({ ...props, ...{ coilWidth: 40 } }), + ).toThrow() +}) + +test("pressureswitch: strict typed schema, units and registry contract", () => { + const expected = { + fn: "pressureswitch", + ...{ width: 32, depth: 28, height: 38, portDiameter: 8, portLength: 10 }, + } + expect( + mp + .string( + "pressureswitch_width32mm_depth28mm_height38mm_portdiameter8mm_portlength10mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "PRESSURESWITCH_WIDTH32MM_DEPTH28MM_HEIGHT38MM_PORTDIAMETER8MM_PORTLENGTH10MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("pressureswitch").json()).toEqual(expected) + expect(pressureSwitchModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + pressureSwitchModelPropsSchema.parse({ ...props, width: "3.2cm" }), + ).toEqual(props) + expect( + getPressureSwitchDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_width1mm", + "_width1e2", + "_width1mmjunk", + "_width", + "__", + ]) + expect(() => + mp + .string( + "pressureswitch_width32mm_depth28mm_height38mm_portdiameter8mm_portlength10mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + pressureSwitchModelPropsSchema.parse({ ...props, width: value }), + ).toThrow() + expect(() => + pressureSwitchModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + pressureSwitchModelPropsSchema.parse({ ...props, ...{ portDiameter: 18 } }), + ).toThrow() +}) + +test("pressuregauge: strict typed schema, units and registry contract", () => { + const expected = { + fn: "pressuregauge", + ...{ diameter: 60, depth: 22, stemDiameter: 10, stemLength: 15 }, + } + expect( + mp + .string( + "pressuregauge_diameter60mm_depth22mm_stemdiameter10mm_stemlength15mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "PRESSUREGAUGE_DIAMETER60MM_DEPTH22MM_STEMDIAMETER10MM_STEMLENGTH15MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("pressuregauge").json()).toEqual(expected) + expect(pressureGaugeModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + pressureGaugeModelPropsSchema.parse({ ...props, diameter: "6.0cm" }), + ).toEqual(props) + expect( + getPressureGaugeDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_diameter1mm", + "_diameter1e2", + "_diameter1mmjunk", + "_diameter", + "__", + ]) + expect(() => + mp + .string( + "pressuregauge_diameter60mm_depth22mm_stemdiameter10mm_stemlength15mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + pressureGaugeModelPropsSchema.parse({ ...props, diameter: value }), + ).toThrow() + expect(() => + pressureGaugeModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + pressureGaugeModelPropsSchema.parse({ ...props, ...{ stemDiameter: 22 } }), + ).toThrow() +}) + +test("flowmeter: strict typed schema, units and registry contract", () => { + const expected = { + fn: "flowmeter", + ...{ + length: 120, + diameter: 24, + boreDiameter: 12, + endDiameter: 32, + endLength: 12, + floatDiameter: 8, + }, + } + expect( + mp + .string( + "flowmeter_length120mm_diameter24mm_borediameter12mm_enddiameter32mm_endlength12mm_floatdiameter8mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "FLOWMETER_LENGTH120MM_DIAMETER24MM_BOREDIAMETER12MM_ENDDIAMETER32MM_ENDLENGTH12MM_FLOATDIAMETER8MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("flowmeter").json()).toEqual(expected) + expect(flowMeterModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + flowMeterModelPropsSchema.parse({ ...props, length: "12.0cm" }), + ).toEqual(props) + expect( + getFlowMeterDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_length1mm", + "_length1e2", + "_length1mmjunk", + "_length", + "__", + ]) + expect(() => + mp + .string( + "flowmeter_length120mm_diameter24mm_borediameter12mm_enddiameter32mm_endlength12mm_floatdiameter8mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + flowMeterModelPropsSchema.parse({ ...props, length: value }), + ).toThrow() + expect(() => + flowMeterModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + flowMeterModelPropsSchema.parse({ ...props, ...{ floatDiameter: 14 } }), + ).toThrow() +}) + +test("vacuumcup: strict typed schema, units and registry contract", () => { + const expected = { + fn: "vacuumcup", + ...{ + diameter: 35, + height: 22, + neckDiameter: 12, + neckLength: 12, + wallThickness: 2, + boreDiameter: 6, + }, + } + expect( + mp + .string( + "vacuumcup_diameter35mm_height22mm_neckdiameter12mm_necklength12mm_wallthickness2mm_borediameter6mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "VACUUMCUP_DIAMETER35MM_HEIGHT22MM_NECKDIAMETER12MM_NECKLENGTH12MM_WALLTHICKNESS2MM_BOREDIAMETER6MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("vacuumcup").json()).toEqual(expected) + expect(vacuumCupModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + vacuumCupModelPropsSchema.parse({ ...props, diameter: "3.5cm" }), + ).toEqual(props) + expect( + getVacuumCupDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_diameter1mm", + "_diameter1e2", + "_diameter1mmjunk", + "_diameter", + "__", + ]) + expect(() => + mp + .string( + "vacuumcup_diameter35mm_height22mm_neckdiameter12mm_necklength12mm_wallthickness2mm_borediameter6mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + vacuumCupModelPropsSchema.parse({ ...props, diameter: value }), + ).toThrow() + expect(() => + vacuumCupModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + vacuumCupModelPropsSchema.parse({ ...props, ...{ boreDiameter: 10 } }), + ).toThrow() +}) + +test("inductiveproximitysensor: strict typed schema, units and registry contract", () => { + const expected = { + fn: "inductiveproximitysensor", + ...{ + barrelDiameter: 18, + barrelLength: 55, + faceThickness: 2, + cableDiameter: 5, + cableLength: 15, + nutWidth: 22, + nutThickness: 4, + }, + } + expect( + mp + .string( + "inductiveproximitysensor_barreldiameter18mm_barrellength55mm_facethickness2mm_cablediameter5mm_cablelength15mm_nutwidth22mm_nutthickness4mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "INDUCTIVEPROXIMITYSENSOR_BARRELDIAMETER18MM_BARRELLENGTH55MM_FACETHICKNESS2MM_CABLEDIAMETER5MM_CABLELENGTH15MM_NUTWIDTH22MM_NUTTHICKNESS4MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("inductiveproximitysensor").json()).toEqual( + expected, + ) + expect( + inductiveProximitySensorModelDefinitionSchema.parse(expected), + ).toEqual(expected) + const { fn, ...props } = expected + expect( + inductiveProximitySensorModelPropsSchema.parse({ + ...props, + barrelDiameter: "1.8cm", + }), + ).toEqual(props) + expect( + getInductiveProximitySensorDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_barreldiameter1mm", + "_barreldiameter1e2", + "_barreldiameter1mmjunk", + "_barreldiameter", + "__", + ]) + expect(() => + mp + .string( + "inductiveproximitysensor_barreldiameter18mm_barrellength55mm_facethickness2mm_cablediameter5mm_cablelength15mm_nutwidth22mm_nutthickness4mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + inductiveProximitySensorModelPropsSchema.parse({ + ...props, + barrelDiameter: value, + }), + ).toThrow() + expect(() => + inductiveProximitySensorModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + inductiveProximitySensorModelPropsSchema.parse({ + ...props, + ...{ nutWidth: 16 }, + }), + ).toThrow() +}) + +test("photoelectricsensor: strict typed schema, units and registry contract", () => { + const expected = { + fn: "photoelectricsensor", + ...{ + width: 20, + depth: 35, + height: 55, + lensDiameter: 12, + cableDiameter: 5, + mountHoleDiameter: 4, + }, + } + expect( + mp + .string( + "photoelectricsensor_width20mm_depth35mm_height55mm_lensdiameter12mm_cablediameter5mm_mountholediameter4mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "PHOTOELECTRICSENSOR_WIDTH20MM_DEPTH35MM_HEIGHT55MM_LENSDIAMETER12MM_CABLEDIAMETER5MM_MOUNTHOLEDIAMETER4MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("photoelectricsensor").json()).toEqual(expected) + expect( + photoelectricSensorModelDefinitionSchema.parse(expected), + ).toEqual(expected) + const { fn, ...props } = expected + expect( + photoelectricSensorModelPropsSchema.parse({ ...props, width: "2.0cm" }), + ).toEqual(props) + expect( + getPhotoelectricSensorDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_width1mm", + "_width1e2", + "_width1mmjunk", + "_width", + "__", + ]) + expect(() => + mp + .string( + "photoelectricsensor_width20mm_depth35mm_height55mm_lensdiameter12mm_cablediameter5mm_mountholediameter4mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + photoelectricSensorModelPropsSchema.parse({ ...props, width: value }), + ).toThrow() + expect(() => + photoelectricSensorModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + photoelectricSensorModelPropsSchema.parse({ + ...props, + ...{ lensDiameter: 20 }, + }), + ).toThrow() +}) + +test("limitswitch: strict typed schema, units and registry contract", () => { + const expected = { + fn: "limitswitch", + ...{ + width: 30, + depth: 20, + height: 55, + actuatorLength: 30, + rollerDiameter: 12, + leverThickness: 3, + holeDiameter: 4, + }, + } + expect( + mp + .string( + "limitswitch_width30mm_depth20mm_height55mm_actuatorlength30mm_rollerdiameter12mm_leverthickness3mm_holediameter4mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "LIMITSWITCH_WIDTH30MM_DEPTH20MM_HEIGHT55MM_ACTUATORLENGTH30MM_ROLLERDIAMETER12MM_LEVERTHICKNESS3MM_HOLEDIAMETER4MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("limitswitch").json()).toEqual(expected) + expect(limitSwitchModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + limitSwitchModelPropsSchema.parse({ ...props, width: "3.0cm" }), + ).toEqual(props) + expect( + getLimitSwitchDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_width1mm", + "_width1e2", + "_width1mmjunk", + "_width", + "__", + ]) + expect(() => + mp + .string( + "limitswitch_width30mm_depth20mm_height55mm_actuatorlength30mm_rollerdiameter12mm_leverthickness3mm_holediameter4mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + limitSwitchModelPropsSchema.parse({ ...props, width: value }), + ).toThrow() + expect(() => + limitSwitchModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + limitSwitchModelPropsSchema.parse({ ...props, ...{ rollerDiameter: 35 } }), + ).toThrow() +}) + +test("pushbuttonoperator: strict typed schema, units and registry contract", () => { + const expected = { + fn: "pushbuttonoperator", + ...{ + panelDiameter: 22, + bezelDiameter: 30, + buttonDiameter: 22, + bodyLength: 35, + buttonHeight: 8, + }, + } + expect( + mp + .string( + "pushbuttonoperator_paneldiameter22mm_bezeldiameter30mm_buttondiameter22mm_bodylength35mm_buttonheight8mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "PUSHBUTTONOPERATOR_PANELDIAMETER22MM_BEZELDIAMETER30MM_BUTTONDIAMETER22MM_BODYLENGTH35MM_BUTTONHEIGHT8MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("pushbuttonoperator").json()).toEqual(expected) + expect( + pushbuttonOperatorModelDefinitionSchema.parse(expected), + ).toEqual(expected) + const { fn, ...props } = expected + expect( + pushbuttonOperatorModelPropsSchema.parse({ + ...props, + panelDiameter: "2.2cm", + }), + ).toEqual(props) + expect( + getPushbuttonOperatorDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_paneldiameter1mm", + "_paneldiameter1e2", + "_paneldiameter1mmjunk", + "_paneldiameter", + "__", + ]) + expect(() => + mp + .string( + "pushbuttonoperator_paneldiameter22mm_bezeldiameter30mm_buttondiameter22mm_bodylength35mm_buttonheight8mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + pushbuttonOperatorModelPropsSchema.parse({ + ...props, + panelDiameter: value, + }), + ).toThrow() + expect(() => + pushbuttonOperatorModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + pushbuttonOperatorModelPropsSchema.parse({ + ...props, + ...{ buttonDiameter: 30 }, + }), + ).toThrow() +}) + +test("emergencystopswitch: strict typed schema, units and registry contract", () => { + const expected = { + fn: "emergencystopswitch", + ...{ + panelDiameter: 22, + headDiameter: 40, + bodyLength: 45, + headHeight: 18, + collarDiameter: 30, + }, + } + expect( + mp + .string( + "emergencystopswitch_paneldiameter22mm_headdiameter40mm_bodylength45mm_headheight18mm_collardiameter30mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "EMERGENCYSTOPSWITCH_PANELDIAMETER22MM_HEADDIAMETER40MM_BODYLENGTH45MM_HEADHEIGHT18MM_COLLARDIAMETER30MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("emergencystopswitch").json()).toEqual(expected) + expect( + emergencyStopSwitchModelDefinitionSchema.parse(expected), + ).toEqual(expected) + const { fn, ...props } = expected + expect( + emergencyStopSwitchModelPropsSchema.parse({ + ...props, + panelDiameter: "2.2cm", + }), + ).toEqual(props) + expect( + getEmergencyStopSwitchDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_paneldiameter1mm", + "_paneldiameter1e2", + "_paneldiameter1mmjunk", + "_paneldiameter", + "__", + ]) + expect(() => + mp + .string( + "emergencystopswitch_paneldiameter22mm_headdiameter40mm_bodylength45mm_headheight18mm_collardiameter30mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + emergencyStopSwitchModelPropsSchema.parse({ + ...props, + panelDiameter: value, + }), + ).toThrow() + expect(() => + emergencyStopSwitchModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + emergencyStopSwitchModelPropsSchema.parse({ + ...props, + ...{ headDiameter: 28 }, + }), + ).toThrow() +}) + +test("selectoroperator: strict typed schema, units and registry contract", () => { + const expected = { + fn: "selectoroperator", + ...{ + panelDiameter: 22, + bezelDiameter: 30, + bodyLength: 35, + knobWidth: 28, + knobDepth: 10, + knobHeight: 15, + }, + } + expect( + mp + .string( + "selectoroperator_paneldiameter22mm_bezeldiameter30mm_bodylength35mm_knobwidth28mm_knobdepth10mm_knobheight15mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "SELECTOROPERATOR_PANELDIAMETER22MM_BEZELDIAMETER30MM_BODYLENGTH35MM_KNOBWIDTH28MM_KNOBDEPTH10MM_KNOBHEIGHT15MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("selectoroperator").json()).toEqual(expected) + expect( + selectorOperatorModelDefinitionSchema.parse(expected), + ).toEqual(expected) + const { fn, ...props } = expected + expect( + selectorOperatorModelPropsSchema.parse({ + ...props, + panelDiameter: "2.2cm", + }), + ).toEqual(props) + expect( + getSelectorOperatorDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_paneldiameter1mm", + "_paneldiameter1e2", + "_paneldiameter1mmjunk", + "_paneldiameter", + "__", + ]) + expect(() => + mp + .string( + "selectoroperator_paneldiameter22mm_bezeldiameter30mm_bodylength35mm_knobwidth28mm_knobdepth10mm_knobheight15mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + selectorOperatorModelPropsSchema.parse({ + ...props, + panelDiameter: value, + }), + ).toThrow() + expect(() => + selectorOperatorModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + selectorOperatorModelPropsSchema.parse({ ...props, ...{ knobDepth: 30 } }), + ).toThrow() +}) + +test("pilotlight: strict typed schema, units and registry contract", () => { + const expected = { + fn: "pilotlight", + ...{ panelDiameter: 22, lensDiameter: 28, bodyLength: 28, lensHeight: 8 }, + } + expect( + mp + .string( + "pilotlight_paneldiameter22mm_lensdiameter28mm_bodylength28mm_lensheight8mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "PILOTLIGHT_PANELDIAMETER22MM_LENSDIAMETER28MM_BODYLENGTH28MM_LENSHEIGHT8MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("pilotlight").json()).toEqual(expected) + expect(pilotLightModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + pilotLightModelPropsSchema.parse({ ...props, panelDiameter: "2.2cm" }), + ).toEqual(props) + expect( + getPilotLightDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_paneldiameter1mm", + "_paneldiameter1e2", + "_paneldiameter1mmjunk", + "_paneldiameter", + "__", + ]) + expect(() => + mp + .string( + "pilotlight_paneldiameter22mm_lensdiameter28mm_bodylength28mm_lensheight8mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + pilotLightModelPropsSchema.parse({ ...props, panelDiameter: value }), + ).toThrow() + expect(() => + pilotLightModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + pilotLightModelPropsSchema.parse({ ...props, ...{ lensDiameter: 20 } }), + ).toThrow() +}) + +test("dinrail: strict typed schema, units and registry contract", () => { + const expected = { + fn: "dinrail", + ...{ + width: 35, + height: 7.5, + thickness: 1, + length: 100, + holeDiameter: 5, + slotLength: 10, + slotPitch: 25, + }, + } + expect( + mp + .string( + "dinrail_width35mm_height7.5mm_thickness1mm_length100mm_holediameter5mm_slotlength10mm_slotpitch25mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "DINRAIL_WIDTH35MM_HEIGHT7.5MM_THICKNESS1MM_LENGTH100MM_HOLEDIAMETER5MM_SLOTLENGTH10MM_SLOTPITCH25MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("dinrail").json()).toEqual(expected) + expect(dinRailModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + dinRailModelPropsSchema.parse({ ...props, width: "3.5cm" }), + ).toEqual(props) + expect( + getDinRailDimensions(props).size.every((v) => Number.isFinite(v) && v > 0), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_width1mm", + "_width1e2", + "_width1mmjunk", + "_width", + "__", + ]) + expect(() => + mp + .string( + "dinrail_width35mm_height7.5mm_thickness1mm_length100mm_holediameter5mm_slotlength10mm_slotpitch25mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + dinRailModelPropsSchema.parse({ ...props, width: value }), + ).toThrow() + expect(() => + dinRailModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + dinRailModelPropsSchema.parse({ ...props, ...{ slotPitch: 10 } }), + ).toThrow() +}) + +test("terminalblock: strict typed schema, units and registry contract", () => { + const expected = { + fn: "terminalblock", + ...{ width: 6, depth: 40, height: 35, wireDiameter: 3, screwDiameter: 4 }, + } + expect( + mp + .string( + "terminalblock_width6mm_depth40mm_height35mm_wirediameter3mm_screwdiameter4mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "TERMINALBLOCK_WIDTH6MM_DEPTH40MM_HEIGHT35MM_WIREDIAMETER3MM_SCREWDIAMETER4MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("terminalblock").json()).toEqual(expected) + expect(terminalBlockModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + terminalBlockModelPropsSchema.parse({ ...props, width: "0.6cm" }), + ).toEqual(props) + expect( + getTerminalBlockDimensions(props).size.every( + (v) => Number.isFinite(v) && v > 0, + ), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_width1mm", + "_width1e2", + "_width1mmjunk", + "_width", + "__", + ]) + expect(() => + mp + .string( + "terminalblock_width6mm_depth40mm_height35mm_wirediameter3mm_screwdiameter4mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + terminalBlockModelPropsSchema.parse({ ...props, width: value }), + ).toThrow() + expect(() => + terminalBlockModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + terminalBlockModelPropsSchema.parse({ ...props, ...{ screwDiameter: 6 } }), + ).toThrow() +}) + +test("wireduct: strict typed schema, units and registry contract", () => { + const expected = { + fn: "wireduct", + ...{ + width: 40, + height: 40, + length: 100, + thickness: 2, + slotWidth: 4, + slotPitch: 8, + holeDiameter: 5, + }, + } + expect( + mp + .string( + "wireduct_width40mm_height40mm_length100mm_thickness2mm_slotwidth4mm_slotpitch8mm_holediameter5mm", + ) + .json(), + ).toEqual(expected) + expect( + mp + .string( + "WIREDUCT_WIDTH40MM_HEIGHT40MM_LENGTH100MM_THICKNESS2MM_SLOTWIDTH4MM_SLOTPITCH8MM_HOLEDIAMETER5MM", + ) + .json(), + ).toEqual(expected) + expect(mp.string("wireduct").json()).toEqual(expected) + expect(wireDuctModelDefinitionSchema.parse(expected)).toEqual( + expected, + ) + const { fn, ...props } = expected + expect( + wireDuctModelPropsSchema.parse({ ...props, width: "4.0cm" }), + ).toEqual(props) + expect( + getWireDuctDimensions(props).size.every((v) => Number.isFinite(v) && v > 0), + ).toBe(true) + for (const suffix of [ + "_typo1mm", + "_width1mm", + "_width1e2", + "_width1mmjunk", + "_width", + "__", + ]) + expect(() => + mp + .string( + "wireduct_width40mm_height40mm_length100mm_thickness2mm_slotwidth4mm_slotpitch8mm_holediameter5mm" + + suffix, + ) + .json(), + ).toThrow() + for (const value of [ + 0, + -1, + Infinity, + NaN, + "9".repeat(400) + "mm", + "1mmjunk", + "1e2", + "(2mm)", + ]) + expect(() => + wireDuctModelPropsSchema.parse({ ...props, width: value }), + ).toThrow() + expect(() => + wireDuctModelPropsSchema.parse({ ...props, unknown: true }), + ).toThrow() + expect(() => + wireDuctModelPropsSchema.parse({ ...props, ...{ slotWidth: 9 } }), + ).toThrow() +}) + +test("repeated rail and duct patterns reject excessive feature counts", () => { + expect(() => + dinRailModelPropsSchema.parse({ + length: 100, + slotPitch: 0.01, + slotLength: 0.005, + holeDiameter: 0.003, + }), + ).toThrow() + expect(() => + wireDuctModelPropsSchema.parse({ + length: 100, + slotPitch: 0.01, + slotWidth: 0.005, + }), + ).toThrow() + expect(() => + wireDuctModelPropsSchema.parse({ length: 12801, slotPitch: 30 }), + ).toThrow() +}) diff --git a/tests/industrial-fasteners.test.ts b/tests/industrial-fasteners.test.ts new file mode 100644 index 0000000..2af4d3c --- /dev/null +++ b/tests/industrial-fasteners.test.ts @@ -0,0 +1,413 @@ +import { expect, test } from "bun:test" +import { ModelRegistry } from "../src/model-registry" +import { parseModelStringParams } from "../src/parse-model-string" +import { + flatWasherModelPropsSchema, + getFlatWasherDimensions, +} from "../src/models/flatwasher" +import { parseFlatWasherModelParams } from "../src/models/flatwasher/parse-model-string" +import { model as flatWasherRegistration } from "../src/models/flatwasher/register" +import { + toothedWasherModelPropsSchema, + getToothedWasherDimensions, +} from "../src/models/toothedwasher" +import { parseToothedWasherModelParams } from "../src/models/toothedwasher/parse-model-string" +import { model as toothedWasherRegistration } from "../src/models/toothedwasher/register" +import { + waveWasherModelPropsSchema, + getWaveWasherDimensions, +} from "../src/models/wavewasher" +import { parseWaveWasherModelParams } from "../src/models/wavewasher/parse-model-string" +import { model as waveWasherRegistration } from "../src/models/wavewasher/register" +import { + bellevilleWasherModelPropsSchema, + getBellevilleWasherDimensions, +} from "../src/models/bellevillewasher" +import { parseBellevilleWasherModelParams } from "../src/models/bellevillewasher/parse-model-string" +import { model as bellevilleWasherRegistration } from "../src/models/bellevillewasher/register" +import { + shoulderBoltModelPropsSchema, + getShoulderBoltDimensions, +} from "../src/models/shoulderbolt" +import { parseShoulderBoltModelParams } from "../src/models/shoulderbolt/parse-model-string" +import { model as shoulderBoltRegistration } from "../src/models/shoulderbolt/register" +import { uBoltModelPropsSchema, getUBoltDimensions } from "../src/models/ubolt" +import { parseUBoltModelParams } from "../src/models/ubolt/parse-model-string" +import { model as uBoltRegistration } from "../src/models/ubolt/register" +import { + eyeBoltModelPropsSchema, + getEyeBoltDimensions, +} from "../src/models/eyebolt" +import { parseEyeBoltModelParams } from "../src/models/eyebolt/parse-model-string" +import { model as eyeBoltRegistration } from "../src/models/eyebolt/register" +import { + carriageBoltModelPropsSchema, + getCarriageBoltDimensions, +} from "../src/models/carriagebolt" +import { parseCarriageBoltModelParams } from "../src/models/carriagebolt/parse-model-string" +import { model as carriageBoltRegistration } from "../src/models/carriagebolt/register" +import { + wingNutModelPropsSchema, + getWingNutDimensions, +} from "../src/models/wingnut" +import { parseWingNutModelParams } from "../src/models/wingnut/parse-model-string" +import { model as wingNutRegistration } from "../src/models/wingnut/register" +import { + capNutModelPropsSchema, + getCapNutDimensions, +} from "../src/models/capnut" +import { parseCapNutModelParams } from "../src/models/capnut/parse-model-string" +import { model as capNutRegistration } from "../src/models/capnut/register" +import { + couplingNutModelPropsSchema, + getCouplingNutDimensions, +} from "../src/models/couplingnut" +import { parseCouplingNutModelParams } from "../src/models/couplingnut/parse-model-string" +import { model as couplingNutRegistration } from "../src/models/couplingnut/register" +import { + tSlotNutModelPropsSchema, + getTSlotNutDimensions, +} from "../src/models/tslotnut" +import { parseTSlotNutModelParams } from "../src/models/tslotnut/parse-model-string" +import { model as tSlotNutRegistration } from "../src/models/tslotnut/register" +import { + rivetNutModelPropsSchema, + getRivetNutDimensions, +} from "../src/models/rivetnut" +import { parseRivetNutModelParams } from "../src/models/rivetnut/parse-model-string" +import { model as rivetNutRegistration } from "../src/models/rivetnut/register" +import { + solidRivetModelPropsSchema, + getSolidRivetDimensions, +} from "../src/models/solidrivet" +import { parseSolidRivetModelParams } from "../src/models/solidrivet/parse-model-string" +import { model as solidRivetRegistration } from "../src/models/solidrivet/register" +import { + blindRivetModelPropsSchema, + getBlindRivetDimensions, +} from "../src/models/blindrivet" +import { parseBlindRivetModelParams } from "../src/models/blindrivet/parse-model-string" +import { model as blindRivetRegistration } from "../src/models/blindrivet/register" +import { + clevisPinModelPropsSchema, + getClevisPinDimensions, +} from "../src/models/clevispin" +import { parseClevisPinModelParams } from "../src/models/clevispin/parse-model-string" +import { model as clevisPinRegistration } from "../src/models/clevispin/register" +import { + springPinModelPropsSchema, + getSpringPinDimensions, +} from "../src/models/springpin" +import { parseSpringPinModelParams } from "../src/models/springpin/parse-model-string" +import { model as springPinRegistration } from "../src/models/springpin/register" +import { + taperPinModelPropsSchema, + getTaperPinDimensions, +} from "../src/models/taperpin" +import { parseTaperPinModelParams } from "../src/models/taperpin/parse-model-string" +import { model as taperPinRegistration } from "../src/models/taperpin/register" +import { + retainingRingModelPropsSchema, + getRetainingRingDimensions, +} from "../src/models/retainingring" +import { parseRetainingRingModelParams } from "../src/models/retainingring/parse-model-string" +import { model as retainingRingRegistration } from "../src/models/retainingring/register" +import { + hairpinCotterModelPropsSchema, + getHairpinCotterDimensions, +} from "../src/models/hairpincotter" +import { parseHairpinCotterModelParams } from "../src/models/hairpincotter/parse-model-string" +import { model as hairpinCotterRegistration } from "../src/models/hairpincotter/register" +const cases = [ + { + name: "flatwasher", + schema: flatWasherModelPropsSchema, + parse: parseFlatWasherModelParams, + defaults: getFlatWasherDimensions(), + registration: flatWasherRegistration, + example: "flatwasher_id6.4mm_od12mm_t1.6mm", + key: "innerDiameter", + alias: "id", + invalid: { innerDiameter: 13 }, + }, + { + name: "toothedwasher", + schema: toothedWasherModelPropsSchema, + parse: parseToothedWasherModelParams, + defaults: getToothedWasherDimensions(), + registration: toothedWasherRegistration, + example: "toothedwasher_id6.4mm_od13mm_t0.8mm_td1.5mm", + key: "innerDiameter", + alias: "id", + invalid: { toothDepth: 5 }, + }, + { + name: "wavewasher", + schema: waveWasherModelPropsSchema, + parse: parseWaveWasherModelParams, + defaults: getWaveWasherDimensions(), + registration: waveWasherRegistration, + example: "wavewasher_id10mm_od16mm_t0.6mm_wh1.5mm", + key: "innerDiameter", + alias: "id", + invalid: { innerDiameter: 17 }, + }, + { + name: "bellevillewasher", + schema: bellevilleWasherModelPropsSchema, + parse: parseBellevilleWasherModelParams, + defaults: getBellevilleWasherDimensions(), + registration: bellevilleWasherRegistration, + example: "bellevillewasher_id6.4mm_od14mm_t0.8mm_ch1.5mm", + key: "innerDiameter", + alias: "id", + invalid: { innerDiameter: 15 }, + }, + { + name: "shoulderbolt", + schema: shoulderBoltModelPropsSchema, + parse: parseShoulderBoltModelParams, + defaults: getShoulderBoltDimensions(), + registration: shoulderBoltRegistration, + example: "shoulderbolt_sd8mm_sl20mm_td6mm_tl10mm_hd13mm_hh5mm", + key: "shoulderDiameter", + alias: "sd", + invalid: { threadDiameter: 9 }, + }, + { + name: "ubolt", + schema: uBoltModelPropsSchema, + parse: parseUBoltModelParams, + defaults: getUBoltDimensions(), + registration: uBoltRegistration, + example: "ubolt_d6mm_s30mm_l35mm", + key: "rodDiameter", + alias: "d", + invalid: { rodDiameter: 31 }, + }, + { + name: "eyebolt", + schema: eyeBoltModelPropsSchema, + parse: parseEyeBoltModelParams, + defaults: getEyeBoltDimensions(), + registration: eyeBoltRegistration, + example: "eyebolt_d6mm_l24mm_id14mm_wd5mm", + key: "shaftDiameter", + alias: "d", + invalid: { eyeWireDiameter: 15 }, + }, + { + name: "carriagebolt", + schema: carriageBoltModelPropsSchema, + parse: parseCarriageBoltModelParams, + defaults: getCarriageBoltDimensions(), + registration: carriageBoltRegistration, + example: "carriagebolt_d6mm_l25mm_nw6.5mm_nh3mm_hd14mm_hh4mm", + key: "shaftDiameter", + alias: "d", + invalid: { neckWidth: 11 }, + }, + { + name: "wingnut", + schema: wingNutModelPropsSchema, + parse: parseWingNutModelParams, + defaults: getWingNutDimensions(), + registration: wingNutRegistration, + example: "wingnut_id6mm_af10mm_h5mm_s28mm_wh10mm_wt3mm", + key: "boreDiameter", + alias: "id", + invalid: { wingSpan: 8 }, + }, + { + name: "capnut", + schema: capNutModelPropsSchema, + parse: parseCapNutModelParams, + defaults: getCapNutDimensions(), + registration: capNutRegistration, + example: "capnut_id6mm_af10mm_h5mm_ch5mm_wt1mm", + key: "boreDiameter", + alias: "id", + invalid: { wallThickness: 3 }, + }, + { + name: "couplingnut", + schema: couplingNutModelPropsSchema, + parse: parseCouplingNutModelParams, + defaults: getCouplingNutDimensions(), + registration: couplingNutRegistration, + example: "couplingnut_id6mm_af10mm_h18mm", + key: "boreDiameter", + alias: "id", + invalid: { height: 9 }, + }, + { + name: "tslotnut", + schema: tSlotNutModelPropsSchema, + parse: parseTSlotNutModelParams, + defaults: getTSlotNutDimensions(), + registration: tSlotNutRegistration, + example: "tslotnut_id6mm_w16mm_l20mm_bh4mm_nw10mm_nh4mm", + key: "boreDiameter", + alias: "id", + invalid: { neckWidth: 17 }, + }, + { + name: "rivetnut", + schema: rivetNutModelPropsSchema, + parse: parseRivetNutModelParams, + defaults: getRivetNutDimensions(), + registration: rivetNutRegistration, + example: "rivetnut_id6mm_d9mm_l14mm_fd12mm_ft1.5mm", + key: "boreDiameter", + alias: "id", + invalid: { boreDiameter: 10 }, + }, + { + name: "solidrivet", + schema: solidRivetModelPropsSchema, + parse: parseSolidRivetModelParams, + defaults: getSolidRivetDimensions(), + registration: solidRivetRegistration, + example: "solidrivet_d4mm_l12mm_hd8mm_hh2.5mm", + key: "shaftDiameter", + alias: "d", + invalid: { shaftDiameter: 9 }, + }, + { + name: "blindrivet", + schema: blindRivetModelPropsSchema, + parse: parseBlindRivetModelParams, + defaults: getBlindRivetDimensions(), + registration: blindRivetRegistration, + example: "blindrivet_d4mm_l10mm_fd8mm_ft1.2mm_md2mm_ml20mm", + key: "bodyDiameter", + alias: "d", + invalid: { mandrelDiameter: 5 }, + }, + { + name: "clevispin", + schema: clevisPinModelPropsSchema, + parse: parseClevisPinModelParams, + defaults: getClevisPinDimensions(), + registration: clevisPinRegistration, + example: "clevispin_d6mm_l25mm_hd10mm_hh2mm_hol2mm_ho3mm", + key: "shaftDiameter", + alias: "d", + invalid: { holeOffset: 0.5 }, + }, + { + name: "springpin", + schema: springPinModelPropsSchema, + parse: parseSpringPinModelParams, + defaults: getSpringPinDimensions(), + registration: springPinRegistration, + example: "springpin_od6mm_l20mm_wt1mm_g1mm", + key: "outerDiameter", + alias: "od", + invalid: { wallThickness: 4 }, + }, + { + name: "taperpin", + schema: taperPinModelPropsSchema, + parse: parseTaperPinModelParams, + defaults: getTaperPinDimensions(), + registration: taperPinRegistration, + example: "taperpin_ld6mm_sd5mm_l30mm", + key: "largeDiameter", + alias: "ld", + invalid: { smallDiameter: 7 }, + }, + { + name: "retainingring", + schema: retainingRingModelPropsSchema, + parse: parseRetainingRingModelParams, + defaults: getRetainingRingDimensions(), + registration: retainingRingRegistration, + example: "retainingring_id16mm_od20mm_t1.2mm_g5mm_ld4mm_hol1.5mm", + key: "innerDiameter", + alias: "id", + invalid: { holeDiameter: 5 }, + }, + { + name: "hairpincotter", + schema: hairpinCotterModelPropsSchema, + parse: parseHairpinCotterModelParams, + defaults: getHairpinCotterDimensions(), + registration: hairpinCotterRegistration, + example: "hairpincotter_d2mm_l40mm_ld10mm", + key: "wireDiameter", + alias: "d", + invalid: { length: 10 }, + }, +] +for (const entry of cases) { + test(`${entry.name} has typed defaults and its explicit dimensional string routes through its registration`, () => { + const actual = entry.parse(parseModelStringParams(entry.example)) + expect(actual).toEqual({ fn: entry.name, ...entry.defaults }) + expect(entry.parse(parseModelStringParams(entry.name))).toEqual(actual) + const registry = new ModelRegistry() + registry.register( + entry.registration as Parameters[0], + ) + expect( + registry.parse(parseModelStringParams(entry.example)), + ).toEqual(actual) + }) + test(`${entry.name} accepts equivalent inch and centimeter inputs`, () => { + const defaults = entry.defaults as Record + const inches = Object.fromEntries( + Object.entries(defaults).map(([key, value]) => [ + key, + `${value / 25.4}in`, + ]), + ) + const centimeters = Object.fromEntries( + Object.entries(defaults).map(([key, value]) => [key, `${value / 10}cm`]), + ) + for (const input of [inches, centimeters]) { + const p = entry.schema.parse(input) as Record + for (const [key, value] of Object.entries(defaults)) + expect(p[key]).toBeCloseTo(value, 9) + } + const token = entry.alias + String(defaults[entry.key]! / 25.4) + "in" + const parsed = entry.parse( + parseModelStringParams(`${entry.name}_${token}`), + ) as unknown as Record + expect(parsed[entry.key]).toBeCloseTo(defaults[entry.key]!, 9) + }) + test(`${entry.name} rejects ambiguous tokens, invalid units and impossible dimensions`, () => { + for (const value of [0, -1, NaN, Infinity, 0.000001, 10001]) + for (const key of Object.keys(entry.defaults)) + expect(entry.schema.safeParse({ [key]: value }).success).toBe(false) + expect(entry.schema.safeParse(entry.invalid).success).toBe(false) + expect(entry.schema.safeParse({ unknown: 1 }).success).toBe(false) + for (const suffix of [ + "unknown5", + `${entry.alias}1watts`, + `${entry.alias}NaN`, + `${entry.alias}5mm_${entry.key.toLowerCase()}5mm`, + ]) + expect(() => + entry.parse(parseModelStringParams(`${entry.name}_${suffix}`)), + ).toThrow() + expect(() => + entry.parse(parseModelStringParams(`${entry.name}1`)), + ).toThrow() + }) +} + +test("thin fastener walls and intersecting retention features fail before rendering", () => { + for (const [schema, props] of [ + [flatWasherModelPropsSchema, { innerDiameter: 11.999999 }], + [toothedWasherModelPropsSchema, { innerDiameter: 9.999999 }], + [wingNutModelPropsSchema, { boreDiameter: 9.999999 }], + [capNutModelPropsSchema, { wallThickness: 1.999999 }], + [rivetNutModelPropsSchema, { boreDiameter: 8.8 }], + [blindRivetModelPropsSchema, { mandrelDiameter: 3.8 }], + [clevisPinModelPropsSchema, { holeOffset: 1.000001 }], + [springPinModelPropsSchema, { gapWidth: 3.999999 }], + [taperPinModelPropsSchema, { smallDiameter: 5.999999 }], + [retainingRingModelPropsSchema, { gapWidth: 4.000001 }], + ] as const) + expect(schema.safeParse(props).success).toBe(false) +}) diff --git a/tests/industrial-plumbing-contracts.test.ts b/tests/industrial-plumbing-contracts.test.ts new file mode 100644 index 0000000..0a19440 --- /dev/null +++ b/tests/industrial-plumbing-contracts.test.ts @@ -0,0 +1,1185 @@ +import { expect, test } from "bun:test" +import { ModelRegistry } from "../src/model-registry" +import { parseModelStringParams } from "../src/parse-model-string" +import { model as rigidPipe } from "../src/models/rigidpipe/register" +import { rigidPipeModelPropsSchema as rigidPipeSchema } from "../src/models/rigidpipe" +import { model as pipeElbow } from "../src/models/pipeelbow/register" +import { pipeElbowModelPropsSchema as pipeElbowSchema } from "../src/models/pipeelbow" +import { model as pipeTee } from "../src/models/pipetee/register" +import { pipeTeeModelPropsSchema as pipeTeeSchema } from "../src/models/pipetee" +import { model as pipeCross } from "../src/models/pipecross/register" +import { pipeCrossModelPropsSchema as pipeCrossSchema } from "../src/models/pipecross" +import { model as pipeCoupling } from "../src/models/pipecoupling/register" +import { pipeCouplingModelPropsSchema as pipeCouplingSchema } from "../src/models/pipecoupling" +import { model as pipeUnion } from "../src/models/pipeunion/register" +import { pipeUnionModelPropsSchema as pipeUnionSchema } from "../src/models/pipeunion" +import { model as pipeNipple } from "../src/models/pipenipple/register" +import { pipeNippleModelPropsSchema as pipeNippleSchema } from "../src/models/pipenipple" +import { model as pipeCap } from "../src/models/pipecap/register" +import { pipeCapModelPropsSchema as pipeCapSchema } from "../src/models/pipecap" +import { model as pipePlug } from "../src/models/pipeplug/register" +import { pipePlugModelPropsSchema as pipePlugSchema } from "../src/models/pipeplug" +import { model as reducingBushing } from "../src/models/reducingbushing/register" +import { reducingBushingModelPropsSchema as reducingBushingSchema } from "../src/models/reducingbushing" +import { model as hoseBarb } from "../src/models/hosebarb/register" +import { hoseBarbModelPropsSchema as hoseBarbSchema } from "../src/models/hosebarb" +import { model as bulkheadFitting } from "../src/models/bulkheadfitting/register" +import { bulkheadFittingModelPropsSchema as bulkheadFittingSchema } from "../src/models/bulkheadfitting" +import { model as pushConnectFitting } from "../src/models/pushconnectfitting/register" +import { pushConnectFittingModelPropsSchema as pushConnectFittingSchema } from "../src/models/pushconnectfitting" +import { model as compressionFitting } from "../src/models/compressionfitting/register" +import { compressionFittingModelPropsSchema as compressionFittingSchema } from "../src/models/compressionfitting" +import { model as hoseClamp } from "../src/models/hoseclamp/register" +import { hoseClampModelPropsSchema as hoseClampSchema } from "../src/models/hoseclamp" +import { model as pipeFlange } from "../src/models/pipeflange/register" +import { pipeFlangeModelPropsSchema as pipeFlangeSchema } from "../src/models/pipeflange" +import { model as ballValve } from "../src/models/ballvalve/register" +import { ballValveModelPropsSchema as ballValveSchema } from "../src/models/ballvalve" +import { model as checkValve } from "../src/models/checkvalve/register" +import { checkValveModelPropsSchema as checkValveSchema } from "../src/models/checkvalve" +import { model as needleValve } from "../src/models/needlevalve/register" +import { needleValveModelPropsSchema as needleValveSchema } from "../src/models/needlevalve" +import { model as manifold } from "../src/models/manifold/register" +import { manifoldModelPropsSchema as manifoldSchema } from "../src/models/manifold" + +test("rigidpipe: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(rigidPipe) + const p = rigidPipeSchema.parse({}) + expect(p).toEqual({ outerDiameter: 20, boreDiameter: 16, length: 100 }) + expect( + rigidPipeSchema.parse({ + outerDiameter: "2.0cm", + boreDiameter: "1.6cm", + length: "10.0cm", + }), + ).toEqual(p) + expect( + registry.parse(parseModelStringParams("rigidpipe_od20mm_bore16mm_l100mm")), + ).toEqual({ fn: "rigidpipe", ...p }) + expect(registry.parse(parseModelStringParams("rigidpipe"))).toEqual({ + fn: "rigidpipe", + ...p, + }) + expect(rigidPipe.schema.parse({ fn: "rigidpipe", ...p })).toEqual({ + fn: "rigidpipe", + ...p, + }) + expect(() => rigidPipeSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => rigidPipeSchema.parse({ ...p, outerDiameter: bad })).toThrow() + for (const text of [ + "rigidpipe_od20mm_outerdiameter20mm", + "rigidpipe_unknown3mm", + "rigidpipe_od2mmjunk", + "rigidpipe_od", + "rigidpipe2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("pipeelbow: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(pipeElbow) + const p = pipeElbowSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 24, + boreDiameter: 16, + legLength: 36, + socketDiameter: 28, + socketLength: 10, + }) + expect( + pipeElbowSchema.parse({ + outerDiameter: "2.4cm", + boreDiameter: "1.6cm", + legLength: "3.6cm", + socketDiameter: "2.8cm", + socketLength: "1.0cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "pipeelbow_od24mm_bore16mm_leg36mm_socket28mm_socketl10mm", + ), + ), + ).toEqual({ fn: "pipeelbow", ...p }) + expect(registry.parse(parseModelStringParams("pipeelbow"))).toEqual({ + fn: "pipeelbow", + ...p, + }) + expect(pipeElbow.schema.parse({ fn: "pipeelbow", ...p })).toEqual({ + fn: "pipeelbow", + ...p, + }) + expect(() => pipeElbowSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => pipeElbowSchema.parse({ ...p, outerDiameter: bad })).toThrow() + for (const text of [ + "pipeelbow_od24mm_outerdiameter24mm", + "pipeelbow_unknown3mm", + "pipeelbow_od2mmjunk", + "pipeelbow_od", + "pipeelbow2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("pipetee: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(pipeTee) + const p = pipeTeeSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 24, + boreDiameter: 16, + length: 70, + branchLength: 35, + socketDiameter: 28, + socketLength: 10, + }) + expect( + pipeTeeSchema.parse({ + outerDiameter: "2.4cm", + boreDiameter: "1.6cm", + length: "7.0cm", + branchLength: "3.5cm", + socketDiameter: "2.8cm", + socketLength: "1.0cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "pipetee_od24mm_bore16mm_l70mm_branch35mm_socket28mm_socketl10mm", + ), + ), + ).toEqual({ fn: "pipetee", ...p }) + expect(registry.parse(parseModelStringParams("pipetee"))).toEqual({ + fn: "pipetee", + ...p, + }) + expect(pipeTee.schema.parse({ fn: "pipetee", ...p })).toEqual({ + fn: "pipetee", + ...p, + }) + expect(() => pipeTeeSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => pipeTeeSchema.parse({ ...p, outerDiameter: bad })).toThrow() + for (const text of [ + "pipetee_od24mm_outerdiameter24mm", + "pipetee_unknown3mm", + "pipetee_od2mmjunk", + "pipetee_od", + "pipetee2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("pipecross: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(pipeCross) + const p = pipeCrossSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 24, + boreDiameter: 16, + span: 70, + socketDiameter: 28, + socketLength: 10, + }) + expect( + pipeCrossSchema.parse({ + outerDiameter: "2.4cm", + boreDiameter: "1.6cm", + span: "7.0cm", + socketDiameter: "2.8cm", + socketLength: "1.0cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "pipecross_od24mm_bore16mm_span70mm_socket28mm_socketl10mm", + ), + ), + ).toEqual({ fn: "pipecross", ...p }) + expect(registry.parse(parseModelStringParams("pipecross"))).toEqual({ + fn: "pipecross", + ...p, + }) + expect(pipeCross.schema.parse({ fn: "pipecross", ...p })).toEqual({ + fn: "pipecross", + ...p, + }) + expect(() => pipeCrossSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => pipeCrossSchema.parse({ ...p, outerDiameter: bad })).toThrow() + for (const text of [ + "pipecross_od24mm_outerdiameter24mm", + "pipecross_unknown3mm", + "pipecross_od2mmjunk", + "pipecross_od", + "pipecross2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("pipecoupling: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(pipeCoupling) + const p = pipeCouplingSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 30, + boreDiameter: 16, + socketDiameter: 20, + length: 40, + socketDepth: 14, + }) + expect( + pipeCouplingSchema.parse({ + outerDiameter: "3.0cm", + boreDiameter: "1.6cm", + socketDiameter: "2.0cm", + length: "4.0cm", + socketDepth: "1.4cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "pipecoupling_od30mm_bore16mm_socket20mm_l40mm_depth14mm", + ), + ), + ).toEqual({ fn: "pipecoupling", ...p }) + expect(registry.parse(parseModelStringParams("pipecoupling"))).toEqual({ + fn: "pipecoupling", + ...p, + }) + expect(pipeCoupling.schema.parse({ fn: "pipecoupling", ...p })).toEqual({ + fn: "pipecoupling", + ...p, + }) + expect(() => pipeCouplingSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => + pipeCouplingSchema.parse({ ...p, outerDiameter: bad }), + ).toThrow() + for (const text of [ + "pipecoupling_od30mm_outerdiameter30mm", + "pipecoupling_unknown3mm", + "pipecoupling_od2mmjunk", + "pipecoupling_od", + "pipecoupling2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("pipeunion: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(pipeUnion) + const p = pipeUnionSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 30, + boreDiameter: 16, + length: 55, + nutAcrossFlats: 36, + nutLength: 20, + endDiameter: 24, + }) + expect( + pipeUnionSchema.parse({ + outerDiameter: "3.0cm", + boreDiameter: "1.6cm", + length: "5.5cm", + nutAcrossFlats: "3.6cm", + nutLength: "2.0cm", + endDiameter: "2.4cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "pipeunion_od30mm_bore16mm_l55mm_af36mm_nutl20mm_endod24mm", + ), + ), + ).toEqual({ fn: "pipeunion", ...p }) + expect(registry.parse(parseModelStringParams("pipeunion"))).toEqual({ + fn: "pipeunion", + ...p, + }) + expect(pipeUnion.schema.parse({ fn: "pipeunion", ...p })).toEqual({ + fn: "pipeunion", + ...p, + }) + expect(() => pipeUnionSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => pipeUnionSchema.parse({ ...p, outerDiameter: bad })).toThrow() + for (const text of [ + "pipeunion_od30mm_outerdiameter30mm", + "pipeunion_unknown3mm", + "pipeunion_od2mmjunk", + "pipeunion_od", + "pipeunion2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("pipenipple: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(pipeNipple) + const p = pipeNippleSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 20, + boreDiameter: 16, + length: 50, + threadLength: 12, + shoulderDiameter: 22, + }) + expect( + pipeNippleSchema.parse({ + outerDiameter: "2.0cm", + boreDiameter: "1.6cm", + length: "5.0cm", + threadLength: "1.2cm", + shoulderDiameter: "2.2cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "pipenipple_od20mm_bore16mm_l50mm_threadl12mm_shoulder22mm", + ), + ), + ).toEqual({ fn: "pipenipple", ...p }) + expect(registry.parse(parseModelStringParams("pipenipple"))).toEqual({ + fn: "pipenipple", + ...p, + }) + expect(pipeNipple.schema.parse({ fn: "pipenipple", ...p })).toEqual({ + fn: "pipenipple", + ...p, + }) + expect(() => pipeNippleSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => pipeNippleSchema.parse({ ...p, outerDiameter: bad })).toThrow() + for (const text of [ + "pipenipple_od20mm_outerdiameter20mm", + "pipenipple_unknown3mm", + "pipenipple_od2mmjunk", + "pipenipple_od", + "pipenipple2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("pipecap: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(pipeCap) + const p = pipeCapSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 30, + socketDiameter: 20, + length: 25, + floorThickness: 4, + hexAcrossFlats: 32, + }) + expect( + pipeCapSchema.parse({ + outerDiameter: "3.0cm", + socketDiameter: "2.0cm", + length: "2.5cm", + floorThickness: "0.4cm", + hexAcrossFlats: "3.2cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams("pipecap_od30mm_socket20mm_l25mm_floor4mm_af32mm"), + ), + ).toEqual({ fn: "pipecap", ...p }) + expect(registry.parse(parseModelStringParams("pipecap"))).toEqual({ + fn: "pipecap", + ...p, + }) + expect(pipeCap.schema.parse({ fn: "pipecap", ...p })).toEqual({ + fn: "pipecap", + ...p, + }) + expect(() => pipeCapSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => pipeCapSchema.parse({ ...p, outerDiameter: bad })).toThrow() + for (const text of [ + "pipecap_od30mm_outerdiameter30mm", + "pipecap_unknown3mm", + "pipecap_od2mmjunk", + "pipecap_od", + "pipecap2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("pipeplug: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(pipePlug) + const p = pipePlugSchema.parse({}) + expect(p).toEqual({ + diameter: 20, + length: 20, + headAcrossFlats: 26, + headHeight: 8, + }) + expect( + pipePlugSchema.parse({ + diameter: "2.0cm", + length: "2.0cm", + headAcrossFlats: "2.6cm", + headHeight: "0.8cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams("pipeplug_d20mm_l20mm_af26mm_head8mm"), + ), + ).toEqual({ fn: "pipeplug", ...p }) + expect(registry.parse(parseModelStringParams("pipeplug"))).toEqual({ + fn: "pipeplug", + ...p, + }) + expect(pipePlug.schema.parse({ fn: "pipeplug", ...p })).toEqual({ + fn: "pipeplug", + ...p, + }) + expect(() => pipePlugSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => pipePlugSchema.parse({ ...p, diameter: bad })).toThrow() + for (const text of [ + "pipeplug_d20mm_diameter20mm", + "pipeplug_unknown3mm", + "pipeplug_d2mmjunk", + "pipeplug_d", + "pipeplug2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("reducingbushing: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(reducingBushing) + const p = reducingBushingSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 26, + largeBoreDiameter: 18, + smallBoreDiameter: 10, + length: 30, + hexAcrossFlats: 30, + hexLength: 10, + }) + expect( + reducingBushingSchema.parse({ + outerDiameter: "2.6cm", + largeBoreDiameter: "1.8cm", + smallBoreDiameter: "1.0cm", + length: "3.0cm", + hexAcrossFlats: "3.0cm", + hexLength: "1.0cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "reducingbushing_od26mm_largebore18mm_smallbore10mm_l30mm_af30mm_hexl10mm", + ), + ), + ).toEqual({ fn: "reducingbushing", ...p }) + expect(registry.parse(parseModelStringParams("reducingbushing"))).toEqual({ + fn: "reducingbushing", + ...p, + }) + expect(reducingBushing.schema.parse({ fn: "reducingbushing", ...p })).toEqual( + { fn: "reducingbushing", ...p }, + ) + expect(() => reducingBushingSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => + reducingBushingSchema.parse({ ...p, outerDiameter: bad }), + ).toThrow() + for (const text of [ + "reducingbushing_od26mm_outerdiameter26mm", + "reducingbushing_unknown3mm", + "reducingbushing_od2mmjunk", + "reducingbushing_od", + "reducingbushing2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("hosebarb: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(hoseBarb) + const p = hoseBarbSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 20, + boreDiameter: 10, + length: 45, + barbDiameter: 16, + barbLength: 25, + barbPitch: 6, + hexAcrossFlats: 24, + }) + expect( + hoseBarbSchema.parse({ + outerDiameter: "2.0cm", + boreDiameter: "1.0cm", + length: "4.5cm", + barbDiameter: "1.6cm", + barbLength: "2.5cm", + barbPitch: "0.6cm", + hexAcrossFlats: "2.4cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "hosebarb_od20mm_bore10mm_l45mm_barbod16mm_barbl25mm_pitch6mm_af24mm", + ), + ), + ).toEqual({ fn: "hosebarb", ...p }) + expect(registry.parse(parseModelStringParams("hosebarb"))).toEqual({ + fn: "hosebarb", + ...p, + }) + expect(hoseBarb.schema.parse({ fn: "hosebarb", ...p })).toEqual({ + fn: "hosebarb", + ...p, + }) + expect(() => hoseBarbSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => hoseBarbSchema.parse({ ...p, outerDiameter: bad })).toThrow() + for (const text of [ + "hosebarb_od20mm_outerdiameter20mm", + "hosebarb_unknown3mm", + "hosebarb_od2mmjunk", + "hosebarb_od", + "hosebarb2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("bulkheadfitting: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(bulkheadFitting) + const p = bulkheadFittingSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 20, + boreDiameter: 12, + length: 55, + flangeDiameter: 32, + flangeThickness: 4, + nutAcrossFlats: 28, + nutThickness: 8, + panelGap: 10, + }) + expect( + bulkheadFittingSchema.parse({ + outerDiameter: "2.0cm", + boreDiameter: "1.2cm", + length: "5.5cm", + flangeDiameter: "3.2cm", + flangeThickness: "0.4cm", + nutAcrossFlats: "2.8cm", + nutThickness: "0.8cm", + panelGap: "1.0cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "bulkheadfitting_od20mm_bore12mm_l55mm_flange32mm_flanget4mm_af28mm_nutt8mm_gap10mm", + ), + ), + ).toEqual({ fn: "bulkheadfitting", ...p }) + expect(registry.parse(parseModelStringParams("bulkheadfitting"))).toEqual({ + fn: "bulkheadfitting", + ...p, + }) + expect(bulkheadFitting.schema.parse({ fn: "bulkheadfitting", ...p })).toEqual( + { fn: "bulkheadfitting", ...p }, + ) + expect(() => bulkheadFittingSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => + bulkheadFittingSchema.parse({ ...p, outerDiameter: bad }), + ).toThrow() + for (const text of [ + "bulkheadfitting_od20mm_outerdiameter20mm", + "bulkheadfitting_unknown3mm", + "bulkheadfitting_od2mmjunk", + "bulkheadfitting_od", + "bulkheadfitting2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("pushconnectfitting: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(pushConnectFitting) + const p = pushConnectFittingSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 26, + boreDiameter: 8, + length: 40, + tubeDiameter: 12, + colletDiameter: 20, + colletLength: 6, + hexAcrossFlats: 28, + }) + expect( + pushConnectFittingSchema.parse({ + outerDiameter: "2.6cm", + boreDiameter: "0.8cm", + length: "4.0cm", + tubeDiameter: "1.2cm", + colletDiameter: "2.0cm", + colletLength: "0.6cm", + hexAcrossFlats: "2.8cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "pushconnectfitting_od26mm_bore8mm_l40mm_tube12mm_collet20mm_colletl6mm_af28mm", + ), + ), + ).toEqual({ fn: "pushconnectfitting", ...p }) + expect(registry.parse(parseModelStringParams("pushconnectfitting"))).toEqual({ + fn: "pushconnectfitting", + ...p, + }) + expect( + pushConnectFitting.schema.parse({ fn: "pushconnectfitting", ...p }), + ).toEqual({ fn: "pushconnectfitting", ...p }) + expect(() => + pushConnectFittingSchema.parse({ ...p, nonexistent: 1 }), + ).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => + pushConnectFittingSchema.parse({ ...p, outerDiameter: bad }), + ).toThrow() + for (const text of [ + "pushconnectfitting_od26mm_outerdiameter26mm", + "pushconnectfitting_unknown3mm", + "pushconnectfitting_od2mmjunk", + "pushconnectfitting_od", + "pushconnectfitting2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("compressionfitting: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(compressionFitting) + const p = compressionFittingSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 22, + boreDiameter: 8, + length: 40, + tubeDiameter: 12, + nutAcrossFlats: 26, + nutLength: 10, + }) + expect( + compressionFittingSchema.parse({ + outerDiameter: "2.2cm", + boreDiameter: "0.8cm", + length: "4.0cm", + tubeDiameter: "1.2cm", + nutAcrossFlats: "2.6cm", + nutLength: "1.0cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "compressionfitting_od22mm_bore8mm_l40mm_tube12mm_af26mm_nutl10mm", + ), + ), + ).toEqual({ fn: "compressionfitting", ...p }) + expect(registry.parse(parseModelStringParams("compressionfitting"))).toEqual({ + fn: "compressionfitting", + ...p, + }) + expect( + compressionFitting.schema.parse({ fn: "compressionfitting", ...p }), + ).toEqual({ fn: "compressionfitting", ...p }) + expect(() => + compressionFittingSchema.parse({ ...p, nonexistent: 1 }), + ).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => + compressionFittingSchema.parse({ ...p, outerDiameter: bad }), + ).toThrow() + for (const text of [ + "compressionfitting_od22mm_outerdiameter22mm", + "compressionfitting_unknown3mm", + "compressionfitting_od2mmjunk", + "compressionfitting_od", + "compressionfitting2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("hoseclamp: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(hoseClamp) + const p = hoseClampSchema.parse({}) + expect(p).toEqual({ + innerDiameter: 30, + bandThickness: 1.5, + bandWidth: 8, + housingLength: 14, + housingWidth: 8, + housingHeight: 7, + }) + expect( + hoseClampSchema.parse({ + innerDiameter: "3.0cm", + bandThickness: "0.15cm", + bandWidth: "0.8cm", + housingLength: "1.4cm", + housingWidth: "0.8cm", + housingHeight: "0.7cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "hoseclamp_id30mm_bandt1.5mm_bandw8mm_housingl14mm_housingw8mm_housingh7mm", + ), + ), + ).toEqual({ fn: "hoseclamp", ...p }) + expect(registry.parse(parseModelStringParams("hoseclamp"))).toEqual({ + fn: "hoseclamp", + ...p, + }) + expect(hoseClamp.schema.parse({ fn: "hoseclamp", ...p })).toEqual({ + fn: "hoseclamp", + ...p, + }) + expect(() => hoseClampSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => hoseClampSchema.parse({ ...p, innerDiameter: bad })).toThrow() + for (const text of [ + "hoseclamp_id30mm_innerdiameter30mm", + "hoseclamp_unknown3mm", + "hoseclamp_id2mmjunk", + "hoseclamp_id", + "hoseclamp2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("pipeflange: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(pipeFlange) + const p = pipeFlangeSchema.parse({}) + expect(p).toEqual({ + flangeDiameter: 70, + boreDiameter: 16, + hubDiameter: 30, + flangeThickness: 8, + hubLength: 20, + boltCircleDiameter: 50, + boltHoleDiameter: 6, + boltCount: 4, + }) + expect( + pipeFlangeSchema.parse({ + flangeDiameter: "7.0cm", + boreDiameter: "1.6cm", + hubDiameter: "3.0cm", + flangeThickness: "0.8cm", + hubLength: "2.0cm", + boltCircleDiameter: "5.0cm", + boltHoleDiameter: "0.6cm", + boltCount: 4, + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "pipeflange_flange70mm_bore16mm_hub30mm_flanget8mm_hubl20mm_bc50mm_hole6mm_bolts4", + ), + ), + ).toEqual({ fn: "pipeflange", ...p }) + expect(registry.parse(parseModelStringParams("pipeflange"))).toEqual({ + fn: "pipeflange", + ...p, + }) + expect(pipeFlange.schema.parse({ fn: "pipeflange", ...p })).toEqual({ + fn: "pipeflange", + ...p, + }) + expect(() => pipeFlangeSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => + pipeFlangeSchema.parse({ ...p, flangeDiameter: bad }), + ).toThrow() + for (const text of [ + "pipeflange_flange70mm_flangediameter70mm", + "pipeflange_unknown3mm", + "pipeflange_flange2mmjunk", + "pipeflange_flange", + "pipeflange2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("ballvalve: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(ballValve) + const p = ballValveSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 30, + boreDiameter: 16, + length: 65, + endDiameter: 24, + handleLength: 55, + handleWidth: 10, + stemHeight: 14, + handleThickness: 4, + }) + expect( + ballValveSchema.parse({ + outerDiameter: "3.0cm", + boreDiameter: "1.6cm", + length: "6.5cm", + endDiameter: "2.4cm", + handleLength: "5.5cm", + handleWidth: "1.0cm", + stemHeight: "1.4cm", + handleThickness: "0.4cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "ballvalve_od30mm_bore16mm_l65mm_endod24mm_handlel55mm_handlew10mm_stemh14mm_handlet4mm", + ), + ), + ).toEqual({ fn: "ballvalve", ...p }) + expect(registry.parse(parseModelStringParams("ballvalve"))).toEqual({ + fn: "ballvalve", + ...p, + }) + expect(ballValve.schema.parse({ fn: "ballvalve", ...p })).toEqual({ + fn: "ballvalve", + ...p, + }) + expect(() => ballValveSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => ballValveSchema.parse({ ...p, outerDiameter: bad })).toThrow() + for (const text of [ + "ballvalve_od30mm_outerdiameter30mm", + "ballvalve_unknown3mm", + "ballvalve_od2mmjunk", + "ballvalve_od", + "ballvalve2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("checkvalve: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(checkValve) + const p = checkValveSchema.parse({}) + expect(p).toEqual({ + outerDiameter: 28, + boreDiameter: 12, + length: 60, + endDiameter: 20, + hexAcrossFlats: 30, + nutLength: 12, + }) + expect( + checkValveSchema.parse({ + outerDiameter: "2.8cm", + boreDiameter: "1.2cm", + length: "6.0cm", + endDiameter: "2.0cm", + hexAcrossFlats: "3.0cm", + nutLength: "1.2cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "checkvalve_od28mm_bore12mm_l60mm_endod20mm_af30mm_nutl12mm", + ), + ), + ).toEqual({ fn: "checkvalve", ...p }) + expect(registry.parse(parseModelStringParams("checkvalve"))).toEqual({ + fn: "checkvalve", + ...p, + }) + expect(checkValve.schema.parse({ fn: "checkvalve", ...p })).toEqual({ + fn: "checkvalve", + ...p, + }) + expect(() => checkValveSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => checkValveSchema.parse({ ...p, outerDiameter: bad })).toThrow() + for (const text of [ + "checkvalve_od28mm_outerdiameter28mm", + "checkvalve_unknown3mm", + "checkvalve_od2mmjunk", + "checkvalve_od", + "checkvalve2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("needlevalve: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(needleValve) + const p = needleValveSchema.parse({}) + expect(p).toEqual({ + bodyLength: 38, + bodyWidth: 24, + bodyHeight: 26, + boreDiameter: 8, + portDiameter: 16, + portLength: 10, + stemHeight: 24, + wheelDiameter: 26, + wheelThickness: 5, + }) + expect( + needleValveSchema.parse({ + bodyLength: "3.8cm", + bodyWidth: "2.4cm", + bodyHeight: "2.6cm", + boreDiameter: "0.8cm", + portDiameter: "1.6cm", + portLength: "1.0cm", + stemHeight: "2.4cm", + wheelDiameter: "2.6cm", + wheelThickness: "0.5cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "needlevalve_bodyl38mm_bodyw24mm_bodyh26mm_bore8mm_port16mm_portl10mm_stemh24mm_wheel26mm_wheelt5mm", + ), + ), + ).toEqual({ fn: "needlevalve", ...p }) + expect(registry.parse(parseModelStringParams("needlevalve"))).toEqual({ + fn: "needlevalve", + ...p, + }) + expect(needleValve.schema.parse({ fn: "needlevalve", ...p })).toEqual({ + fn: "needlevalve", + ...p, + }) + expect(() => needleValveSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => needleValveSchema.parse({ ...p, bodyLength: bad })).toThrow() + for (const text of [ + "needlevalve_bodyl38mm_bodylength38mm", + "needlevalve_unknown3mm", + "needlevalve_bodyl2mmjunk", + "needlevalve_bodyl", + "needlevalve2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("manifold: complete unit contract, registry routing and strict validation", () => { + const registry = new ModelRegistry() + registry.register(manifold) + const p = manifoldSchema.parse({}) + expect(p).toEqual({ + length: 100, + width: 30, + height: 26, + boreDiameter: 12, + portDiameter: 8, + portPitch: 25, + portCount: 3, + mountingHoleDiameter: 5, + }) + expect( + manifoldSchema.parse({ + length: "10.0cm", + width: "3.0cm", + height: "2.6cm", + boreDiameter: "1.2cm", + portDiameter: "0.8cm", + portPitch: "2.5cm", + portCount: 3, + mountingHoleDiameter: "0.5cm", + }), + ).toEqual(p) + expect( + registry.parse( + parseModelStringParams( + "manifold_l100mm_w30mm_h26mm_bore12mm_port8mm_pitch25mm_ports3_hole5mm", + ), + ), + ).toEqual({ fn: "manifold", ...p }) + expect(registry.parse(parseModelStringParams("manifold"))).toEqual({ + fn: "manifold", + ...p, + }) + expect(manifold.schema.parse({ fn: "manifold", ...p })).toEqual({ + fn: "manifold", + ...p, + }) + expect(() => manifoldSchema.parse({ ...p, nonexistent: 1 })).toThrow() + for (const bad of [0, -1, NaN, Infinity, "2mmjunk", "1e2mm"]) + expect(() => manifoldSchema.parse({ ...p, length: bad })).toThrow() + for (const text of [ + "manifold_l100mm_length100mm", + "manifold_unknown3mm", + "manifold_l2mmjunk", + "manifold_l", + "manifold2", + ]) + expect(() => registry.parse(parseModelStringParams(text))).toThrow() +}) + +test("rigidpipe: impossible mating dimensions fail", () => { + expect(() => rigidPipeSchema.parse({ boreDiameter: 20 })).toThrow() +}) + +test("pipeelbow: impossible mating dimensions fail", () => { + expect(() => pipeElbowSchema.parse({ socketLength: 40 })).toThrow() +}) + +test("pipetee: impossible mating dimensions fail", () => { + expect(() => pipeTeeSchema.parse({ branchLength: 20 })).toThrow() +}) + +test("pipecross: impossible mating dimensions fail", () => { + expect(() => pipeCrossSchema.parse({ span: 40 })).toThrow() +}) + +test("pipecoupling: impossible mating dimensions fail", () => { + expect(() => pipeCouplingSchema.parse({ socketDepth: 21 })).toThrow() +}) + +test("pipeunion: impossible mating dimensions fail", () => { + expect(() => pipeUnionSchema.parse({ nutAcrossFlats: 29 })).toThrow() +}) + +test("pipenipple: impossible mating dimensions fail", () => { + expect(() => pipeNippleSchema.parse({ threadLength: 25 })).toThrow() +}) + +test("pipecap: impossible mating dimensions fail", () => { + expect(() => pipeCapSchema.parse({ floorThickness: 25 })).toThrow() +}) + +test("pipeplug: impossible mating dimensions fail", () => { + expect(() => pipePlugSchema.parse({ headAcrossFlats: 19 })).toThrow() +}) + +test("reducingbushing: impossible mating dimensions fail", () => { + expect(() => reducingBushingSchema.parse({ smallBoreDiameter: 19 })).toThrow() +}) + +test("hosebarb: impossible mating dimensions fail", () => { + expect(() => hoseBarbSchema.parse({ barbPitch: 0.5 })).toThrow() +}) + +test("bulkheadfitting: impossible mating dimensions fail", () => { + expect(() => bulkheadFittingSchema.parse({ panelGap: 50 })).toThrow() +}) + +test("pushconnectfitting: impossible mating dimensions fail", () => { + expect(() => pushConnectFittingSchema.parse({ tubeDiameter: 22 })).toThrow() +}) + +test("compressionfitting: impossible mating dimensions fail", () => { + expect(() => compressionFittingSchema.parse({ nutLength: 25 })).toThrow() +}) + +test("hoseclamp: impossible mating dimensions fail", () => { + expect(() => hoseClampSchema.parse({ bandThickness: 8 })).toThrow() +}) + +test("pipeflange: impossible mating dimensions fail", () => { + expect(() => pipeFlangeSchema.parse({ boltCircleDiameter: 69 })).toThrow() +}) + +test("ballvalve: impossible mating dimensions fail", () => { + expect(() => ballValveSchema.parse({ endDiameter: 15 })).toThrow() +}) + +test("checkvalve: impossible mating dimensions fail", () => { + expect(() => checkValveSchema.parse({ nutLength: 31 })).toThrow() +}) + +test("needlevalve: impossible mating dimensions fail", () => { + expect(() => needleValveSchema.parse({ portDiameter: 30 })).toThrow() +}) + +test("manifold: impossible mating dimensions fail", () => { + expect(() => manifoldSchema.parse({ portCount: 8 })).toThrow() +}) + +test("pipeflange: counts require bounded integers without length units", () => { + for (const value of [1, 2.5, 13, "4mm"]) + expect(() => pipeFlangeSchema.parse({ boltCount: value })).toThrow() + expect(() => + pipeFlange.parse(parseModelStringParams("pipeflange_bolts4mm")), + ).toThrow() +}) + +test("manifold: counts require bounded integers without length units", () => { + for (const value of [1, 2.5, 13, "4mm"]) + expect(() => manifoldSchema.parse({ portCount: value })).toThrow() + expect(() => + manifold.parse(parseModelStringParams("manifold_ports4mm")), + ).toThrow() +}) + +test("rigidpipe: subresolution walls or mating gaps fail before mesh generation", () => { + expect(() => rigidPipeSchema.parse({ boreDiameter: 19.99999 })).toThrow( + "resolution", + ) +}) + +test("pipeelbow: subresolution walls or mating gaps fail before mesh generation", () => { + expect(() => pipeElbowSchema.parse({ boreDiameter: 23.99999 })).toThrow( + "resolution", + ) +}) + +test("pipecoupling: subresolution walls or mating gaps fail before mesh generation", () => { + expect(() => pipeCouplingSchema.parse({ socketDiameter: 29.99999 })).toThrow( + "resolution", + ) +}) + +test("reducingbushing: subresolution walls or mating gaps fail before mesh generation", () => { + expect(() => + reducingBushingSchema.parse({ smallBoreDiameter: 17.99999 }), + ).toThrow("resolution") +}) + +test("hosebarb: subresolution walls or mating gaps fail before mesh generation", () => { + expect(() => hoseBarbSchema.parse({ boreDiameter: 12.79999 })).toThrow( + "resolution", + ) +}) + +test("pipeflange: subresolution walls or mating gaps fail before mesh generation", () => { + expect(() => + pipeFlangeSchema.parse({ boltCircleDiameter: 63.99999 }), + ).toThrow("resolution") +}) + +test("manifold: subresolution walls or mating gaps fail before mesh generation", () => { + expect(() => manifoldSchema.parse({ width: 27.00001 })).toThrow("resolution") +}) diff --git a/tests/machine-hardware-contracts.test.ts b/tests/machine-hardware-contracts.test.ts new file mode 100644 index 0000000..7694de9 --- /dev/null +++ b/tests/machine-hardware-contracts.test.ts @@ -0,0 +1,73 @@ +import { expect, test } from "bun:test" +import { parseModelStringParams } from "../src/parse-model-string" +import { ModelRegistry } from "../src/model-registry" +import { machineHardwareContracts } from "./fixtures/machine-hardware-models" + +for (const item of machineHardwareContracts) { + test(`${item.name}: defaults, units, registration and roundtrip`, () => { + const defaults = item.defaults as unknown as Record + expect(item.schema.parse({})).toEqual(defaults) + const registry = new ModelRegistry() + item.register(registry) + expect(registry.getModelNames()).toEqual([item.name]) + expect( + registry.parse(parseModelStringParams(item.source)), + ).toEqual({ fn: item.name, ...defaults }) + const unitInput = Object.fromEntries( + Object.entries(defaults).map(([key, value]) => [ + key, + ["spokes", "lobes", "bladeCount"].includes(key) ? value : `${value}mm`, + ]), + ) + expect(item.schema.parse(unitInput)).toEqual(defaults) + const inches = defaults[item.first]! / 25.4 + const normalized = item.schema.parse({ + ...defaults, + [item.first]: `${inches}in`, + }) as unknown as Record + expect(normalized[item.first]).toBeCloseTo( + defaults[item.first]!, + 9, + ) + const definition = registry.parse(parseModelStringParams(item.source)) + expect(item.definition.parse(definition)).toEqual(definition) + expect(item.parse(parseModelStringParams(item.name))).toEqual( + definition, + ) + }) + test(`${item.name}: strict decimal, unknown, duplicate and impossible geometry validation`, () => { + for (const value of [0, -1, Infinity, NaN, "1e3mm", "12oops", "0.001mm"]) + expect(() => + item.schema.parse({ [item.first]: value }), + ).toThrow() + for (const input of [{ typo: 1 }, item.invalid]) + expect(() => item.schema.parse(input)).toThrow() + expect(() => item.definition.parse({ fn: "unknown" })).toThrow() + const value = (item.defaults as unknown as Record)[ + item.first + ] + for (const source of [ + `${item.name}_typo1mm`, + `${item.name}_${item.first.toLowerCase()}${value}mm_${item.first.toLowerCase()}${value}mm`, + `${item.name}_${item.first.toLowerCase()}(1mm)`, + `${item.name}12`, + ]) + expect(() => + item.parse(parseModelStringParams(source)), + ).toThrow() + for (const [key, value] of Object.entries(item.defaults)) { + if (["spokes", "lobes", "bladeCount"].includes(key)) { + expect(() => item.schema.parse({ [key]: 2 })).toThrow() + expect(() => + item.schema.parse({ [key]: value + 0.5 }), + ).toThrow() + expect(() => item.schema.parse({ [key]: 30 })).toThrow() + expect(() => + item.parse( + parseModelStringParams(`${item.name}_${key.toLowerCase()}6mm`), + ), + ).toThrow() + } + } + }) +} diff --git a/tests/motion-contracts.test.ts b/tests/motion-contracts.test.ts new file mode 100644 index 0000000..9bf6af2 --- /dev/null +++ b/tests/motion-contracts.test.ts @@ -0,0 +1,996 @@ +import { expect, test } from "bun:test" +import { + mp, + modelDefinitionSchema, + pillowBlockBearingModelPropsSchema, + flangeBearingModelPropsSchema, + needleBearingModelPropsSchema, + cylindricalRollerBearingModelPropsSchema, + camFollowerModelPropsSchema, + vGrooveWheelModelPropsSchema, + rodEndModelPropsSchema, + sphericalBearingModelPropsSchema, + clevisModelPropsSchema, + gearRackModelPropsSchema, + sprocketModelPropsSchema, + rollerChainModelPropsSchema, + vBeltSheaveModelPropsSchema, + conveyorRollerModelPropsSchema, + shaftSupportModelPropsSchema, + getPillowBlockBearingDimensions, + getFlangeBearingDimensions, + getNeedleBearingDimensions, + getCylindricalRollerBearingDimensions, + getCamFollowerDimensions, + getVGrooveWheelDimensions, + getRodEndDimensions, + getSphericalBearingDimensions, + getClevisDimensions, + getGearRackDimensions, + getSprocketDimensions, + getRollerChainDimensions, + getVBeltSheaveDimensions, + getConveyorRollerDimensions, + getShaftSupportDimensions, +} from "../src" + +test("pillowblockbearing: defaults, model strings, units, schema and dimensions", () => { + const source = + "pillowblockbearing_bore20mm_bw80mm_bd30mm_bt8mm_hd46mm_ch30mm_ms60mm_mh6mm" + const expected = { + fn: "pillowblockbearing", + boreDiameter: 20, + baseWidth: 80, + baseDepth: 30, + baseThickness: 8, + housingDiameter: 46, + centerHeight: 30, + mountSpacing: 60, + mountHoleDiameter: 6, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("pillowblockbearing").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(pillowBlockBearingModelPropsSchema.parse(p)).toEqual(p) + expect( + pillowBlockBearingModelPropsSchema.parse({ ...p, boreDiameter: "2cm" }), + ).toEqual(p) + expect(mp.string(source.replace("bore20mm", "bore2cm")).json()).toEqual( + expected, + ) + const dimensions = getPillowBlockBearingDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getPillowBlockBearingDimensions()).toEqual(dimensions) +}) +test("pillowblockbearing: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = + "pillowblockbearing_bore20mm_bw80mm_bd30mm_bt8mm_hd46mm_ch30mm_ms60mm_mh6mm" + for (const suffix of [ + "", + "typo1mm", + "constructor1", + "bore1mm", + "borediameter1mm", + ]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + boreDiameter: 20, + baseWidth: 80, + baseDepth: 30, + baseThickness: 8, + housingDiameter: 46, + centerHeight: 30, + mountSpacing: 60, + mountHoleDiameter: 6, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + pillowBlockBearingModelPropsSchema.parse({ ...p, boreDiameter: value }), + ).toThrow() + expect(() => + pillowBlockBearingModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [ + { housingDiameter: 25 }, + { centerHeight: 15 }, + { mountSpacing: 80 }, + { mountSpacing: 30 }, + { baseDepth: 5 }, + ]) + expect(() => + pillowBlockBearingModelPropsSchema.parse({ ...p, ...patch }), + ).toThrow() +}) + +test("flangebearing: defaults, model strings, units, schema and dimensions", () => { + const source = + "flangebearing_bore20mm_fw64mm_ft6mm_hd44mm_hh20mm_ms46mm_mh6mm" + const expected = { + fn: "flangebearing", + boreDiameter: 20, + flangeWidth: 64, + flangeThickness: 6, + housingDiameter: 44, + housingHeight: 20, + mountSpacing: 46, + mountHoleDiameter: 6, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("flangebearing").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(flangeBearingModelPropsSchema.parse(p)).toEqual(p) + expect( + flangeBearingModelPropsSchema.parse({ ...p, boreDiameter: "2cm" }), + ).toEqual(p) + expect(mp.string(source.replace("bore20mm", "bore2cm")).json()).toEqual( + expected, + ) + const dimensions = getFlangeBearingDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getFlangeBearingDimensions()).toEqual(dimensions) +}) +test("flangebearing: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = + "flangebearing_bore20mm_fw64mm_ft6mm_hd44mm_hh20mm_ms46mm_mh6mm" + for (const suffix of [ + "", + "typo1mm", + "constructor1", + "bore1mm", + "borediameter1mm", + ]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + boreDiameter: 20, + flangeWidth: 64, + flangeThickness: 6, + housingDiameter: 44, + housingHeight: 20, + mountSpacing: 46, + mountHoleDiameter: 6, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + flangeBearingModelPropsSchema.parse({ ...p, boreDiameter: value }), + ).toThrow() + expect(() => + flangeBearingModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [ + { housingDiameter: 25 }, + { mountSpacing: 60 }, + { mountSpacing: 20 }, + { housingHeight: 3 }, + ]) + expect(() => + flangeBearingModelPropsSchema.parse({ ...p, ...patch }), + ).toThrow() +}) + +test("needlebearing: defaults, model strings, units, schema and dimensions", () => { + const source = "needlebearing_id20mm_od30mm_w20mm_race1.5mm_rollers18" + const expected = { + fn: "needlebearing", + innerDiameter: 20, + outerDiameter: 30, + width: 20, + raceThickness: 1.5, + rollerCount: 18, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("needlebearing").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(needleBearingModelPropsSchema.parse(p)).toEqual(p) + expect( + needleBearingModelPropsSchema.parse({ ...p, innerDiameter: "2cm" }), + ).toEqual(p) + expect(mp.string(source.replace("id20mm", "id2cm")).json()).toEqual(expected) + const dimensions = getNeedleBearingDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getNeedleBearingDimensions()).toEqual(dimensions) +}) +test("needlebearing: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = "needlebearing_id20mm_od30mm_w20mm_race1.5mm_rollers18" + for (const suffix of [ + "", + "typo1mm", + "constructor1", + "id1mm", + "innerdiameter1mm", + ]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + innerDiameter: 20, + outerDiameter: 30, + width: 20, + raceThickness: 1.5, + rollerCount: 18, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + needleBearingModelPropsSchema.parse({ ...p, innerDiameter: value }), + ).toThrow() + expect(() => + needleBearingModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [ + { outerDiameter: 25 }, + { width: 2 }, + { rollerCount: 64 }, + ]) + expect(() => + needleBearingModelPropsSchema.parse({ ...p, ...patch }), + ).toThrow() + for (const value of [1, 65, 2.5, Infinity, "3mm"]) + expect(() => + needleBearingModelPropsSchema.parse({ ...p, rollerCount: value }), + ).toThrow() + expect(() => + mp.string(source.replace("rollers18", "rollers18mm")).json(), + ).toThrow() +}) + +test("cylindricalrollerbearing: defaults, model strings, units, schema and dimensions", () => { + const source = + "cylindricalrollerbearing_id20mm_od42mm_w16mm_race3mm_rollers12" + const expected = { + fn: "cylindricalrollerbearing", + innerDiameter: 20, + outerDiameter: 42, + width: 16, + raceThickness: 3, + rollerCount: 12, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("cylindricalrollerbearing").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(cylindricalRollerBearingModelPropsSchema.parse(p)).toEqual(p) + expect( + cylindricalRollerBearingModelPropsSchema.parse({ + ...p, + innerDiameter: "2cm", + }), + ).toEqual(p) + expect(mp.string(source.replace("id20mm", "id2cm")).json()).toEqual(expected) + const dimensions = getCylindricalRollerBearingDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getCylindricalRollerBearingDimensions()).toEqual(dimensions) +}) +test("cylindricalrollerbearing: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = + "cylindricalrollerbearing_id20mm_od42mm_w16mm_race3mm_rollers12" + for (const suffix of [ + "", + "typo1mm", + "constructor1", + "id1mm", + "innerdiameter1mm", + ]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + innerDiameter: 20, + outerDiameter: 42, + width: 16, + raceThickness: 3, + rollerCount: 12, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + cylindricalRollerBearingModelPropsSchema.parse({ + ...p, + innerDiameter: value, + }), + ).toThrow() + expect(() => + cylindricalRollerBearingModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [ + { outerDiameter: 30 }, + { width: 5 }, + { rollerCount: 64 }, + ]) + expect(() => + cylindricalRollerBearingModelPropsSchema.parse({ ...p, ...patch }), + ).toThrow() + for (const value of [1, 65, 2.5, Infinity, "3mm"]) + expect(() => + cylindricalRollerBearingModelPropsSchema.parse({ + ...p, + rollerCount: value, + }), + ).toThrow() + expect(() => + mp.string(source.replace("rollers12", "rollers12mm")).json(), + ).toThrow() +}) + +test("camfollower: defaults, model strings, units, schema and dimensions", () => { + const source = "camfollower_od32mm_w14mm_stud10mm_sl26mm_socket5mm_rollers10" + const expected = { + fn: "camfollower", + wheelDiameter: 32, + wheelWidth: 14, + studDiameter: 10, + studLength: 26, + socketDiameter: 5, + rollerCount: 10, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("camfollower").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(camFollowerModelPropsSchema.parse(p)).toEqual(p) + expect( + camFollowerModelPropsSchema.parse({ ...p, wheelDiameter: "3.2cm" }), + ).toEqual(p) + expect(mp.string(source.replace("od32mm", "od3.2cm")).json()).toEqual( + expected, + ) + const dimensions = getCamFollowerDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getCamFollowerDimensions()).toEqual(dimensions) +}) +test("camfollower: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = "camfollower_od32mm_w14mm_stud10mm_sl26mm_socket5mm_rollers10" + for (const suffix of [ + "", + "typo1mm", + "constructor1", + "od1mm", + "wheeldiameter1mm", + ]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + wheelDiameter: 32, + wheelWidth: 14, + studDiameter: 10, + studLength: 26, + socketDiameter: 5, + rollerCount: 10, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + camFollowerModelPropsSchema.parse({ ...p, wheelDiameter: value }), + ).toThrow() + expect(() => + camFollowerModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [ + { wheelDiameter: 20 }, + { wheelWidth: 4 }, + { socketDiameter: 12 }, + { studLength: 8 }, + ]) + expect(() => + camFollowerModelPropsSchema.parse({ ...p, ...patch }), + ).toThrow() + for (const value of [1, 65, 2.5, Infinity, "3mm"]) + expect(() => + camFollowerModelPropsSchema.parse({ ...p, rollerCount: value }), + ).toThrow() + expect(() => + mp.string(source.replace("rollers10", "rollers10mm")).json(), + ).toThrow() +}) + +test("vgroovewheel: defaults, model strings, units, schema and dimensions", () => { + const source = "vgroovewheel_od40mm_root28mm_bore8mm_w12mm" + const expected = { + fn: "vgroovewheel", + outerDiameter: 40, + rootDiameter: 28, + boreDiameter: 8, + width: 12, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("vgroovewheel").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(vGrooveWheelModelPropsSchema.parse(p)).toEqual(p) + expect( + vGrooveWheelModelPropsSchema.parse({ ...p, outerDiameter: "4cm" }), + ).toEqual(p) + expect(mp.string(source.replace("od40mm", "od4cm")).json()).toEqual(expected) + const dimensions = getVGrooveWheelDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getVGrooveWheelDimensions()).toEqual(dimensions) +}) +test("vgroovewheel: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = "vgroovewheel_od40mm_root28mm_bore8mm_w12mm" + for (const suffix of [ + "", + "typo1mm", + "constructor1", + "od1mm", + "outerdiameter1mm", + ]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { outerDiameter: 40, rootDiameter: 28, boreDiameter: 8, width: 12 } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + vGrooveWheelModelPropsSchema.parse({ ...p, outerDiameter: value }), + ).toThrow() + expect(() => + vGrooveWheelModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [{ rootDiameter: 8 }, { outerDiameter: 28 }]) + expect(() => + vGrooveWheelModelPropsSchema.parse({ ...p, ...patch }), + ).toThrow() +}) + +test("rodend: defaults, model strings, units, schema and dimensions", () => { + const source = "rodend_hd28mm_bore12mm_w12mm_stem10mm_sl30mm" + const expected = { + fn: "rodend", + headDiameter: 28, + boreDiameter: 12, + headWidth: 12, + stemDiameter: 10, + stemLength: 30, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("rodend").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(rodEndModelPropsSchema.parse(p)).toEqual(p) + expect(rodEndModelPropsSchema.parse({ ...p, headDiameter: "2.8cm" })).toEqual( + p, + ) + expect(mp.string(source.replace("hd28mm", "hd2.8cm")).json()).toEqual( + expected, + ) + const dimensions = getRodEndDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getRodEndDimensions()).toEqual(dimensions) +}) +test("rodend: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = "rodend_hd28mm_bore12mm_w12mm_stem10mm_sl30mm" + for (const suffix of [ + "", + "typo1mm", + "constructor1", + "hd1mm", + "headdiameter1mm", + ]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + headDiameter: 28, + boreDiameter: 12, + headWidth: 12, + stemDiameter: 10, + stemLength: 30, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + rodEndModelPropsSchema.parse({ ...p, headDiameter: value }), + ).toThrow() + expect(() => rodEndModelPropsSchema.parse({ ...p, unknown: true })).toThrow() + for (const patch of [ + { headDiameter: 18 }, + { headWidth: 19 }, + { headWidth: 3 }, + { stemDiameter: 13 }, + ]) + expect(() => rodEndModelPropsSchema.parse({ ...p, ...patch })).toThrow() +}) + +test("sphericalbearing: defaults, model strings, units, schema and dimensions", () => { + const source = "sphericalbearing_od30mm_bore12mm_w14mm_ball22mm" + const expected = { + fn: "sphericalbearing", + outerDiameter: 30, + boreDiameter: 12, + width: 14, + ballDiameter: 22, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("sphericalbearing").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(sphericalBearingModelPropsSchema.parse(p)).toEqual(p) + expect( + sphericalBearingModelPropsSchema.parse({ ...p, outerDiameter: "3cm" }), + ).toEqual(p) + expect(mp.string(source.replace("od30mm", "od3cm")).json()).toEqual(expected) + const dimensions = getSphericalBearingDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getSphericalBearingDimensions()).toEqual(dimensions) +}) +test("sphericalbearing: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = "sphericalbearing_od30mm_bore12mm_w14mm_ball22mm" + for (const suffix of [ + "", + "typo1mm", + "constructor1", + "od1mm", + "outerdiameter1mm", + ]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { outerDiameter: 30, boreDiameter: 12, width: 14, ballDiameter: 22 } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + sphericalBearingModelPropsSchema.parse({ ...p, outerDiameter: value }), + ).toThrow() + expect(() => + sphericalBearingModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [ + { outerDiameter: 20 }, + { ballDiameter: 10 }, + { width: 22 }, + { width: 2 }, + { width: 20 }, + ]) + expect(() => + sphericalBearingModelPropsSchema.parse({ ...p, ...patch }), + ).toThrow() +}) + +test("clevis: defaults, model strings, units, schema and dimensions", () => { + const source = "clevis_w30mm_l45mm_h20mm_gap14mm_back10mm_pin8mm_mount10mm" + const expected = { + fn: "clevis", + width: 30, + length: 45, + height: 20, + gap: 14, + backThickness: 10, + pinDiameter: 8, + mountDiameter: 10, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("clevis").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(clevisModelPropsSchema.parse(p)).toEqual(p) + expect(clevisModelPropsSchema.parse({ ...p, width: "3cm" })).toEqual(p) + expect(mp.string(source.replace("w30mm", "w3cm")).json()).toEqual(expected) + const dimensions = getClevisDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getClevisDimensions()).toEqual(dimensions) +}) +test("clevis: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = "clevis_w30mm_l45mm_h20mm_gap14mm_back10mm_pin8mm_mount10mm" + for (const suffix of ["", "typo1mm", "constructor1", "w1mm", "width1mm"]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + width: 30, + length: 45, + height: 20, + gap: 14, + backThickness: 10, + pinDiameter: 8, + mountDiameter: 10, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => clevisModelPropsSchema.parse({ ...p, width: value })).toThrow() + expect(() => clevisModelPropsSchema.parse({ ...p, unknown: true })).toThrow() + for (const patch of [ + { gap: 30 }, + { pinDiameter: 20 }, + { mountDiameter: 21 }, + { length: 25 }, + ]) + expect(() => clevisModelPropsSchema.parse({ ...p, ...patch })).toThrow() +}) + +test("gearrack: defaults, model strings, units, schema and dimensions", () => { + const source = "gearrack_pitch8mm_teeth12_w12mm_h8mm_th4mm_mh3.5mm" + const expected = { + fn: "gearrack", + pitch: 8, + toothCount: 12, + width: 12, + baseHeight: 8, + toothHeight: 4, + mountHoleDiameter: 3.5, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("gearrack").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(gearRackModelPropsSchema.parse(p)).toEqual(p) + expect(gearRackModelPropsSchema.parse({ ...p, pitch: "0.8cm" })).toEqual(p) + expect(mp.string(source.replace("pitch8mm", "pitch0.8cm")).json()).toEqual( + expected, + ) + const dimensions = getGearRackDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getGearRackDimensions()).toEqual(dimensions) +}) +test("gearrack: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = "gearrack_pitch8mm_teeth12_w12mm_h8mm_th4mm_mh3.5mm" + for (const suffix of ["", "typo1mm", "constructor1", "pitch1mm", "pitch1mm"]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + pitch: 8, + toothCount: 12, + width: 12, + baseHeight: 8, + toothHeight: 4, + mountHoleDiameter: 3.5, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + gearRackModelPropsSchema.parse({ ...p, pitch: value }), + ).toThrow() + expect(() => + gearRackModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [ + { mountHoleDiameter: 10 }, + { toothHeight: 9 }, + { toothCount: 2 }, + ]) + expect(() => gearRackModelPropsSchema.parse({ ...p, ...patch })).toThrow() + for (const value of [1, 65, 2.5, Infinity, "3mm"]) + expect(() => + gearRackModelPropsSchema.parse({ ...p, toothCount: value }), + ).toThrow() + expect(() => + mp.string(source.replace("teeth12", "teeth12mm")).json(), + ).toThrow() +}) + +test("sprocket: defaults, model strings, units, schema and dimensions", () => { + const source = "sprocket_od60mm_root48mm_w8mm_bore12mm_teeth16_hub26mm_hh10mm" + const expected = { + fn: "sprocket", + outerDiameter: 60, + rootDiameter: 48, + width: 8, + boreDiameter: 12, + toothCount: 16, + hubDiameter: 26, + hubHeight: 10, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("sprocket").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(sprocketModelPropsSchema.parse(p)).toEqual(p) + expect( + sprocketModelPropsSchema.parse({ ...p, outerDiameter: "6cm" }), + ).toEqual(p) + expect(mp.string(source.replace("od60mm", "od6cm")).json()).toEqual(expected) + const dimensions = getSprocketDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getSprocketDimensions()).toEqual(dimensions) +}) +test("sprocket: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = "sprocket_od60mm_root48mm_w8mm_bore12mm_teeth16_hub26mm_hh10mm" + for (const suffix of [ + "", + "typo1mm", + "constructor1", + "od1mm", + "outerdiameter1mm", + ]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + outerDiameter: 60, + rootDiameter: 48, + width: 8, + boreDiameter: 12, + toothCount: 16, + hubDiameter: 26, + hubHeight: 10, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + sprocketModelPropsSchema.parse({ ...p, outerDiameter: value }), + ).toThrow() + expect(() => + sprocketModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [ + { outerDiameter: 40 }, + { rootDiameter: 24 }, + { hubDiameter: 10 }, + { rootDiameter: 30 }, + { toothCount: 64 }, + ]) + expect(() => sprocketModelPropsSchema.parse({ ...p, ...patch })).toThrow() + for (const value of [1, 65, 2.5, Infinity, "3mm"]) + expect(() => + sprocketModelPropsSchema.parse({ ...p, toothCount: value }), + ).toThrow() + expect(() => + mp.string(source.replace("teeth16", "teeth16mm")).json(), + ).toThrow() +}) + +test("rollerchain: defaults, model strings, units, schema and dimensions", () => { + const source = + "rollerchain_pitch12.7mm_links6_roller7.9mm_w8mm_pin4mm_plate1.2mm" + const expected = { + fn: "rollerchain", + pitch: 12.7, + linkCount: 6, + rollerDiameter: 7.9, + width: 8, + pinDiameter: 4, + plateThickness: 1.2, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("rollerchain").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(rollerChainModelPropsSchema.parse(p)).toEqual(p) + expect(rollerChainModelPropsSchema.parse({ ...p, pitch: "1.27cm" })).toEqual( + p, + ) + expect( + mp.string(source.replace("pitch12.7mm", "pitch1.27cm")).json(), + ).toEqual(expected) + const dimensions = getRollerChainDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getRollerChainDimensions()).toEqual(dimensions) +}) +test("rollerchain: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = + "rollerchain_pitch12.7mm_links6_roller7.9mm_w8mm_pin4mm_plate1.2mm" + for (const suffix of ["", "typo1mm", "constructor1", "pitch1mm", "pitch1mm"]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + pitch: 12.7, + linkCount: 6, + rollerDiameter: 7.9, + width: 8, + pinDiameter: 4, + plateThickness: 1.2, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + rollerChainModelPropsSchema.parse({ ...p, pitch: value }), + ).toThrow() + expect(() => + rollerChainModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [ + { pitch: 8 }, + { rollerDiameter: 3 }, + { width: 5 }, + { pinDiameter: 1 }, + ]) + expect(() => + rollerChainModelPropsSchema.parse({ ...p, ...patch }), + ).toThrow() + for (const value of [1, 65, 2.5, Infinity, "3mm"]) + expect(() => + rollerChainModelPropsSchema.parse({ ...p, linkCount: value }), + ).toThrow() + expect(() => mp.string(source.replace("links6", "links6mm")).json()).toThrow() +}) + +test("vbeltsheave: defaults, model strings, units, schema and dimensions", () => { + const source = + "vbeltsheave_od60mm_bore12mm_w24mm_grooves2_depth8mm_gw9mm_hub26mm_hh8mm" + const expected = { + fn: "vbeltsheave", + outerDiameter: 60, + boreDiameter: 12, + width: 24, + grooveCount: 2, + grooveDepth: 8, + grooveWidth: 9, + hubDiameter: 26, + hubHeight: 8, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("vbeltsheave").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(vBeltSheaveModelPropsSchema.parse(p)).toEqual(p) + expect( + vBeltSheaveModelPropsSchema.parse({ ...p, outerDiameter: "6cm" }), + ).toEqual(p) + expect(mp.string(source.replace("od60mm", "od6cm")).json()).toEqual(expected) + const dimensions = getVBeltSheaveDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getVBeltSheaveDimensions()).toEqual(dimensions) +}) +test("vbeltsheave: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = + "vbeltsheave_od60mm_bore12mm_w24mm_grooves2_depth8mm_gw9mm_hub26mm_hh8mm" + for (const suffix of [ + "", + "typo1mm", + "constructor1", + "od1mm", + "outerdiameter1mm", + ]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + outerDiameter: 60, + boreDiameter: 12, + width: 24, + grooveCount: 2, + grooveDepth: 8, + grooveWidth: 9, + hubDiameter: 26, + hubHeight: 8, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + vBeltSheaveModelPropsSchema.parse({ ...p, outerDiameter: value }), + ).toThrow() + expect(() => + vBeltSheaveModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [ + { grooveWidth: 12 }, + { grooveDepth: 20 }, + { hubDiameter: 10 }, + ]) + expect(() => + vBeltSheaveModelPropsSchema.parse({ ...p, ...patch }), + ).toThrow() + for (const value of [1, 65, 2.5, Infinity, "3mm"]) + expect(() => + vBeltSheaveModelPropsSchema.parse({ ...p, grooveCount: value }), + ).toThrow() + expect(() => + mp.string(source.replace("grooves2", "grooves2mm")).json(), + ).toThrow() +}) + +test("conveyorroller: defaults, model strings, units, schema and dimensions", () => { + const source = "conveyorroller_od50mm_l150mm_wall3mm_shaft10mm_ext15mm_end4mm" + const expected = { + fn: "conveyorroller", + outerDiameter: 50, + length: 150, + wallThickness: 3, + shaftDiameter: 10, + shaftExtension: 15, + endThickness: 4, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("conveyorroller").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(conveyorRollerModelPropsSchema.parse(p)).toEqual(p) + expect( + conveyorRollerModelPropsSchema.parse({ ...p, outerDiameter: "5cm" }), + ).toEqual(p) + expect(mp.string(source.replace("od50mm", "od5cm")).json()).toEqual(expected) + const dimensions = getConveyorRollerDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getConveyorRollerDimensions()).toEqual(dimensions) +}) +test("conveyorroller: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = "conveyorroller_od50mm_l150mm_wall3mm_shaft10mm_ext15mm_end4mm" + for (const suffix of [ + "", + "typo1mm", + "constructor1", + "od1mm", + "outerdiameter1mm", + ]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + outerDiameter: 50, + length: 150, + wallThickness: 3, + shaftDiameter: 10, + shaftExtension: 15, + endThickness: 4, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + conveyorRollerModelPropsSchema.parse({ ...p, outerDiameter: value }), + ).toThrow() + expect(() => + conveyorRollerModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [ + { outerDiameter: 15 }, + { length: 6 }, + { wallThickness: 21 }, + ]) + expect(() => + conveyorRollerModelPropsSchema.parse({ ...p, ...patch }), + ).toThrow() +}) + +test("shaftsupport: defaults, model strings, units, schema and dimensions", () => { + const source = + "shaftsupport_bore16mm_bw55mm_bd32mm_bt7mm_hw26mm_hd20mm_hh34mm_ch26mm_ms40mm_mh5mm_slit1.5mm_clamp4mm" + const expected = { + fn: "shaftsupport", + boreDiameter: 16, + baseWidth: 55, + baseDepth: 32, + baseThickness: 7, + headWidth: 26, + headDepth: 20, + headHeight: 34, + centerHeight: 26, + mountSpacing: 40, + mountHoleDiameter: 5, + slitWidth: 1.5, + clampHoleDiameter: 4, + } as const + expect(mp.string(source).json()).toEqual(expected) + expect(mp.string("shaftsupport").json()).toEqual(expected) + expect(mp.string(source.toUpperCase()).json()).toEqual(expected) + expect(modelDefinitionSchema.parse(expected)).toEqual(expected) + const { fn, ...p } = expected + expect(shaftSupportModelPropsSchema.parse(p)).toEqual(p) + expect( + shaftSupportModelPropsSchema.parse({ ...p, boreDiameter: "1.6cm" }), + ).toEqual(p) + expect(mp.string(source.replace("bore16mm", "bore1.6cm")).json()).toEqual( + expected, + ) + const dimensions = getShaftSupportDimensions(p) + expect(dimensions.size.every((v) => Number.isFinite(v) && v > 0)).toBe(true) + expect(getShaftSupportDimensions()).toEqual(dimensions) +}) +test("shaftsupport: strict tokens, duplicates, finite lengths and incompatible relations", () => { + const source = + "shaftsupport_bore16mm_bw55mm_bd32mm_bt7mm_hw26mm_hd20mm_hh34mm_ch26mm_ms40mm_mh5mm_slit1.5mm_clamp4mm" + for (const suffix of [ + "", + "typo1mm", + "constructor1", + "bore1mm", + "borediameter1mm", + ]) + expect(() => mp.string(`${source}_${suffix}`).json()).toThrow() + const p = { + boreDiameter: 16, + baseWidth: 55, + baseDepth: 32, + baseThickness: 7, + headWidth: 26, + headDepth: 20, + headHeight: 34, + centerHeight: 26, + mountSpacing: 40, + mountHoleDiameter: 5, + slitWidth: 1.5, + clampHoleDiameter: 4, + } + for (const value of [0, -1, Infinity, NaN, "12mmjunk", "1e2", "(12mm)"]) + expect(() => + shaftSupportModelPropsSchema.parse({ ...p, boreDiameter: value }), + ).toThrow() + expect(() => + shaftSupportModelPropsSchema.parse({ ...p, unknown: true }), + ).toThrow() + for (const patch of [ + { headWidth: 16 }, + { centerHeight: 15 }, + { headHeight: 25 }, + { mountSpacing: 51 }, + { mountSpacing: 20 }, + { slitWidth: 16 }, + { headDepth: 32 }, + { mountHoleDiameter: 33 }, + { clampHoleDiameter: 21 }, + ]) + expect(() => + shaftSupportModelPropsSchema.parse({ ...p, ...patch }), + ).toThrow() +})