diff --git a/apps/desktop/e2e/practice.spec.ts b/apps/desktop/e2e/practice.spec.ts index 03ea4ba..e519eb7 100644 --- a/apps/desktop/e2e/practice.spec.ts +++ b/apps/desktop/e2e/practice.spec.ts @@ -58,6 +58,15 @@ const PARTS: Part[] = [ 'Baue eine flache Dose 60 × 40 × 20 mm mit einem Deckel, der über ein Scharnier an einer langen Seite aufklappt. ' + 'Dose und Deckel sollen zusammen in einem Stück druckbar sein, ohne Stützmaterial.', }, + // The standards of 2026-11-11: no request names a tool; the work log shows whether the block or mechanism was taken, + // and for the last two the answer shows whether partbuddy_standard was read. + { id: '7-poti-knopf', request: 'Bau mir einen Drehknopf für ein Poti mit 6-mm-D-Welle.' }, + { id: '8-schnapphaken', request: 'Bau ein flaches Gehäuse 60 × 40 × 20 mm aus Boden und Haube, die mit Schnapphaken aufgeklipst wird.' }, + { id: '9-steckverbindung', request: 'Teile einen Block 60 × 30 × 20 mm in zwei Hälften, die ich zusammenstecken und verkleben kann.' }, + { id: '10-magnetdeckel', request: 'Bau einen Deckel 50 × 50 × 4 mm mit vier Taschen für 6×2-mm-Magnete in den Ecken.' }, + { id: '11-schnapplasche', request: 'Bau eine Dose 60 × 40 × 30 mm mit aufgesetztem Deckel und einem Verschluss außen an der Vorderseite.' }, + { id: '12-pi-gehaeuse', request: 'Bau ein Gehäuse für einen Raspberry Pi 4 mit Lüftungsschlitzen.' }, + { id: '13-rohrschelle', request: 'Bau eine Schelle für ein 22-mm-Rohr zum Aufschnappen.' }, ]; const PRINT_CHECK = diff --git a/apps/desktop/e2e/solidcraft.contract.spec.ts b/apps/desktop/e2e/solidcraft.contract.spec.ts index 18fa302..91f1fda 100644 --- a/apps/desktop/e2e/solidcraft.contract.spec.ts +++ b/apps/desktop/e2e/solidcraft.contract.spec.ts @@ -3382,7 +3382,7 @@ test('the assistant makes a knob for a D shaft with one call: a part of its own await expect(page.getByTestId('connection-status')).toHaveAttribute('data-engine', 'headless', { timeout: 30_000 }); await page.getByRole('textbox', { name: 'Message to the assistant' }).fill('Build a volume knob'); await page.getByRole('button', { name: 'Send' }).click(); - await expect(page.getByText(/Built the knob "Volume" for a 6 mm D shaft \(flat at 4\.5 mm\): the bore is 6\.2 mm wide and 11 mm deep, flat over its last 7 mm .* with 11 flutes to grip/)).toBeVisible({ timeout: 60_000 }); + await expect(page.getByText(/Built the knob "Volume" for a 6 mm D shaft \(flat at 4\.5 mm\): the bore is 6\.2 mm wide and 11 mm deep, flat over its last 7 mm .* with a chamfered mouth, to push on.* with 11 flutes to grip/)).toBeVisible({ timeout: 60_000 }); await expect(page.getByText(/SolidCraft warns:/)).toHaveCount(0); await expect(page.getByTestId('mesh-summary')).toContainText('1 body', { timeout: 30_000 }); await expect(page.getByText('Knob “Volume” for a shaft')).toHaveCount(1); @@ -3425,6 +3425,8 @@ test('the assistant latches a lid onto its box: the arm grows out of the lid and await expect(findings).toBeVisible(); await page.waitForTimeout(1500); await expect(findings.locator('[data-kind="overlap"]')).toHaveCount(0); + // Bead and hook end in a flat crest, not a knife edge the nozzle cannot print. + await expect(findings.locator('[data-kind="thinWall"]')).toHaveCount(0); await page.screenshot({ path: test.info().outputPath('latch-built.png') }); } finally { await launched.close(); diff --git a/apps/desktop/src/main/agents/blockTool.ts b/apps/desktop/src/main/agents/blockTool.ts index 25b1f6a..d43bc9c 100644 --- a/apps/desktop/src/main/agents/blockTool.ts +++ b/apps/desktop/src/main/agents/blockTool.ts @@ -188,7 +188,7 @@ export function knobOf(args: Record): { body: string; steps: St const insert = knobInsert(spec); const hold = shaft === 'd' - ? `for a ${String(shaftDiameter)} mm D shaft (flat at ${String(flat)} mm): the bore is ${String(bore)} mm wide and ${String(insert)} mm deep, flat over its last ${String(Math.min(KNOB.flat, insert))} mm on the side away from the pointer, to push on` + ? `for a ${String(shaftDiameter)} mm D shaft (flat at ${String(flat)} mm): the bore is ${String(bore)} mm wide and ${String(insert)} mm deep, flat over its last ${String(Math.min(KNOB.flat, insert))} mm on the side away from the pointer, with a chamfered mouth, to push on` : shaft === 'knurl' ? `for a split knurled shaft of ${String(shaftDiameter)} mm: a plain bore ${String(bore)} mm wide and ${String(insert)} mm deep, which the shaft springs into` : `for a round ${String(shaftDiameter)} mm shaft: the bore is ${String(bore)} mm wide and ${String(insert)} mm deep, and an M3 set screw cuts its own thread in the ${String(KNOB.setScrew)} mm hole on the side away from the pointer`; diff --git a/apps/desktop/src/main/agents/mechanismTool.ts b/apps/desktop/src/main/agents/mechanismTool.ts index e427275..54c53e6 100644 --- a/apps/desktop/src/main/agents/mechanismTool.ts +++ b/apps/desktop/src/main/agents/mechanismTool.ts @@ -480,7 +480,7 @@ async function buildMechanism(upstream: UpstreamTools, args: Record = { d: 0.2, round: 0.2, knurl: 0 }; @@ -106,6 +107,8 @@ export function knobSteps(spec: KnobSpec): Step[] { { command: 'solid.cylinder', params: { diameter: mm(KNOB.flute), height: mm(spec.height - KNOB.band), base: at(0, (radius + KNOB.flute / 4) * Math.cos(half), (radius + KNOB.flute / 4) * Math.sin(half)), axis: tidy(axis), operation: 'cut', targets: [name], name: `${name} ◆` } }, { command: 'solid.pattern.circular', params: { features: [`${name} ◆`], axis: { origin: at(0), dir: tidy(axis) }, count: flutes } }, { command: 'solid.cylinder', params: { diameter: mm(bore), height: mm(insert), base: at(0), axis: tidy(axis), operation: 'cut', targets: [name], name: `${name} ○` } }, + // The shaft finds its way in; printed on its top, the mouth is the last layer and needs no support. + { command: 'solid.cone', params: { diameter: mm(bore + 2 * KNOB.mouth), top_diameter: mm(bore), height: mm(KNOB.mouth), base: at(0), axis: tidy(axis), operation: 'cut', targets: [name], name: `${name} ◣` } }, ]; if (spec.shaft === 'd') { // The flat fills the bore's end again, on the side away from the pointer; the entry stays round, as the shaft is below its flat. diff --git a/apps/desktop/test/blockTool.test.ts b/apps/desktop/test/blockTool.test.ts index b396d1d..2e36df7 100644 --- a/apps/desktop/test/blockTool.test.ts +++ b/apps/desktop/test/blockTool.test.ts @@ -66,6 +66,7 @@ describe('a knob for a shaft (standards)', () => { ['solid.cylinder', 'Knob ◆'], ['solid.pattern.circular', ''], ['solid.cylinder', 'Knob ○'], + ['solid.cone', 'Knob ◣'], ['sketch.create', 'Knob ▬'], ['sketch.line', ''], ['solid.extrude', 'Knob ▬'], @@ -81,15 +82,17 @@ describe('a knob for a shaft (standards)', () => { // The bore: the shaft and 0.2 mm, 11 mm deep under 5 mm of roof. expect(steps[4]?.params).toMatchObject({ diameter: '6.2 mm', height: '11 mm', base: [0, 0, 0], operation: 'cut' }); // The flat over the bore's last 7 mm: sketch x runs to the pointer (+Y), and the flat lies at x = −(4.5 − 3 + 0.1). - expect(steps[5]?.params.plane).toEqual({ origin: [0, 0, 4], x_dir: [0, 1, 0], y_dir: [-1, 0, 0] }); - expect(steps[6]?.params).toEqual({ points: [[-1.6, -3.1], [-1.6, 3.1], [-3.6, 3.1], [-3.6, -3.1]], closed: true }); - expect(steps[7]?.params).toMatchObject({ distance: '7 mm', operation: 'join' }); + // Its mouth opens by 0.5 mm, so the shaft finds its way in. + expect(steps[5]?.params).toMatchObject({ diameter: '7.2 mm', top_diameter: '6.2 mm', height: '0.5 mm', base: [0, 0, 0], operation: 'cut' }); + expect(steps[6]?.params.plane).toEqual({ origin: [0, 0, 4], x_dir: [0, 1, 0], y_dir: [-1, 0, 0] }); + expect(steps[7]?.params).toEqual({ points: [[-1.6, -3.1], [-1.6, 3.1], [-3.6, 3.1], [-3.6, -3.1]], closed: true }); + expect(steps[8]?.params).toMatchObject({ distance: '7 mm', operation: 'join' }); // The pointer: a groove on the top from a third of the radius to the rim, towards +Y. - expect(steps[8]?.params.plane).toEqual({ origin: [0, 0, 16], x_dir: [-1, 0, 0], y_dir: [0, 1, 0] }); - expect(steps[9]?.params).toEqual({ p0: [0, 4.375], p1: [0, 12.5], width: 1.2 }); - expect(steps[10]?.params).toMatchObject({ distance: '0.6 mm', operation: 'cut' }); - expect(steps[11]?.params).toEqual({ body: 'Knob', point: [0, 0, 16] }); - expect(KNOB).toMatchObject({ roof: 3, wall: 2.4, insert: 11, flat: 7, setScrew: 2.6 }); + expect(steps[9]?.params.plane).toEqual({ origin: [0, 0, 16], x_dir: [-1, 0, 0], y_dir: [0, 1, 0] }); + expect(steps[10]?.params).toEqual({ p0: [0, 4.375], p1: [0, 12.5], width: 1.2 }); + expect(steps[11]?.params).toMatchObject({ distance: '0.6 mm', operation: 'cut' }); + expect(steps[12]?.params).toEqual({ body: 'Knob', point: [0, 0, 16] }); + expect(KNOB).toMatchObject({ roof: 3, wall: 2.4, insert: 11, flat: 7, mouth: 0.5, setScrew: 2.6 }); }); it('gives a round shaft a set screw from the side away from the pointer, and a knurled one a plain bore', () => { @@ -100,15 +103,16 @@ describe('a knob for a shaft (standards)', () => { const knurl = knobSteps({ ...knob, shaft: 'knurl', clearance: 0 }); expect(knurl.map((step) => step.command)).not.toContain('sketch.line'); expect(knurl[4]?.params).toMatchObject({ diameter: '6 mm' }); + expect(knurl[5]?.params).toMatchObject({ diameter: '7 mm', top_diameter: '6 mm' }); }); it('fills a knob in from the standard and refuses what cannot hold a shaft', () => { const built = knobOf({ block: 'knob', body: 'Volume', point: [0, 0, 0] }); expect(built).toMatchObject({ body: 'Volume', fresh: true }); - expect((built as { text: string }).text).toContain('Built the knob "Volume" for a 6 mm D shaft (flat at 4.5 mm): the bore is 6.2 mm wide and 11 mm deep, flat over its last 7 mm on the side away from the pointer, to push on. It is 25 mm wide and 16 mm high, with 11 flutes to grip'); + expect((built as { text: string }).text).toContain('Built the knob "Volume" for a 6 mm D shaft (flat at 4.5 mm): the bore is 6.2 mm wide and 11 mm deep, flat over its last 7 mm on the side away from the pointer, with a chamfered mouth, to push on. It is 25 mm wide and 16 mm high, with 11 flutes to grip'); expect((knobOf({ block: 'knob', body: 'Volume', point: [0, 0, 0], shaft: 'knurl' }) as { text: string }).text).toContain('a plain bore 6 mm wide and 11 mm deep'); // A shaft along Y takes its pointer up. - expect((knobOf({ block: 'knob', body: 'Volume', point: [0, 0, 0], normal: [0, 1, 0] }) as { steps: { params: { plane?: { y_dir: number[] } } }[] }).steps[8]?.params.plane?.y_dir).toEqual([0, 0, 1]); + expect((knobOf({ block: 'knob', body: 'Volume', point: [0, 0, 0], normal: [0, 1, 0] }) as { steps: { params: { plane?: { y_dir: number[] } } }[] }).steps[9]?.params.plane?.y_dir).toEqual([0, 0, 1]); expect(knobOf({ block: 'knob', body: 'Volume', point: [0, 0, 0], shaftDiameter: 6.35 })).toBe("a D shaft of 6.35 mm has no one flat: give flat, the shaft's thickness there, as measured"); expect(knobOf({ block: 'knob', body: 'Volume', point: [0, 0, 0], shaftDiameter: 6.35, flat: 5.54 })).toMatchObject({ body: 'Volume' }); expect(knobOf({ block: 'knob', body: 'Volume', point: [0, 0, 0], diameter: 10 })).toBe('diameter must be 13–80 mm for a 6 mm shaft');