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
9 changes: 9 additions & 0 deletions docs/cornerfoot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# CornerFoot

```text
cornerfoot_w25mm_d25mm_h12mm_wall3mm_seat(20mm,20mm)_hole4mm_base3mm_cornercup
```

Square or rectangular corner foot centered on XY. Base mounting face is Z=0; the supported member sits at Z=baseThickness. Two perpendicular locating walls occupy the negative-X and negative-Y outside edges and reach Z=height. The stated seat starts at their inside corner. One central fixing hole at X=Y=0 passes through the base only. Base thickness defaults to the wall thickness. No rounded exterior or inferred load rating.

Lengths accept complete decimals with optional units and normalize to millimeters. The schema rejects unknown fields, duplicate aliases, malformed tokens and dimensions that eliminate required material. `getCornerFootDimensions` exposes the resolved dimensions and installation datums. Legacy roadmap selectors map to value-free flags; there are no enum selectors in the resolved contract.
1 change: 1 addition & 0 deletions src/models/cornerfoot/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./schema"
55 changes: 55 additions & 0 deletions src/models/cornerfoot/parse-model-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { RawModelprinterParams } from "../../parse-model-string"
import { splitModelStringTokens } from "../../split-model-string-tokens"
import { cornerFootModelDefinitionSchema } from "./schema"
const lengths: Record<string, string> = {
w: "width",
d: "depth",
h: "height",
wall: "wallThickness",
base: "baseThickness",
seatw: "seatWidth",
seatd: "seatDepth",
hole: "holeDiameter",
width: "width",
depth: "depth",
height: "height",
wallthickness: "wallThickness",
basethickness: "baseThickness",
seatwidth: "seatWidth",
seatdepth: "seatDepth",
holediameter: "holeDiameter",
}
const flags: Record<string, string> = { cornercup: "cornerCup" }
const integers: Record<string, string> = {}
const tuples: Record<string, readonly [string, string]> = {
seat: ["seatWidth", "seatDepth"],
}
export function parseCornerFootModelParams(raw: RawModelprinterParams) {
const [name, ...tokens] = splitModelStringTokens(raw.string)
if (raw.fn !== "cornerfoot" || name?.toLowerCase() !== "cornerfoot")
throw new Error("Expected cornerfoot without inline values")
const props: Record<string, unknown> = { fn: "cornerfoot" }
const assign = (property: string, value: unknown) => {
if (Object.hasOwn(props, property))
throw new Error(`Duplicate cornerfoot property "${property}"`)
props[property] = value
}
for (const token of tokens) {
const match = token.match(/^([a-z]+)(.*)$/i)
if (!match) throw new Error(`Malformed cornerfoot token "${token}"`)
const key = match[1]!.toLowerCase(),
value = match[2]!
if (Object.hasOwn(lengths, key) && value) assign(lengths[key]!, value)
else if (Object.hasOwn(flags, key) && !value) assign(flags[key]!, true)
else if (Object.hasOwn(integers, key) && /^\d+$/.test(value))
assign(integers[key]!, Number(value))
else if (Object.hasOwn(tuples, key)) {
const pair = value.match(/^\(([^(),]+),([^(),]+)\)$/)
if (!pair)
throw new Error(`Token "${key}" requires two parenthesized lengths`)
assign(tuples[key]![0], pair[1]!.trim())
assign(tuples[key]![1], pair[2]!.trim())
} else throw new Error(`Unknown or malformed cornerfoot token "${token}"`)
}
return cornerFootModelDefinitionSchema.parse(props)
}
11 changes: 11 additions & 0 deletions src/models/cornerfoot/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineModel, type ModelRegistry } from "../../model-registry"
import { cornerFootModelDefinitionSchema } from "./schema"
import { parseCornerFootModelParams } from "./parse-model-string"
export const model = defineModel({
name: "cornerfoot",
schema: cornerFootModelDefinitionSchema,
parse: parseCornerFootModelParams,
})
export function register(registry: ModelRegistry): void {
registry.register(model)
}
129 changes: 129 additions & 0 deletions src/models/cornerfoot/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { z } from "zod"
import { modelLengthSchema } from "../../model-length-schema"

const length = z
.union([
z.number(),
z
.string()
.regex(
/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:mm|cm|m|in|inch|mil|ft|feet)?$/i,
"Expected a complete numeric length with an optional unit",
)
.transform((value) => value.toLowerCase()),
])
.pipe(modelLengthSchema)
const positiveLength = length.refine(
(value) => value > 0,
"Length must be positive",
)
const nonnegativeLength = length.refine(
(value) => value >= 0,
"Length cannot be negative",
)
const shape = {
width: positiveLength,
depth: positiveLength,
height: positiveLength,
wallThickness: positiveLength,
baseThickness: positiveLength.optional(),
seatWidth: positiveLength,
seatDepth: positiveLength,
holeDiameter: positiveLength,
cornerCup: z.literal(true).default(true),
}
const resolve = <P extends { wallThickness: number; baseThickness?: number }>(
p: P,
) => ({ ...p, baseThickness: p.baseThickness ?? p.wallThickness })

type ResolvedProps = z.output<z.ZodObject<typeof shape>> & {
baseThickness: number
}
function validate(p: ResolvedProps, context: z.RefinementCtx) {
const issue = (message: string, path: keyof ResolvedProps) =>
context.addIssue({ code: "custom", message, path: [path] })
if (2 * p.wallThickness >= Math.min(p.width, p.depth))
issue(
"Locating walls must leave space around the central fixing hole",
"wallThickness",
)
if (p.baseThickness >= p.height)
issue("The base must leave positive-height locating walls", "baseThickness")
if (
p.seatWidth > p.width - p.wallThickness ||
p.seatDepth > p.depth - p.wallThickness
)
issue(
"The seat must fit within both locating walls and the far base edges",
"seatWidth",
)
if (
p.holeDiameter / 2 >=
Math.min(p.width / 2, p.depth / 2) - p.wallThickness
)
issue(
"The central fixing hole must leave material before both locating walls",
"holeDiameter",
)
if (
p.width / 2 - p.wallThickness + p.holeDiameter / 2 > p.seatWidth ||
p.depth / 2 - p.wallThickness + p.holeDiameter / 2 > p.seatDepth
)
issue(
"The central fixing hole must lie completely within the stated seat",
"seatWidth",
)
if (
!Number.isFinite(
8 *
Math.max(
p.width,
p.depth,
p.height,
p.wallThickness,
p.baseThickness,
p.seatWidth,
p.seatDepth,
p.holeDiameter,
) **
3,
)
)
issue("Dimensions exceed finite derived geometry limits", "width")
}
/** Square or rectangular corner foot centered on XY. Base mounting face is Z=0; the supported member sits at Z=baseThickness. Two perpendicular locating walls occupy the negative-X and negative-Y outside edges and reach Z=height. The stated seat starts at their inside corner. One central fixing hole at X=Y=0 passes through the base only. Base thickness defaults to the wall thickness. No rounded exterior or inferred load rating. */
export const cornerFootModelPropsSchema = z
.object(shape)
.strict()
.transform(resolve)
.superRefine(validate)
export const cornerFootModelDefinitionSchema = z
.object({ fn: z.literal("cornerfoot"), ...shape })
.strict()
.transform(resolve)
.superRefine(validate)
export type CornerFootModelPropsInput = z.input<
typeof cornerFootModelPropsSchema
>
export type CornerFootModelProps = z.output<typeof cornerFootModelPropsSchema>
export type CornerFootModelDefinition = z.output<
typeof cornerFootModelDefinitionSchema
>

/** Normalized millimeter dimensions and installation datums. */
export function getCornerFootDimensions(input: CornerFootModelPropsInput) {
const p = cornerFootModelPropsSchema.parse(input)
return {
...p,
size: [p.width, p.depth, p.height] as [number, number, number],
bottomZ: 0,
topZ: p.height,
seatMinX: -p.width / 2 + p.wallThickness,
seatMinY: -p.depth / 2 + p.wallThickness,
seatMaxX: -p.width / 2 + p.wallThickness + p.seatWidth,
seatMaxY: -p.depth / 2 + p.wallThickness + p.seatDepth,
seatZ: p.baseThickness,
holeCenter: [0, 0, 0] as [number, number, number],
holeDepth: p.baseThickness,
}
}
31 changes: 31 additions & 0 deletions tests/cornerfoot-contract-01.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect, test } from "bun:test"
import { mp, cornerFootModelPropsSchema, getCornerFootDimensions } from "../src"
test("cornerfoot complete strings and public dimensions agree", () => {
const props = {
width: 25,
depth: 25,
height: 12,
wallThickness: 3,
baseThickness: 3,
seatWidth: 20,
seatDepth: 20,
holeDiameter: 4,
cornerCup: true,
} as const
expect(
mp
.string(
"cornerfoot_w25mm_d25mm_h12mm_wall3mm_seat(20mm,20mm)_hole4mm_base3mm_cornercup",
)
.json(),
).toEqual({ fn: "cornerfoot", ...props })
expect(
mp
.string(
"cornerfoot_w25mm_d25mm_h12mm_wall3mm_seat(20mm,20mm)_hole4mm_cornercup",
)
.json(),
).toEqual({ fn: "cornerfoot", ...props })
expect(cornerFootModelPropsSchema.parse(props)).toEqual(props)
expect(getCornerFootDimensions(props).size).toEqual([25, 25, 12])
})
40 changes: 40 additions & 0 deletions tests/cornerfoot-contract-02.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect, test } from "bun:test"
import { mp, cornerFootModelPropsSchema } from "../src"
test("cornerfoot unit and token case normalization preserves dimensions", () => {
expect(
cornerFootModelPropsSchema.parse({
width: "2.5cm",
depth: "2.5cm",
height: "1.2cm",
wallThickness: "0.3cm",
baseThickness: "0.3cm",
seatWidth: "2.0cm",
seatDepth: "2.0cm",
holeDiameter: "0.4cm",
cornerCup: true,
}),
).toEqual({
width: 25,
depth: 25,
height: 12,
wallThickness: 3,
baseThickness: 3,
seatWidth: 20,
seatDepth: 20,
holeDiameter: 4,
cornerCup: true,
})
expect(
mp
.string(
"CORNERFOOT_W25MM_D25MM_H12MM_WALL3MM_SEAT(20MM,20MM)_HOLE4MM_BASE3MM_CORNERCUP",
)
.json(),
).toEqual(
mp
.string(
"cornerfoot_w25mm_d25mm_h12mm_wall3mm_seat(20mm,20mm)_hole4mm_base3mm_cornercup",
)
.json(),
)
})
30 changes: 30 additions & 0 deletions tests/cornerfoot-contract-03.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expect, test } from "bun:test"
import { mp, cornerFootModelPropsSchema } from "../src"
test("cornerfoot rejects ambiguous and physically incompatible inputs", () => {
const props = {
width: 25,
depth: 25,
height: 12,
wallThickness: 3,
baseThickness: 3,
seatWidth: 20,
seatDepth: 20,
holeDiameter: 4,
cornerCup: true,
} as const
for (const source of [
"cornerfoot_w25mm_d25mm_h12mm_wall3mm_seat(20mm,20mm)_hole4mm_base3mm_cornercup_unknown1mm",
"cornerfoot_w25mm_d25mm_h12mm_wall3mm_seat(20mm,20mm)_hole4mm_base3mm_cornercup_seat(30mm,20mm)",
"cornerfoot",
])
expect(() => mp.string(source).json()).toThrow()
expect(() =>
cornerFootModelPropsSchema.parse({ ...props, ...{ holeDiameter: 20 } }),
).toThrow()
expect(() =>
cornerFootModelPropsSchema.parse({ ...props, width: Infinity }),
).toThrow()
expect(() =>
cornerFootModelPropsSchema.parse({ ...props, width: "12mmjunk" }),
).toThrow()
})
9 changes: 9 additions & 0 deletions tests/cornerfoot-contract-boolean-selectors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { expect, test } from "bun:test"
import { mp } from "../src"

test("cornerfoot uses boolean flags and rejects enum-style selectors", () => {
const base = "cornerfoot_w25mm_d25mm_h12mm_wall3mm_seat(20mm,20mm)_hole4mm"
expect(mp.string(base + "_cornercup").json()).toEqual(mp.string(base).json())
for (const selector of ["shape(cornercup)"])
expect(() => mp.string(base + "_" + selector).json()).toThrow()
})
Loading