diff --git a/tests/routes/hicad-manufacturing-extension.test.ts b/tests/routes/hicad-manufacturing-extension.test.ts index 184ea3bf5..43e8197b9 100644 --- a/tests/routes/hicad-manufacturing-extension.test.ts +++ b/tests/routes/hicad-manufacturing-extension.test.ts @@ -8,6 +8,7 @@ import { createSnapshotDataUrl, detectPanelHoles, filterManufacturingBodies, + getOverallBounds, groupBodiesIntoBom, localizePartName, type BodyMeasurement, @@ -286,6 +287,10 @@ test.describe('HiCAD manufacturing extension', () => { expect(html).toContain('data-generated-exploded-view="true"') expect(html.match(/class="explode-part"/g)).toHaveLength(10) expect(html).not.toContain('暂无爆炸图快照') + expect(html.match(/class="part-front-view"/g)).toHaveLength(4) + expect(html.match(/class="part-side-view"/g)).toHaveLength(4) + expect(html).toContain('正面图') + expect(html).toContain('侧面图') await page.setViewportSize({ width: 1600, height: 900 }) await page.setContent(html) @@ -307,6 +312,31 @@ test.describe('HiCAD manufacturing extension', () => { }) }) + test('renders every BOM part without limiting the number of process drawings', () => { + const manyParts: BodyMeasurement[] = Array.from( + { length: 7 }, + (_, index) => ({ + id: `custom-part-${index + 1}`, + name: `自定义零件${index + 1}`, + center: { x: index * 30, y: 0, z: 0 }, + dimensions: { x: 20 + index, y: 12 + index, z: 4 + index }, + }) + ) + const rows = groupBodiesIntoBom(manyParts) + const html = createEngineeringSheetHtml({ + projectName: '多配件生产图', + generatedAt: '2026-07-27 23:00', + overallDimensions: getOverallBounds(manyParts).dimensions, + bodies: manyParts, + bomRows: rows, + }) + + expect(rows).toHaveLength(7) + expect(html.match(/class="part-detail"/g)).toHaveLength(7) + expect(html.match(/class="part-front-view"/g)).toHaveLength(7) + expect(html.match(/class="part-side-view"/g)).toHaveLength(7) + }) + 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 7947f5a6a..bf7e4d068 100644 --- a/vendor/zoo-modeling-app/manufacturing.ts +++ b/vendor/zoo-modeling-app/manufacturing.ts @@ -881,6 +881,111 @@ function uniqueRounded(values: number[]) { .sort((left, right) => left - right) } +function sideDimensionSvg(row: BomRow) { + const axes = panelAxes(row.dimensions) + const horizontal = Math.abs(row.dimensions[axes.depth]) + const vertical = Math.abs(row.dimensions[axes.vertical]) + const maxObjectWidth = 112 + const maxObjectHeight = 145 + const scale = Math.min( + maxObjectWidth / Math.max(horizontal, EPSILON), + maxObjectHeight / Math.max(vertical, EPSILON) + ) + const objectWidth = Math.max(horizontal * scale, 1.5) + const objectHeight = Math.max(vertical * scale, 1.5) + const objectX = 68 + (maxObjectWidth - objectWidth) / 2 + const objectY = 42 + (maxObjectHeight - objectHeight) / 2 + const hiddenHoleRows = new Map< + string, + { center: number; diameter: number } + >() + + row.holes + .filter((hole) => hole.axis === axes.depth) + .forEach((hole) => { + const center = + hole.center[axes.vertical] - + row.center[axes.vertical] + + vertical / 2 + hiddenHoleRows.set( + `${roundDimension(center)}:${roundDimension(hole.diameter)}`, + { center, diameter: hole.diameter } + ) + }) + + const hiddenHoleMarkup = [...hiddenHoleRows.values()] + .map(({ center, diameter }) => { + const centerY = objectY + objectHeight - center * scale + const radius = (diameter / 2) * scale + return ` + + ` + }) + .join('') + + return ` + + + + + + + 侧面图 + + ${hiddenHoleMarkup} + + + + ${formatDimension(horizontal)} + + + + ${formatDimension( + vertical + )} + ` +} + function machiningDetailSvg(row: BomRow) { const axes = panelAxes(row.dimensions) const horizontal = Math.abs(row.dimensions[axes.horizontal]) @@ -979,7 +1084,7 @@ function machiningDetailSvg(row: BomRow) { 数量 ${row.quantity} · 厚度 ${formatDimension(depth)}
- @@ -988,6 +1093,7 @@ function machiningDetailSvg(row: BomRow) { + 正面图 孔位与加工基准
    ${holeNotes}
@@ -1122,13 +1229,14 @@ export function createEngineeringSheetHtml(input: EngineeringSheetInput) { th { background: #f3f4f6; } .bom { grid-column: 1 / -1; } .part-details { grid-column: 1 / -1; break-before: page; } - .part-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } + .part-detail-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; } .part-detail { border: 1px solid #6b7280; break-inside: avoid; } .part-detail-heading { display: flex; justify-content: space-between; gap: 12px; padding: 7px 9px; border-bottom: 1px solid #9ca3af; background: #f3f4f6; font-size: 12px; } - .part-detail-layout { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(190px, .8fr); gap: 8px; align-items: start; padding: 6px; } + .part-detail-layout { display: grid; grid-template-columns: minmax(0, 1.12fr) minmax(220px, .72fr) minmax(230px, .86fr); gap: 10px; align-items: start; padding: 6px; } .part-detail svg { width: 100%; display: block; font-size: 10px; } .detail-hole { fill: white; stroke: #111827; stroke-width: 1.5; } .center-line { stroke: #6b7280; stroke-width: .7; stroke-dasharray: 4 3; } + .hidden-line { stroke: #6b7280; stroke-width: .8; stroke-dasharray: 5 3; } .hole-schedule { padding: 8px 6px 6px 0; font-size: 10px; line-height: 1.5; } .hole-schedule ul { margin: 5px 0 8px; padding-left: 18px; } .hole-schedule p { margin: 5px 0; }