From 5fadb1ac613d5f7ff8b38976778a5dd963a0af29 Mon Sep 17 00:00:00 2001 From: 134 Date: Mon, 27 Jul 2026 21:47:45 +0800 Subject: [PATCH] fix: render HiCAD orthographic views and Chinese BOM names --- .../hicad-manufacturing-extension.test.ts | 13 +- .../HicadManufacturingPanel.tsx.template | 15 +- vendor/zoo-modeling-app/manufacturing.ts | 142 ++++++++++++++++-- 3 files changed, 151 insertions(+), 19 deletions(-) diff --git a/tests/routes/hicad-manufacturing-extension.test.ts b/tests/routes/hicad-manufacturing-extension.test.ts index 5e0ec8271..60bcaa673 100644 --- a/tests/routes/hicad-manufacturing-extension.test.ts +++ b/tests/routes/hicad-manufacturing-extension.test.ts @@ -107,7 +107,7 @@ test.describe('HiCAD manufacturing extension', () => { test('groups matching parts and exports a UTF-8 BOM', () => { const rows = groupBodiesIntoBom(bodies) - const legRow = rows.find((row) => row.name === 'leg') + const legRow = rows.find((row) => row.name === '支腿') expect(rows).toHaveLength(2) expect(legRow?.quantity).toBe(2) @@ -148,7 +148,8 @@ test.describe('HiCAD manufacturing extension', () => { expect(filtered).toHaveLength(5) expect(rows).toHaveLength(2) - expect(rows.find((row) => row.name === 'Leg')?.quantity).toBe(4) + expect(rows.find((row) => row.name === '支腿')?.quantity).toBe(4) + expect(rows.find((row) => row.name === '座板')?.quantity).toBe(1) expect(rows.some((row) => row.name === 'unitBox')).toBe(false) }) @@ -167,6 +168,7 @@ test.describe('HiCAD manufacturing extension', () => { projectName: '<工作台>', generatedAt: '2026-07-27 20:00', overallDimensions: { x: 120, y: 60, z: 92 }, + bodies, brandName: '由壹点AI', brandLogoUrl: '/api/favicon?surface=hicad', snapshotDataUrl: 'data:image/png;base64,abc', @@ -178,6 +180,9 @@ test.describe('HiCAD manufacturing extension', () => { expect(html).toContain('正视图') expect(html).toContain('俯视图') expect(html).toContain('右视图') + expect(html.match(/class="projection-part"/g)).toHaveLength(9) + expect(html).toContain('data-part-name="支腿"') + expect(html).toContain('data-part-name="台面板"') expect(html).toContain('120 mm') expect(html).toContain('配件明细表(BOM)') expect(html).toContain('由壹点AI') @@ -193,6 +198,7 @@ test.describe('HiCAD manufacturing extension', () => { projectName: '工作台生产图', generatedAt: '2026-07-27 20:00', overallDimensions: { x: 120, y: 60, z: 92 }, + bodies, brandName: '由壹点AI', brandLogoUrl: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="80" height="80"%3E%3Crect width="80" height="80" fill="%23111827"/%3E%3Cpath d="M20 20h40v40H20z" fill="white"/%3E%3C/svg%3E', @@ -209,12 +215,15 @@ test.describe('HiCAD manufacturing extension', () => { clientWidth: document.documentElement.clientWidth, tableCount: document.querySelectorAll('table').length, dimensionViewCount: document.querySelectorAll('.dimension-view').length, + projectionPartCount: + document.querySelectorAll('.projection-part').length, brandCount: document.querySelectorAll('.brand-name').length, })) expect(layout.scrollWidth).toBeLessThanOrEqual(layout.clientWidth) expect(layout.tableCount).toBe(1) expect(layout.dimensionViewCount).toBe(3) + expect(layout.projectionPartCount).toBe(bodies.length * 3) expect(layout.brandCount).toBe(1) await page.screenshot({ path: testInfo.outputPath('engineering-sheet.png'), diff --git a/vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template b/vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template index bc64270f4..86da8e8e5 100644 --- a/vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template +++ b/vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template @@ -27,6 +27,7 @@ type ModelingResponse = { type BomOverrides = Record< string, { + name?: string material?: string process?: string } @@ -536,7 +537,7 @@ export function HicadManufacturingPanel({ const updateRow = ( row: BomRow, - field: 'material' | 'process', + field: 'name' | 'material' | 'process', value: string ) => { setOverrides((current) => ({ @@ -577,6 +578,7 @@ export function HicadManufacturingPanel({ projectName: resolvedProjectName, generatedAt: new Date().toLocaleString('zh-CN'), overallDimensions: overall.dimensions, + bodies, brandName: brand.name, brandLogoUrl: brand.logoUrl, snapshotDataUrl: snapshot, @@ -731,7 +733,16 @@ export function HicadManufacturingPanel({ className="border-b border-chalkboard-30 last:border-b-0 dark:border-chalkboard-70" > {row.itemNumber} - {row.name} + + + updateRow(row, 'name', event.target.value) + } + aria-label={`零件名称 ${row.itemNumber}`} + className="h-8 w-36 border border-chalkboard-30 bg-transparent px-2 font-medium dark:border-chalkboard-70" + /> + {row.quantity} {formatNumber(row.dimensions.x)} ×{' '} diff --git a/vendor/zoo-modeling-app/manufacturing.ts b/vendor/zoo-modeling-app/manufacturing.ts index 7f20c6404..002d466b3 100644 --- a/vendor/zoo-modeling-app/manufacturing.ts +++ b/vendor/zoo-modeling-app/manufacturing.ts @@ -35,6 +35,7 @@ export type EngineeringSheetInput = { projectName: string generatedAt: string overallDimensions: Point3d + bodies?: BodyMeasurement[] brandName?: string brandLogoUrl?: string snapshotDataUrl?: string @@ -209,6 +210,44 @@ function normalizePartName(name: string) { return locationLegMatch?.[1] ?? withoutCopySuffix } +const CHINESE_PART_NAMES: Record = { + backpanel: '背板', + base: '底座', + baseboard: '底板', + bottompanel: '底板', + bracket: '支架', + dividerpanel: '隔板', + doorpanel: '门板', + dowel: '木榫', + drawer: '抽屉', + drawerfront: '抽屉面板', + foot: '支腿', + handle: '拉手', + hinge: '铰链', + leftsidepanel: '左侧板', + leg: '支腿', + rightsidepanel: '右侧板', + screw: '螺钉', + seat: '座板', + seatboard: '座板', + seatbody: '座板', + shelf: '层板', + shelfpanel: '层板', + sidepanel: '侧板', + support: '支撑件', + tablebody: '桌体', + tabletop: '台面板', + topboard: '顶板', + topbottompanel: '顶底板', + toppanel: '顶板', +} + +export function localizePartName(name: string) { + const cleaned = name.trim() || '未命名零件' + if (/[\u3400-\u9fff]/.test(cleaned)) return cleaned + return CHINESE_PART_NAMES[compactPartName(cleaned)] ?? cleaned +} + function dimensionsKey(dimensions: Point3d) { return [dimensions.x, dimensions.y, dimensions.z] .map((value) => roundDimension(value).toFixed(2)) @@ -235,8 +274,11 @@ export function groupBodiesIntoBom(bodies: BodyMeasurement[]): BomRow[] { const groups = new Map() for (const body of bodies) { - const name = normalizePartName(body.name) - const key = `${name.toLowerCase()}::${dimensionsKey(body.dimensions)}` + const normalizedName = normalizePartName(body.name) + const name = localizePartName(normalizedName) + const key = `${normalizedName.toLowerCase()}::${dimensionsKey( + body.dimensions + )}` const existing = groups.get(key) if (existing) { @@ -317,19 +359,79 @@ function formatDimension(value: number) { return `${roundDimension(value)} mm` } -function dimensionViewSvg( +function projectedBodyRect( + body: BodyMeasurement, + horizontalAxis: keyof Point3d, + verticalAxis: keyof Point3d, + originX: number, + originY: number, + scale: number, + minHorizontal: number, + minVertical: number, + projectedHeight: number +) { + const horizontalStart = + body.center[horizontalAxis] - body.dimensions[horizontalAxis] / 2 + const verticalStart = + body.center[verticalAxis] - body.dimensions[verticalAxis] / 2 + const rectX = originX + (horizontalStart - minHorizontal) * scale + const rectWidth = Math.max(body.dimensions[horizontalAxis] * scale, 1) + const rectHeight = Math.max(body.dimensions[verticalAxis] * scale, 1) + const rectY = + originY + + projectedHeight - + (verticalStart - minVertical) * scale - + rectHeight + + return `` +} + +function orthographicViewSvg( label: string, - horizontal: number, - vertical: number, + bodies: BodyMeasurement[], + horizontalAxis: keyof Point3d, + verticalAxis: keyof Point3d, width = 310, height = 210 ) { - const objectX = 72 - const objectY = 40 - const objectWidth = 180 - const objectHeight = 112 + const overall = getOverallBounds(bodies) + const horizontal = overall.dimensions[horizontalAxis] + const vertical = overall.dimensions[verticalAxis] + const maxObjectWidth = 180 + const maxObjectHeight = 112 + const scale = Math.min( + maxObjectWidth / Math.max(horizontal, EPSILON), + maxObjectHeight / Math.max(vertical, EPSILON) + ) + const objectWidth = Math.max(horizontal * scale, 1) + const objectHeight = Math.max(vertical * scale, 1) + const objectX = 72 + (maxObjectWidth - objectWidth) / 2 + const objectY = 40 + (maxObjectHeight - objectHeight) / 2 + const minHorizontal = + overall.center[horizontalAxis] - overall.dimensions[horizontalAxis] / 2 + const minVertical = + overall.center[verticalAxis] - overall.dimensions[verticalAxis] / 2 const horizontalText = escapeHtml(formatDimension(horizontal)) const verticalText = escapeHtml(formatDimension(vertical)) + const projectedBodies = bodies + .map((body) => + projectedBodyRect( + body, + horizontalAxis, + verticalAxis, + objectX, + objectY, + scale, + minHorizontal, + minVertical, + objectHeight + ) + ) + .join('') return `
@@ -340,8 +442,7 @@ function dimensionViewSvg( - + ${projectedBodies} 0 + ? input.bodies + : [ + { + id: 'overall', + name: '总体', + center: { x: 0, y: 0, z: 0 }, + dimensions, + }, + ] const rows = input.bomRows .map( (row) => ` @@ -454,11 +566,11 @@ export function createEngineeringSheetHtml(input: EngineeringSheetInput) {
-

总体三视图与尺寸

+

实体正投影三视图与尺寸

- ${dimensionViewSvg('正视图', dimensions.x, dimensions.z)} - ${dimensionViewSvg('俯视图', dimensions.x, dimensions.y)} - ${dimensionViewSvg('右视图', dimensions.y, dimensions.z)} + ${orthographicViewSvg('正视图', projectionBodies, 'x', 'z')} + ${orthographicViewSvg('俯视图', projectionBodies, 'x', 'y')} + ${orthographicViewSvg('右视图', projectionBodies, 'y', 'z')}