From db763f182e753e2d769471b73251cd3519797650 Mon Sep 17 00:00:00 2001 From: ManuelStaggl <17688572+ManuelStaggl@users.noreply.github.com> Date: Wed, 11 Nov 2026 18:28:02 +0100 Subject: [PATCH 1/2] feat(desktop): the assistant looks up the standards of a kind of part before it designs one partbuddy_standard gives the rules and sizes for eight kinds of part (sliding lid, clamp, feet, hook, cable entry, vent, board mount, star knob) from one table, instead of a fixed block per kind. Co-Authored-By: Claude Fable 5.2 --- apps/desktop/src/main/agents/claudeCode.ts | 2 + apps/desktop/src/main/agents/loop.ts | 2 + apps/desktop/src/main/agents/policy.ts | 2 + apps/desktop/src/main/agents/standardTool.ts | 40 +++++++ apps/desktop/src/main/index.ts | 3 +- apps/desktop/src/shared/standards.ts | 105 +++++++++++++++++++ apps/desktop/test/standardTool.test.ts | 43 ++++++++ docs/feature-catalog.md | 1 + 8 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/main/agents/standardTool.ts create mode 100644 apps/desktop/src/shared/standards.ts create mode 100644 apps/desktop/test/standardTool.test.ts diff --git a/apps/desktop/src/main/agents/claudeCode.ts b/apps/desktop/src/main/agents/claudeCode.ts index a0643da..af910a4 100644 --- a/apps/desktop/src/main/agents/claudeCode.ts +++ b/apps/desktop/src/main/agents/claudeCode.ts @@ -9,6 +9,7 @@ import { CLAUDE_RESEARCH_TOOLS, RESEARCH_GUIDANCE, sourceOfSearchResult, sourceO import { VIEW_GUIDANCE } from './viewContext'; import { ASSEMBLY_GUIDANCE, JOINT_GUIDANCE } from './assemblyTool'; import { BLOCK_GUIDANCE } from './blockTool'; +import { STANDARD_GUIDANCE } from './standardTool'; import { MECHANISM_GUIDANCE } from './mechanismTool'; /** @@ -297,6 +298,7 @@ function roleOf(research: boolean): string { JOINT_GUIDANCE, MECHANISM_GUIDANCE, BLOCK_GUIDANCE, + STANDARD_GUIDANCE, QUESTION_GUIDANCE, ANSWER_GUIDANCE, ...(research ? [RESEARCH_GUIDANCE] : []), diff --git a/apps/desktop/src/main/agents/loop.ts b/apps/desktop/src/main/agents/loop.ts index d5332ec..1ddee1c 100644 --- a/apps/desktop/src/main/agents/loop.ts +++ b/apps/desktop/src/main/agents/loop.ts @@ -9,6 +9,7 @@ import { withToolNotes } from './toolNotes'; import { VIEW_GUIDANCE } from './viewContext'; import { ASSEMBLY_GUIDANCE, JOINT_GUIDANCE } from './assemblyTool'; import { BLOCK_GUIDANCE } from './blockTool'; +import { STANDARD_GUIDANCE } from './standardTool'; import { MECHANISM_GUIDANCE } from './mechanismTool'; /** @@ -28,6 +29,7 @@ const INSTRUCTIONS = [ JOINT_GUIDANCE, MECHANISM_GUIDANCE, BLOCK_GUIDANCE, + STANDARD_GUIDANCE, QUESTION_GUIDANCE, ANSWER_GUIDANCE, ].join(' '); diff --git a/apps/desktop/src/main/agents/policy.ts b/apps/desktop/src/main/agents/policy.ts index 6a5f1be..43172ad 100644 --- a/apps/desktop/src/main/agents/policy.ts +++ b/apps/desktop/src/main/agents/policy.ts @@ -44,6 +44,8 @@ const ALLOWED = new Set([ 'partbuddy_mechanism', // Builds a function block or a wall mounting from the standard tables (blockTool.ts), answered by PartBuddy. 'partbuddy_block', + // The rules and sizes for a kind of part (standardTool.ts), answered by PartBuddy; it builds nothing. + 'partbuddy_standard', ]); /** diff --git a/apps/desktop/src/main/agents/standardTool.ts b/apps/desktop/src/main/agents/standardTool.ts new file mode 100644 index 0000000..97ad10f --- /dev/null +++ b/apps/desktop/src/main/agents/standardTool.ts @@ -0,0 +1,40 @@ +import { STANDARDS, STANDARD_TOPICS, standardText, type StandardTopic } from '../../shared/standards'; +import type { ToolResult, UpstreamTool, UpstreamTools } from './gateProxy'; + +/** + * `partbuddy_standard` (decided 2026-11-11): the rules and sizes for a kind of part, read by the + * assistant before it designs one itself. They stay out of the role prompt, which every request + * carries, and come from one table (`standards.ts`) instead of the model's memory. + */ + +export const STANDARD_TOOL = 'partbuddy_standard'; + +const standardTool: UpstreamTool = { + name: STANDARD_TOOL, + description: `Read PartBuddy's rules and sizes for a kind of part before you design one yourself: ${STANDARD_TOPICS.map((topic) => `${topic} (${STANDARDS[topic].title.toLowerCase()})`).join(', ')}. It builds nothing.`, + inputSchema: { + type: 'object', + properties: { topic: { type: 'string', enum: [...STANDARD_TOPICS] } }, + required: ['topic'], + additionalProperties: false, + }, +}; + +/** The upstream tools plus `partbuddy_standard`. */ +export function withStandardTool(upstream: UpstreamTools): UpstreamTools { + return { + instructions: () => upstream.instructions(), + listTools: async () => [...(await upstream.listTools()), standardTool], + callTool: (name, args, callId): Promise => { + if (name !== STANDARD_TOOL) return upstream.callTool(name, args, callId); + const topic = args.topic; + if (typeof topic !== 'string' || !(STANDARD_TOPICS as readonly string[]).includes(topic)) { + return Promise.resolve({ content: [{ type: 'text', text: `topic must be one of ${STANDARD_TOPICS.join(', ')}` }], isError: true }); + } + return Promise.resolve({ content: [{ type: 'text', text: standardText(topic as StandardTopic) }] }); + }, + }; +} + +/** Added to the role prompt of every route. */ +export const STANDARD_GUIDANCE = `Before you design a sliding lid, a clip or clamp, feet, a hook or an eye, a cable entry, vents or a fan opening, a mount for a circuit board or a star knob, call ${STANDARD_TOOL} with that topic and follow what it says: the sizes come from data sheets and standards, not from your memory.`; diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index ad1292d..045d320 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -28,6 +28,7 @@ import { keepPrintFace, readAssembly, readPrintFaces, writeAssembly } from './pr import { Questions } from './agents/questions'; import { WorkLog } from './agents/workLog'; import { withBlockTool } from './agents/blockTool'; +import { withStandardTool } from './agents/standardTool'; import { withMechanismTool } from './agents/mechanismTool'; import { blockPhrase, describeUserAction } from '../shared/stepPhrases'; import { ChatGptAccount } from './chatgpt/account'; @@ -583,7 +584,7 @@ if (!app.requestSingleInstanceLock()) { steps.tools( withDesignTool( withAssemblyTool( - withViewTool(guardNotes(withBlockTool(withMechanismTool(withDrawings(upstream, drawings), () => printProfiles.part().printer.nozzle, keepMechanismFace), keepMechanismFace), (trial) => supervisor.trialSteps(trial)), () => viewState, () => notesService.forView(), checkForView), + withViewTool(guardNotes(withStandardTool(withBlockTool(withMechanismTool(withDrawings(upstream, drawings), () => printProfiles.part().printer.nozzle, keepMechanismFace), keepMechanismFace)), (trial) => supervisor.trialSteps(trial)), () => viewState, () => notesService.forView(), checkForView), () => viewState?.bodies.map((body) => body.name) ?? [], saveAssembly, ), diff --git a/apps/desktop/src/shared/standards.ts b/apps/desktop/src/shared/standards.ts new file mode 100644 index 0000000..11c4100 --- /dev/null +++ b/apps/desktop/src/shared/standards.ts @@ -0,0 +1,105 @@ +/** + * The standards the assistant looks up before it designs a part of a kind itself (decided + * 2026-11-11: no fixed block per kind, whose shape depends on the part; the rules and sizes are + * what stays). Each line says where its figure comes from: a data sheet or standard, guides for + * printed parts (which disagree, so a range), or PartBuddy's own choice. Nothing here has been + * printed by PartBuddy; a line changes when a print shows it wrong. + */ + +export const STANDARD_TOPICS = ['slidingLid', 'clamp', 'feet', 'hook', 'cableEntry', 'vent', 'boardMount', 'starKnob'] as const; +export type StandardTopic = (typeof STANDARD_TOPICS)[number]; + +export const STANDARDS: Record = { + slidingLid: { + title: 'A lid that slides into grooves of a box', + rules: [ + 'Grooves with 45° flanks (a dovetail) in the two long walls and the back wall, open at the front: they print without support with the box standing. A square groove is a bridge and sags from 5 mm (Hubs).', + 'Lid 3 to 4 mm thick, its edges chamfered at 45° to match; groove 1.5 mm deep, at most half the wall; 2 mm of wall over the groove; walls 3 mm (guides).', + 'Room 0.3 mm per side and 0.3 mm above and below the lid (guides say 0.2 to 0.4; Prusa asks at least 0.3 for moving parts, Hubs 0.5).', + 'Hold it shut with a bump 0.3 mm high across the lid near the open end and a matching dent, and give the lid a finger dent about 12 mm round and 2 mm deep, or a raised ridge (guides, one source each).', + 'Print the box standing and the lid lying flat. The front wall of the box is lower by the lid, so the lid closes it.', + 'For a rail pushed onto another part use partbuddy_mechanism with kind plug and shape dovetail instead.', + ], + }, + clamp: { + title: 'A clip or clamp round a tube, a rod or a cable', + rules: [ + 'A C clip to snap on: inner diameter the tube and 0.3 to 0.5 mm, the opening 75 to 85 % of the tube, arms 1.5 to 2.5 mm thick, 8 to 12 mm wide (guides). Round the ends of the arms outwards so the tube finds its way in.', + 'A clamp closed by a screw: bore the measured diameter to grip (0.1 to 0.2 mm more to slide), a slot 2 to 5 mm wide, wall about 4 mm at 25 mm, and lugs either side of the slot for the screw; make the screw holes with partbuddy_block or partbuddy_mechanism kind screw.', + 'A clamp of two halves: 0.4 mm of room on the tube, a gap of 1 mm between the halves so the screws can pull them tight (guides).', + 'Always print it lying, the bore upright: the layers then run round the ring. Standing, the arms break along their layers (guides, and measured for hooks).', + 'Ask for the measured diameter; tubes are sold in many series and none is assumed here.', + ], + }, + feet: { + title: 'Feet and what a part stands on', + rules: [ + 'Stick-on rubber feet hold on a table; printed feet slide. Common ones (3M Bumpon data sheet, ±0.5 mm): SJ5302 7.9 × 2.2 mm, SJ5076 8.0 × 2.8, SJ5306 9.5 × 3.8, SJ5003 11.1 × 5.0, SJ5012 12.7 × 3.5, SJ5744 19.1 × 4.1.', + 'A recess for such a foot is not needed (3M) but keeps it from being sheared off: 0.5 mm wider than the foot, 0.5 to 1 mm deep, never deeper than half the foot, so it still stands proud (own choice from that tolerance).', + 'Four feet near the corners for anything that is pressed on or may tip; three never rock on an uneven table (guides).', + 'Chamfer every edge that lies on the print bed by 0.4 mm at 45°: the first layer is squeezed wider (the slicers compensate about 0.2 mm; Hubs asks for a chamfer; Gridfinity uses 0.8 mm).', + 'Feet printed on: at least 8 mm across and 2 mm high, with a chamfered rim. Feet of TPU cushion more than they grip (guides).', + ], + }, + hook: { + title: 'Hooks and eyes that carry a load', + rules: [ + 'Print a hook lying on its side, so the load runs along the layers: measured on one test hook (CNC Kitchen), lying held 119 kg in PLA and 106 kg in PETG until it broke, standing 69 kg and 36 kg. These are breaking loads of one shape, not what a hook may carry for years: plastic creeps, so allow a tenth.', + 'Layers of at most 0.2 mm and four or more wall lines; at 0.3 mm the standing hook held half (CNC Kitchen).', + 'Round the inside corner where the hook leaves its plate, with a radius of at least the hook\'s thickness (Hubs asks for a fillet without a size; the size is our choice), and keep the section at least 6 × 6 mm for anything heavier than a coat.', + 'Hang the plate with partbuddy_block keyhole or woodScrew, two of them level from 60 mm of width.', + 'An eye: hole 4 mm for a key ring (5 mm for a heavy one), 3.5 mm for a ball chain, twice the wire and 1 mm for a carabiner, each 0.2 mm more for print; 2 to 3 mm of plastic round the hole (guides say 1.5 to 3).', + 'A slot for a cable tie: ties are 2.5, 3.5 and 4.6 mm wide (HellermannTyton T18R, T30R, T50R) and about 1 mm thick; make the slot 1 mm wider and 1.5 mm high (own choice), and print it so that it stands upright: a lying slot sags.', + ], + }, + cableEntry: { + title: 'A cable into a housing, held against pulling', + rules: [ + 'Cables: USB-C 2 to 4 mm thick (charging) or 4 to 6 mm (full-featured) (USB-IF); mains flex H03VV-F 2 × 0.75 about 5.8 mm, H05VV-F 3 × 0.75 from 6.0 to 7.6 mm by maker. Ask for the measured cable.', + 'A plain hole: the cable and 1 mm, its edges rounded or chamfered by 0.5 mm so they do not cut (own choice). A cable with its plug moulded on needs a slot from the rim of the wall down to the hole, closed by the lid, or the plug\'s own size and 1 mm per side (Hubs for cut-outs).', + 'Inside, do not bend the cable tighter than four times its diameter (Helukabel for H05VV-F, fixed).', + 'Strain relief, simplest first: a cable tie round the cable just inside the wall, wider than the hole; or a bridge on the floor with two slots for a tie (2.5 or 3.5 mm wide, slots 1 mm wider, 1.5 mm high, printed upright); or a clamp bar with two screws over the cable, its saddle 0.5 mm less than the cable so it grips (own choice).', + 'A bought cable gland seals and holds at once. Holes by EN 60423: M12 12.2 mm for cables of 3 to 6.5 mm, M16 16.2 mm for 4 to 8 or 5 to 10 mm, M20 20.2 mm for 6 to 12 mm; PG7 12.7 to 13 mm, PG9 15.4 to 15.7 mm (makers differ). The wall needs to be flat round the hole for the nut.', + 'Mains voltage in a printed housing is the user\'s responsibility: say so, and never present the part as safe for it.', + ], + }, + vent: { + title: 'Vents, grilles and fan openings', + rules: [ + 'In an upright wall make slots that stand upright, 2 to 3 mm wide with bars at least 1.2 mm between them (three lines of a 0.4 mm nozzle; Prusa lists 0.45, 0.9, 1.35 and 1.8 mm for one to four lines); a lying slot is a bridge, fine up to 5 mm long (Hubs). Close a long upright slot with a 45° point at its top.', + 'In a flat top or bottom any pattern prints, as the holes are upright. Hexagons 6 to 8 mm across flats with 1.2 mm bars are stiff and open (own choice; no source gives sizes).', + 'To keep fingers out, no opening over 12 mm (IP2X stops what is larger than 12.5 mm); against wire and small parts 1 mm (IP4X).', + 'Fans, mounting holes centre to centre: 25 mm fan 20, 30 fan 24, 40 fan 32, 50 fan 40, 60 fan 50, 80 fan 71.5, 92 fan 82.5, 120 fan 105 mm. Screws by the fan: commonly M3 up to 60 mm and M4 above; check the fan in hand, sources differ on the hole.', + 'A fan opening: a round hole 2 mm less than the fan, or a grille of bars; leave 3 mm of wall between hole and screw.', + 'Air must get in as well as out: openings low on one side and high on the other, about as much area as the fan sweeps.', + ], + }, + boardMount: { + title: 'Holding a circuit board', + rules: [ + 'Standoffs: outer diameter 2 to 2.5 times the hole, at least 5 mm; high enough for what is soldered under the board, 5 mm as a rule and 10 mm over pin headers (guides). Join each to the floor with a 1 mm fillet.', + 'Screwed straight into plastic: pilot hole 0.25 mm less than the screw for print (Hubs), which is 2.0 mm for M2.5 and 2.5 mm for M3 after print; at least twice the screw\'s diameter deep. For a board taken out often use partbuddy_block insertSeat or boss instead.', + 'Without screws: two pins 0.3 mm less than the board\'s holes to locate it and two snap hooks over its edge. The board is 1.6 mm thick as a rule; a hook 1 mm thick at its root and 5 mm wide, 0.5 mm of room (Hubs).', + 'A slot the board slides into: 2.0 mm wide for a 1.6 mm board (own choice, 0.2 mm per side), 2 mm deep.', + 'Leave 0.5 mm round the board and 1 mm per side round every plug in the wall (Hubs).', + 'Boards (from the makers\' drawings, to be checked against the board in hand): Raspberry Pi 4 and 5 are 85 × 56 mm with four M2.5 holes of 2.7 mm in a rectangle 58 × 49 mm, 3.5 mm from the edges; Pi Zero 65 × 30 mm, holes 58 × 23 mm; Arduino Uno 68.58 × 53.34 mm with 3.2 mm holes at (13.97, 2.54), (15.24, 50.80), (66.04, 7.62) and (66.04, 35.56). For any other board ask for its drawing or measured holes.', + ], + }, + starKnob: { + title: 'A star knob that clamps with a screw or a nut', + rules: [ + 'Sizes of DIN 6336 in plastic (Ganter): diameter 25 mm for M5 (hub 12, height 16), 32 for M6 (hub 14, height 20), 40 for M8 (hub 18, height 25), 50 for M10 (hub 22, height 32), 63 for M12 (hub 26, height 40).', + 'Five lobes are usual (the standard\'s count could not be confirmed); round them generously, lobe radius about a sixth of the diameter.', + 'To hold a hexagon screw: a hexagonal pocket from the top, across flats 0.3 mm more than the head and as deep as the head, with a through hole of the thread and 0.4 mm under it. Heads by ISO 4017, across flats and height: M5 8 and 3.5 mm, M6 10 and 4, M8 13 and 5.3.', + 'To hold a nut: the same pocket from below the hub; nuts by ISO 4032 are as wide as the heads and up to 4.7 (M5), 5.2 (M6) and 6.8 mm (M8) high. For M3 to M6 partbuddy_block nutTrap makes the pocket.', + 'Keep plastic round the pocket as thick as the thread is wide, and glue the screw or nut in with a drop of superglue or close the pocket with a pressed-in cap.', + 'Print it with the pocket opening upwards, flat side down. For a knob on a shaft (potentiometer, encoder) use partbuddy_block knob.', + ], + }, +}; + +/** What the assistant reads for a topic. */ +export function standardText(topic: StandardTopic): string { + const { title, rules } = STANDARDS[topic]; + return `${title}. Design it yourself by these rules, and tell the user where a figure is a range or our own choice:\n${rules.map((rule) => `- ${rule}`).join('\n')}\nNone of this has been printed by PartBuddy yet. The print check in partbuddy_view still judges what you build.`; +} diff --git a/apps/desktop/test/standardTool.test.ts b/apps/desktop/test/standardTool.test.ts new file mode 100644 index 0000000..de98411 --- /dev/null +++ b/apps/desktop/test/standardTool.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; + +import type { UpstreamTools } from '../src/main/agents/gateProxy'; +import { decide } from '../src/main/agents/policy'; +import { STANDARD_GUIDANCE, withStandardTool } from '../src/main/agents/standardTool'; +import { STANDARDS, STANDARD_TOPICS, standardText } from '../src/shared/standards'; + +describe('the standards the assistant looks up', () => { + const upstream: UpstreamTools = { + instructions: () => undefined, + listTools: () => Promise.resolve([]), + callTool: (name) => Promise.resolve({ content: [{ type: 'text', text: `upstream ${name}` }] }), + }; + + it('answers a topic itself with its rules, builds nothing and passes other tools on', async () => { + const tools = withStandardTool(upstream); + const [tool] = await tools.listTools(); + expect(tool?.name).toBe('partbuddy_standard'); + expect(tool?.description).toContain('slidingLid (a lid that slides into grooves of a box)'); + const feet = String((await tools.callTool('partbuddy_standard', { topic: 'feet' })).content[0]?.text); + expect(feet).toContain('Feet and what a part stands on. Design it yourself by these rules'); + expect(feet).toContain('SJ5003 11.1 × 5.0'); + expect(feet).toContain('None of this has been printed by PartBuddy yet.'); + expect(await tools.callTool('partbuddy_standard', { topic: 'gear' })).toMatchObject({ isError: true, content: [{ text: `topic must be one of ${STANDARD_TOPICS.join(', ')}` }] }); + expect(await tools.callTool('inspect_design', {})).toMatchObject({ content: [{ text: 'upstream inspect_design' }] }); + expect(decide('partbuddy_standard', { topic: 'feet' })).toEqual({ allow: true }); + }); + + it('has rules for every topic, names each in the guidance and points to the blocks that exist', () => { + for (const topic of STANDARD_TOPICS) { + expect(STANDARDS[topic].rules.length).toBeGreaterThanOrEqual(5); + expect(standardText(topic).split('\n- ')).toHaveLength(STANDARDS[topic].rules.length + 1); + } + expect(STANDARD_GUIDANCE).toContain('call partbuddy_standard with that topic'); + // Where a block builds the thing, the standard sends the assistant there instead of repeating its sizes. + expect(standardText('starKnob')).toContain('partbuddy_block knob'); + expect(standardText('hook')).toContain('partbuddy_block keyhole'); + expect(standardText('slidingLid')).toContain('kind plug and shape dovetail'); + // Figures from the sheets: fan hole spacing and the Raspberry Pi's hole pattern. + expect(standardText('vent')).toContain('80 fan 71.5'); + expect(standardText('boardMount')).toContain('58 × 49 mm'); + }); +}); diff --git a/docs/feature-catalog.md b/docs/feature-catalog.md index bb3cfb7..23002dc 100644 --- a/docs/feature-catalog.md +++ b/docs/feature-catalog.md @@ -61,6 +61,7 @@ Work runs in three phases, one pull request per step, in this order. | G3g Snap latch outside a box (done) | Standards, 2026-11-11; the product owner chose a latch that swings over the joint, and then, when the numbers showed a hinged one cannot catch (a print-in-place hinge has more play than any catch overlaps, and its arm starts in mid-air), this one without a hinge. `partbuddy_mechanism {kind: latch}` (`latchSteps`): an arm 1.6 mm thick grows out of the lid's outer wall on a 45° slope, hangs 1.5 mm off the wall over the joint and snaps with a hook under a bead 1.2 mm high on the box, 0.8 mm of overlap on 45° faces 0.2 mm apart, 1.3 % strain at the root over a 12 mm lever, 2 mm of tab below the hook; 10 mm wide by default. `from` on the joint line on the wall, `to` straight out of it, `up` from box to lid; no joint. It needs 8.1 mm of flat wall on the lid and 14.1 mm on the box, and prints without support with the lid on its top. Not printed yet: the holding force shows on the first print. Not built: a hinged hasp, a sliding bolt for doors. | | G3f Plugged connection (done) | Standards, 2026-11-11. `partbuddy_mechanism {kind: plug}` makes no joint. Shape `peg` (`pegSteps`): round pegs on one body and pockets in the other along `from`–`to`, 5 mm thick by default (Hubs: thinner ones printed standing are weak) and 1.5 diameters long, 0.2 mm of room per side (the slicers' cut connectors use 0 to 0.1, guides 0.05 to 0.3; `gap` is the lever, 0.1 to press), the pocket 1 mm deeper for air and glue, tip and mouth chamfered by 0.5 mm; `more` for further pegs, and a single one is answered with a warning that the parts can turn. Shape `dovetail`: the guide's rail and groove (45° flanks) with 0.2 mm all round and no joint, pushed on from `from` and stopped where the groove ends in the body. Not printed yet. Not built: separate dowels with a pocket in both parts, square or teardrop pegs for lying pockets, a tapered dovetail that clamps. | | G3e Snap hooks with their windows (done) | Standards, 2026-11-11. `partbuddy_mechanism {kind: snap}` stands cantilever hooks on one body (`plastic.snap_fit`: arm 12 × 1.5 × 5 mm, catch 0.8 mm on its last 1.5 mm, 1.4 % strain at the root by the Bayer/BASF formula) and cuts each one's window through the other body's wall: 0.2 mm between arm and wall, 0.3 mm beside the arm, the edge 0.2 mm under the catch, 0.5 mm of air over the tip (guides for printed parts run from 0.1 to 0.5 mm). The catch's underside is square, so it opens only by pressing the catch back through its window. `from` is the hook's foot at the wall's inner face, `up` the way it stands, `to` a point in the wall, `more` further feet; no joint. Not printed yet. Not built: a lead-in chamfer on the wall's edge, tapered arms, other sizes of hook, a strain limit by material (no primary figures for PETG and ASA). | +| F1e Standards to look up (done) | Decided 2026-11-11 with the product owner: the remaining kinds of the standards list get no fixed block, since their shape depends on the part and only the rules stay; a block is kept for what two parts must match in or SolidCraft is tricky at. `partbuddy_standard {topic}` (`standards.ts`, `standardTool.ts`) gives the assistant the rules and sizes of a kind before it designs one itself: sliding lid, clip and clamp, feet (3M Bumpon sizes), hooks and eyes (measured breaking loads, cable-tie widths), cable entry (cable diameters, gland holes of EN 60423), vents and fans (hole spacings, IP2X), board mounts (Raspberry Pi and Arduino hole patterns), star knob (DIN 6336, ISO 4017 heads). Each rule says whether its figure is from a sheet, a range of guides or our own choice. It builds nothing and is not in the work log; the role prompt only names the topics. Nothing is printed, and what the assistant builds from it is not tested against SolidCraft. | | F1d Magnet pockets (done) | Standards, 2026-11-11. `partbuddy_block {block: magnet, body, point, normal}` cuts pockets for disc magnets (`magnet.ts`), 6 × 2 mm by default (the magnet of Gridfinity): 0.3 mm wider than the magnet to glue it in (the default; magnets are made to ±0.1 mm) or 0.1 mm to press it in (`hold`), 0.2 mm deeper so it lies under the face, the mouth chamfered by 0.4 mm; `more` for further pockets. The answer reminds of the walls (1.2 mm round, 0.6 mm under), the poles of a pair and the 80 °C magnets bear. Published room runs from none to 0.5 mm (Gridfinity's plain hole); nothing is printed yet. Not built: crush ribs, a slot to push the magnet out, magnets slid in from the side or printed over at a pause, the bridged hole top for pockets that open downwards. | | F1c Knob for a shaft (done) | Standards, 2026-11-11. `partbuddy_block {block: knob, body, point}` makes a knob as a part of its own (`knob.ts`): 25 × 16 mm with flutes every 7 mm, a chamfered plain band at the top and a groove as the pointer; a bore 11 mm deep for a 6 mm shaft with 0.2 mm of room, and by `shaft` the flat of a D shaft over its last 7 mm (4.5 mm on 6 mm, Bourns PEC11R and Alps RK09; any other shaft needs `flat` as measured, the sources disagree on 1/4"), a plain bore for a split knurled shaft, or a 2.6 mm pilot hole for an M3 set screw on a round one. Flat and set screw lie on the side away from the pointer (own choice). It prints on its top without support and sets its print face. Nothing of it is printed yet; `clearance` is the lever. Not built: star and clamping knobs (DIN 6336), modelled knurl teeth, a nut for the set screw. | | F1b Function blocks for the assistant (done) | Decided 2026-11-11 with the standards the product owner asked for: the assistant takes sizes from tables, not from memory. `partbuddy_block {block, body, point}` builds the blocks of F1 with the same standard values the form starts from (clearance by ISO 273, heads by ISO 4762 and ISO 10642, nuts by ISO 4032, common heat-set inserts): screw hole, tapped hole, insert seat, nut trap, boss, slot, text, lip, snap fit; the model gives only what differs. Two blocks for hanging a part on a wall come with it (`wallMount.ts`; not in the form yet): `keyhole`, a keyhole slot on the face against the wall for a 3.5, 4, 4.5 or 5 mm wood screw (entry for the head, a slot of the thread's width running 8 mm up through a 2 mm lip, a pocket for the head behind it; printed with that face on the bed the pocket's roof is a bridge), and `woodScrew`, a countersunk hole after DIN 74 with 0.2 mm more for print. The lip's 2 mm are PartBuddy's own choice: no source gives a rule. | From 8ce746fd8d9ada008e162484ef77dd2025a341d9 Mon Sep 17 00:00:00 2001 From: ManuelStaggl <17688572+ManuelStaggl@users.noreply.github.com> Date: Wed, 11 Nov 2026 18:36:30 +0100 Subject: [PATCH 2/2] test(desktop): read the paused joint once it stands The gear test read the driver's position a frame before the pause took hold and then expected that value to be kept (39.2 read, 39.1 kept). Co-Authored-By: Claude Fable 5.2 --- apps/desktop/e2e/app.spec.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/desktop/e2e/app.spec.ts b/apps/desktop/e2e/app.spec.ts index 3eb2353..bb9805a 100644 --- a/apps/desktop/e2e/app.spec.ts +++ b/apps/desktop/e2e/app.spec.ts @@ -793,7 +793,15 @@ test('a gear\'s mate follows its driver in the panel, the playback and what is k await panel.getByTestId('joint-play').click(); await expect.poll(async () => Math.abs(await shown('Small axle')()), { timeout: 30_000 }).toBeGreaterThan(10); await panel.getByTestId('joint-play').click(); - const driver = await shown('Small axle')(); + // The pause takes a frame: read the position once it stands (found 2026-11-11: read 39.2, kept 39.1). + let driver = Number.NaN; + await expect + .poll(async () => { + const before = driver; + driver = await shown('Small axle')(); + return driver === before; + }) + .toBe(true); expect(await shown('Large axle (follows Small axle)')()).toBeCloseTo(-0.5 * driver, 1); await page.getByTestId('viewer').screenshot({ path: test.info().outputPath('gear-paused.png') }); expect(fake.joints.map((joint) => joint.values[0]?.value)).toEqual([0, 0]);