Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions docs/solid-rivet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Solid rivet

`solidrivet` describes an unset, solid cylindrical rivet with a spherical-cap
head and a flat tail (roadmap #13, item 0036). This is a custom nominal geometry,
not a claim of conformance to a fastener standard. It has no hole, mandrel,
threads, chamfers, head fillet, or deformed closing head. Geometry generation
belongs in jscad-electronics.

```ts
import { getSolidRivetDimensions, mp } from "@tscircuit/modelprinter"

mp.string("solidrivet_d3mm_l10mm_headod5.5mm_headh2.2mm_roundhead_flattail_unset").json()

// Fixed construction options are bare boolean flags.
mp.string("solidrivet_custom_d3mm_l10mm_roundhead_headod5.5mm_headh2.2mm_flattail_unset").json()

getSolidRivetDimensions({ diameter: 3, length: 10, headDiameter: 5.5, headHeight: 2.2 })
```

| Property | Default | Tokens | Meaning |
| --- | --- | --- | --- |
| `diameter` | Required | `d`, `diameter` | Solid shank diameter |
| `length` | Required | `l`, `length` | Under-head length, excluding the head |
| `headDiameter` | Required | `headod`, `headdiameter` | Head diameter at the bearing plane |
| `headHeight` | Required | `headh`, `headheight` | Spherical-cap height above the bearing plane |
| `custom` | `true` | `custom` | Explicitly dimensioned custom construction |
| `roundHead` | `true` | `roundhead` | Spherical-cap head |
| `flatTail` | `true` | `flattail` | Flat unpeened tail |
| `unset` | `true` | `unset` | Rivet before installation/deformation |

All four dimensions are required; no fastener size is inferred. Lengths accept
finite numbers in millimeters or complete numeric strings with optional `mm`,
`cm`, `m`, `in`, `inch`, `mil`, `ft`, or `feet` units. Outputs are millimeters.
Token names, unit suffixes, and selector values are case insensitive. Direct
schema property names and selector values use the casing in the table.

The exported strict props and definition schemas reject unknown properties,
nonpositive/nonfinite dimensions, and any alternative head/tail/state. The head
diameter must exceed the shank diameter. The head height must not exceed half
the head diameter: the allowed spherical caps range from a shallow dome through
a hemisphere, without an overhanging bulb. Derived dimensions must remain finite.
Duplicate dimensions, aliases, flags, and equivalent flag/selector combinations
are rejected, even when their values agree.

The local frame is right handed, in millimeters, with the shank concentric with
Z. Its bearing plane is at Z=0, the flat tail at Z=-`length`, and the head tip at
Z=`headHeight`. The shank and head are joined at the bearing plane. X and Y span
the circular cross-section; no angular orientation is preferred.

`getSolidRivetDimensions` returns the four normalized dimensions plus:

| Dimension | Definition |
| --- | --- |
| `overallLength` | `length + headHeight` |
| `headSphereRadius` | `headHeight / 2 + headDiameter² / (8 × headHeight)` |
| `headSphereCenterZ` | `headHeight - headSphereRadius` |
| `shankBottomZ` | `-length` |
| `headTopZ` | `headHeight` |

The spherical cap is the portion of this sphere at Z≥0; its intersection with
the bearing plane is a circle of diameter `headDiameter`. These Z values are
positions in the stated local frame, not directions. Tolerances, load capacity,
material, and installation forces are outside this parameter contract.

`SolidRivetModelPropsInput` accepts unit strings and optional defaults;
`SolidRivetModelProps` contains normalized lengths and resolved flags.
`SolidRivetModelDefinition` adds `fn: "solidrivet"` and participates in the
generated public model-definition union.
1 change: 1 addition & 0 deletions src/models/solidrivet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./schema"
53 changes: 53 additions & 0 deletions src/models/solidrivet/parse-model-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { RawModelprinterParams } from "../../parse-model-string"
import { splitModelStringTokens } from "../../split-model-string-tokens"
import { solidRivetModelDefinitionSchema } from "./schema"

const lengths = {
d: "diameter",
diameter: "diameter",
l: "length",
length: "length",
headod: "headDiameter",
headdiameter: "headDiameter",
headh: "headHeight",
headheight: "headHeight",
} as const

const flags = {
custom: "custom",
roundhead: "roundHead",
flattail: "flatTail",
unset: "unset",
} as const

export function parseSolidRivetModelParams(raw: RawModelprinterParams) {
const tokens = splitModelStringTokens(raw.string)
if (raw.fn !== "solidrivet" || tokens[0]?.toLowerCase() !== "solidrivet")
throw new Error("Expected solidrivet without an inline value")

const props: Record<string, unknown> = { fn: "solidrivet" }
for (const token of tokens.slice(1)) {
const match = token.match(/^([a-z]+)(.*)$/i)
if (!match) throw new Error(`Invalid solid rivet token "${token}"`)
const name = match[1]!.toLowerCase()
const value = match[2]!
let property: string
let parsed: unknown

if (Object.hasOwn(lengths, name)) {
property = lengths[name as keyof typeof lengths]
parsed = value
} else if (Object.hasOwn(flags, name)) {
if (value) throw new Error(`Solid rivet flag "${name}" takes no value`)
property = flags[name as keyof typeof flags]
parsed = true
} else throw new Error(`Unknown solid rivet token "${token}"`)

if (Object.hasOwn(props, property))
throw new Error(
`Solid rivet property "${property}" is set more than once`,
)
props[property] = parsed
}
return solidRivetModelDefinitionSchema.parse(props)
}
13 changes: 13 additions & 0 deletions src/models/solidrivet/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineModel, type ModelRegistry } from "../../model-registry"
import { parseSolidRivetModelParams } from "./parse-model-string"
import { solidRivetModelDefinitionSchema } from "./schema"

export const model = defineModel({
name: "solidrivet",
schema: solidRivetModelDefinitionSchema,
parse: parseSolidRivetModelParams,
})

export function register(registry: ModelRegistry): void {
registry.register(model)
}
94 changes: 94 additions & 0 deletions src/models/solidrivet/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { z } from "zod"
import { modelLengthSchema } from "../../model-length-schema"

const positiveLength = z
.union([
z.number(),
z
.string()
.regex(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:mm|cm|m|in|inch|mil|ft|feet)?$/i)
.transform((value) => value.toLowerCase()),
])
.pipe(modelLengthSchema)
.refine((value) => value > 0, "Length must be positive")

const shape = {
custom: z.literal(true).default(true),
diameter: positiveLength,
/** Under-head shank length; the head is additional to this length. */
length: positiveLength,
headDiameter: positiveLength,
/** Height of a spherical cap above its flat bearing plane. */
headHeight: positiveLength,
roundHead: z.literal(true).default(true),
flatTail: z.literal(true).default(true),
unset: z.literal(true).default(true),
}

type ResolvedProps = z.output<z.ZodObject<typeof shape>>

const getHeadSphereRadius = (props: ResolvedProps) =>
props.headHeight / 2 +
(props.headDiameter / 2) * (props.headDiameter / props.headHeight / 4)

function validate(props: ResolvedProps, context: z.RefinementCtx) {
if (props.headDiameter <= props.diameter)
context.addIssue({
code: "custom",
path: ["headDiameter"],
message: "Head diameter must exceed the shank diameter",
})
if (props.headHeight > props.headDiameter / 2)
context.addIssue({
code: "custom",
path: ["headHeight"],
message:
"The round head must be a spherical cap no taller than a hemisphere",
})
if (
!Number.isFinite(getHeadSphereRadius(props)) ||
!Number.isFinite(props.length + props.headHeight)
)
context.addIssue({
code: "custom",
message: "Derived head radius and overall length must be finite",
})
}

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<typeof solidRivetModelPropsSchema>
export type SolidRivetModelDefinition = z.output<
typeof solidRivetModelDefinitionSchema
>

/**
* Nominal dimensions in a right-handed local XYZ frame, in millimeters.
* The shank axis is Z, the bearing plane is Z=0, the head points toward +Z,
* and the flat unpeened tail is at Z=-length. Returned Z values are positions.
*/
export function getSolidRivetDimensions(input: SolidRivetModelPropsInput) {
const props = solidRivetModelPropsSchema.parse(input)
const headSphereRadius = getHeadSphereRadius(props)
return {
diameter: props.diameter,
length: props.length,
headDiameter: props.headDiameter,
headHeight: props.headHeight,
overallLength: props.length + props.headHeight,
headSphereRadius,
headSphereCenterZ: props.headHeight - headSphereRadius,
shankBottomZ: -props.length,
headTopZ: props.headHeight,
}
}
33 changes: 33 additions & 0 deletions tests/solid-rivet-boolean-selectors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { expect, test } from "bun:test"
import { mp, solidRivetModelPropsSchema } from "../src"

test("solid rivet selectors are boolean flags with no enum aliases", () => {
const base = "solidrivet_d3mm_l10mm_headod5.5mm_headh2.2mm"
const model = mp.string(`${base}_custom_roundhead_flattail_unset`).json()
expect(model).toMatchObject({
custom: true,
roundHead: true,
flatTail: true,
unset: true,
})
expect(model).not.toHaveProperty("specification")
expect(mp.string(base).json()).toEqual(model)
for (const selector of [
"spec(custom)",
"head(round)",
"tail(flat)",
"state(unset)",
"custom_custom",
"custom(true)",
])
expect(() => mp.string(`${base}_${selector}`).json()).toThrow()
expect(() =>
solidRivetModelPropsSchema.parse({
diameter: 3,
length: 10,
headDiameter: 5.5,
headHeight: 2.2,
specification: "custom",
}),
).toThrow()
})
Loading
Loading