diff --git a/cookbook/snippets/assembly-connector-and-revolute-mate.md b/cookbook/snippets/assembly-connector-and-revolute-mate.md new file mode 100644 index 000000000..0825719c6 --- /dev/null +++ b/cookbook/snippets/assembly-connector-and-revolute-mate.md @@ -0,0 +1,36 @@ +--- +id: assembly-connector-and-revolute-mate +title: Declare a connector and join parts with a revolute mate +tags: [assembly, connector, mate, revolute, hinge, joint] +keywords: + - assembly with parts connectors and mates + - how do I declare a connector + - revolute joint + - hinge + - axis connector origin + - pivot joint between parts + - declare connector and mate +when_to_use: >- + The model has multiple mechanical parts (not a single fused body) that + need a named pivot between them. Declare an axis connector on each part + with partRef.connector(name, { type: 'axis', origin: { kind: 'vec3', value: + [...] }, axis: [...] }), then join the connectors with arm.mate(name, + 'partA.conn', 'partB.conn', 'revolute', { limitsDeg: [min, max] }). This is + the canonical assembly-topology vocabulary for hinges, elbows, and any + revolute joint — connector origins must use the tagged + { kind: 'vec3', value: [...] } form, not a bare [x, y, z] array. +--- + +```typescript +const arm = assembly('two-link-arm'); + +const base = arm.part('base', box(40, 40, 16)); +const link = arm.part('arm', box(10, 40, 6).translate(0, 20, 0)); + +base.connector('pivot', { type: 'axis', origin: { kind: 'vec3', value: [5, 20, 16] }, axis: [0, 1, 0] }); +link.connector('pivot', { type: 'axis', origin: { kind: 'vec3', value: [0, 0, 0] }, axis: [0, 1, 0] }); + +arm.mate('elbow', 'base.pivot', 'arm.pivot', 'revolute', { limitsDeg: [0, 90] }); + +return arm.solvedModel({ elbow: 45 }); +``` diff --git a/cookbook/snippets/clamshell-hinge-two-part-assembly.md b/cookbook/snippets/clamshell-hinge-two-part-assembly.md new file mode 100644 index 000000000..12ea9dffc --- /dev/null +++ b/cookbook/snippets/clamshell-hinge-two-part-assembly.md @@ -0,0 +1,34 @@ +--- +id: clamshell-hinge-two-part-assembly +title: Clamshell hinge between a base and a lid, joined by a revolute mate +tags: [assembly, connector, mate, revolute, hinge, joint] +keywords: + - hinge + - revolute joint + - clamshell hinge + - lid pivots on base + - assembly with connectors and mates + - declare a connector + - hingeAxis connector +when_to_use: >- + You are modeling a laptop-lid-style clamshell hinge, or any assembly + where one rigid body swings about a fixed pivot line on another rigid + body. Declare a matching axis connector on both bodies along the + physical hinge line, then join them with a revolute mate and limitsDeg + to bound the swing angle. Relevant for hinge, revolute joint, or + assembly with connectors and mates. +--- + +```typescript +const arm = assembly('clamshell-hinge'); + +const base = arm.part('base', box(300, 200, 12).translate(0, 0, 6)); +const lid = arm.part('lid', box(300, 200, 6).translate(0, 100, 3)); + +base.connector('hingeAxis', { type: 'axis', origin: { kind: 'vec3', value: [0, -100, 12] }, axis: [1, 0, 0] }); +lid.connector('hingeAxis', { type: 'axis', origin: { kind: 'vec3', value: [0, 0, 0] }, axis: [1, 0, 0] }); + +arm.mate('hinge', 'base.hingeAxis', 'lid.hingeAxis', 'revolute', { limitsDeg: [-15, 135] }); + +return arm.solvedModel({ hinge: 90 }); +``` diff --git a/cookbook/tags.json b/cookbook/tags.json index 9dd24a90b..d6833f0c4 100644 --- a/cookbook/tags.json +++ b/cookbook/tags.json @@ -23,5 +23,11 @@ "joinery", "bracket", "stacking", - "symmetry" + "symmetry", + "assembly", + "connector", + "mate", + "revolute", + "hinge", + "joint" ] diff --git a/src/agent/mcp/tools/mechanismCookbook.test.ts b/src/agent/mcp/tools/mechanismCookbook.test.ts new file mode 100644 index 000000000..88d533e6c --- /dev/null +++ b/src/agent/mcp/tools/mechanismCookbook.test.ts @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Andrii Shylenko and kernelCAD contributors +// +// KC-10: lookup_cookbook could not see any mechanism/assembly content — every +// query about connectors, mates, or hinges returned single-body geometry +// snippets (top hit for "assembly with parts, connectors and mates" was +// mirror-half-part). This asserts the cookbook corpus now surfaces the +// mechanism snippets for the queries that used to fall through to +// single-body geometry. +import { describe, it, expect } from 'vitest'; +import { lookupCookbookTool } from './lookupCookbook'; + +const MECHANISM_SNIPPET_IDS = new Set([ + 'assembly-connector-and-revolute-mate', + 'clamshell-hinge-two-part-assembly', +]); + +describe('lookupCookbookTool — mechanism/assembly coverage (KC-10)', () => { + const queries = [ + 'assembly with parts connectors and mates', + 'hinge', + 'revolute joint', + 'how do I declare a connector', + ]; + + for (const query of queries) { + it(`returns a mechanism snippet for "${query}"`, async () => { + const r = await lookupCookbookTool({ query }); + expect(r.ok).toBe(true); + expect(r.hits!.length).toBeGreaterThan(0); + const ids = r.hits!.map((h) => h.id); + expect(ids.some((id) => MECHANISM_SNIPPET_IDS.has(id))).toBe(true); + }); + } + + it('the returned mechanism snippet shows the tagged connector-origin form, not a bare array', async () => { + const r = await lookupCookbookTool({ query: 'how do I declare a connector' }); + const hit = r.hits!.find((h) => MECHANISM_SNIPPET_IDS.has(h.id)); + expect(hit).toBeDefined(); + expect(hit!.body).toContain("origin: { kind: 'vec3', value:"); + expect(hit!.body).toMatch(/type: 'axis'/); + expect(hit!.body).toMatch(/'revolute'/); + }); +}); diff --git a/src/agent/skills/kernelcad-authoring/SKILL.md b/src/agent/skills/kernelcad-authoring/SKILL.md index 94be2ea4c..420c1d7a0 100644 --- a/src/agent/skills/kernelcad-authoring/SKILL.md +++ b/src/agent/skills/kernelcad-authoring/SKILL.md @@ -828,8 +828,10 @@ When you need a canonical pattern, call MCP tool `lookup_cookbook(query, k?)` to | ID | Trigger | |---|---| +| assembly-connector-and-revolute-mate | The model has multiple mechanical parts (not a single fused body) that need a named pivot between them. Declare an axis connector on each part with partRef.connector(name, { type: 'axis', origin: { kind: 'vec3', value: [...] }, axis: [...] }), then join the connectors with arm.mate(name, 'partA.conn', 'partB.conn', 'revolute', { limitsDeg: [min, max] }). This is the canonical assembly-topology vocabulary for hinges, elbows, and any revolute joint — connector origins must use the tagged { kind: 'vec3', value: [...] } form, not a bare [x, y, z] array. | | blind-pocket-from-top | You want a pocket cut into the top face only — the cylinder is shorter than the plate so it does not reach the bottom face. | | chamfer-rotated-face | You rotated a primitive and now want to chamfer one of its canonical faces by name (face-name semantics survive transforms). | +| clamshell-hinge-two-part-assembly | You are modeling a laptop-lid-style clamshell hinge, or any assembly where one rigid body swings about a fixed pivot line on another rigid body. Declare a matching axis connector on both bodies along the physical hinge line, then join them with a revolute mate and limitsDeg to bound the swing angle. Relevant for hinge, revolute joint, or assembly with connectors and mates. | | clearance-hole-through-plate | You need a through-hole sized for a bolt with a small clearance margin; cylinder height extends beyond the plate so the cut is unambiguous. | | extrude-rounded-rect-plate | You want a flat plate with rounded corners; use the dedicated rounded-rect extrude rather than building corners by hand. | | fillet-face-after-subtract | After subtracting a hole or pocket, you want to round only the rim of the resulting opening — not every edge in the part. |