diff --git a/tests/routes/hicad-manufacturing-extension.test.ts b/tests/routes/hicad-manufacturing-extension.test.ts index 66907310e..35b960e22 100644 --- a/tests/routes/hicad-manufacturing-extension.test.ts +++ b/tests/routes/hicad-manufacturing-extension.test.ts @@ -9,6 +9,7 @@ import { detectPanelHoles, filterManufacturingBodies, groupBodiesIntoBom, + localizePartName, type BodyMeasurement, type CircularEdgeSample, } from '../../vendor/zoo-modeling-app/manufacturing' @@ -57,6 +58,78 @@ function circularEdge( } } +function hardwareAssemblyFixture() { + const holeCenters = [ + [-160, -100], + [160, -100], + [-160, 100], + [160, 100], + ] as const + const panel: BodyMeasurement = { + id: 'mounting-panel', + name: 'mountingPanel', + center: { x: 0, y: 0, z: 9 }, + dimensions: { x: 400, y: 300, z: 18 }, + holes: detectPanelHoles( + { + id: 'mounting-panel', + name: 'mountingPanel', + center: { x: 0, y: 0, z: 9 }, + dimensions: { x: 400, y: 300, z: 18 }, + }, + holeCenters.flatMap(([x, y]) => [ + circularEdge(x, y, 0, 4), + circularEdge(x, y, 18, 4), + ]) + ), + } + const plate: BodyMeasurement = { + id: 'connector-plate', + name: 'connectorPlate', + center: { x: 0, y: 0, z: 19.5 }, + dimensions: { x: 360, y: 260, z: 3 }, + holes: detectPanelHoles( + { + id: 'connector-plate', + name: 'connectorPlate', + center: { x: 0, y: 0, z: 19.5 }, + dimensions: { x: 360, y: 260, z: 3 }, + }, + holeCenters.flatMap(([x, y]) => [ + circularEdge(x, y, 18, 4), + circularEdge(x, y, 21, 4), + ]) + ), + } + const bolts = holeCenters.map( + ([x, y], index): BodyMeasurement => ({ + id: `connector-bolt-${index + 1}`, + name: `connectorBolt${index + 1}`, + center: { x, y, z: 11 }, + dimensions: { x: 8, y: 8, z: 30 }, + }) + ) + const washers = holeCenters.map( + ([x, y], index): BodyMeasurement => ({ + id: `washer-${index + 1}`, + name: `washer${index + 1}`, + center: { x, y, z: 22 }, + dimensions: { x: 18, y: 18, z: 2 }, + holes: [ + { + center: { x, y, z: 22 }, + diameter: 8, + axis: 'z', + type: 'through-hole', + depth: 2, + }, + ], + }) + ) + + return [panel, plate, ...bolts, ...washers] +} + test.describe('HiCAD manufacturing extension', () => { test('keeps the extension isolated behind one official registration point', () => { const projectRoot = process.cwd() @@ -172,6 +245,62 @@ test.describe('HiCAD manufacturing extension', () => { expect(rows.some((row) => row.name === 'unitBox')).toBe(false) }) + test('localizes the hardware validation assembly for a Chinese BOM', () => { + expect(localizePartName('mountingPanel')).toBe('安装基板') + expect(localizePartName('connectorPlate')).toBe('连接片') + expect(localizePartName('connectorBolt4')).toBe('连接螺栓') + expect(localizePartName('washer3')).toBe('垫圈') + }) + + test('builds a production-ready hardware BOM and detailed hole schedule', async ({ + page, + }, testInfo) => { + const assembly = hardwareAssemblyFixture() + const rows = groupBodiesIntoBom(assembly) + + expect(assembly).toHaveLength(10) + expect(rows).toHaveLength(4) + expect(rows.find((row) => row.name === '安装基板')?.holes).toHaveLength(4) + expect(rows.find((row) => row.name === '连接片')?.holes).toHaveLength(4) + expect(rows.find((row) => row.name === '连接螺栓')?.quantity).toBe(4) + expect(rows.find((row) => row.name === '垫圈')?.quantity).toBe(4) + + const html = createEngineeringSheetHtml({ + projectName: '五金连接安装板', + generatedAt: '2026-07-27 22:30', + overallDimensions: { x: 400, y: 300, z: 30 }, + bodies: assembly, + bomRows: rows, + }) + expect(html).toContain('安装基板') + expect(html).toContain('连接片') + expect(html).toContain('连接螺栓') + expect(html).toContain('垫圈') + expect(html).toContain('H1:Ø8 通孔') + expect(html).toContain('横向中心孔距 320 mm') + expect(html).toContain('纵向中心孔距 200 mm') + expect(html.match(/class="part-detail"/g)).toHaveLength(4) + + await page.setViewportSize({ width: 1600, height: 900 }) + await page.setContent(html) + expect( + await page.locator('.part-detail').evaluateAll((parts) => + parts.map((part) => part.textContent) + ) + ).toEqual( + expect.arrayContaining([ + expect.stringContaining('安装基板'), + expect.stringContaining('连接片'), + expect.stringContaining('连接螺栓'), + expect.stringContaining('垫圈'), + ]) + ) + await page.screenshot({ + path: testInfo.outputPath('hardware-engineering-sheet.png'), + fullPage: true, + }) + }) + test('normalizes URL-safe engine snapshots for browser image rendering', () => { expect(createSnapshotDataUrl('iVBORw0KGgo_8A')).toBe( 'data:image/png;base64,iVBORw0KGgo/8A==' diff --git a/vendor/zoo-modeling-app/manufacturing.ts b/vendor/zoo-modeling-app/manufacturing.ts index 988ae9312..1994f9fb3 100644 --- a/vendor/zoo-modeling-app/manufacturing.ts +++ b/vendor/zoo-modeling-app/manufacturing.ts @@ -437,6 +437,7 @@ const CHINESE_PART_NAMES: Record = { handle: '拉手', hinge: '铰链', metalbracket: '金属连接角码', + mountingpanel: '安装基板', leftsidepanel: '左侧板', leg: '支腿', rightsidepanel: '右侧板', @@ -458,7 +459,7 @@ const CHINESE_PART_NAMES: Record = { } export function localizePartName(name: string) { - const cleaned = name.trim() || '未命名零件' + const cleaned = normalizePartName(name.trim() || '未命名零件') if (/[\u3400-\u9fff]/.test(cleaned)) return cleaned return CHINESE_PART_NAMES[compactPartName(cleaned)] ?? cleaned }