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')}
|