feat: generate complete front and side part drawings
This commit is contained in:
parent
b5b5398b30
commit
2c81486862
@ -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=='
|
||||
|
||||
114
vendor/zoo-modeling-app/manufacturing.ts
vendored
114
vendor/zoo-modeling-app/manufacturing.ts
vendored
@ -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 `
|
||||
<line class="hidden-line" x1="${roundDimension(objectX)}"
|
||||
y1="${roundDimension(centerY - radius)}"
|
||||
x2="${roundDimension(objectX + objectWidth)}"
|
||||
y2="${roundDimension(centerY - radius)}"/>
|
||||
<line class="hidden-line" x1="${roundDimension(objectX)}"
|
||||
y1="${roundDimension(centerY + radius)}"
|
||||
x2="${roundDimension(objectX + objectWidth)}"
|
||||
y2="${roundDimension(centerY + radius)}"/>`
|
||||
})
|
||||
.join('')
|
||||
|
||||
return `
|
||||
<svg class="part-side-view" viewBox="0 0 250 245" role="img"
|
||||
aria-label="${escapeHtml(row.name)} 侧面尺寸图">
|
||||
<defs>
|
||||
<marker id="side-arrow-${row.itemNumber}" viewBox="0 0 10 10" refX="5" refY="5"
|
||||
markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#111827"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="125" y="18" text-anchor="middle" font-weight="700">侧面图</text>
|
||||
<rect x="${roundDimension(objectX)}" y="${roundDimension(objectY)}"
|
||||
width="${roundDimension(objectWidth)}" height="${roundDimension(
|
||||
objectHeight
|
||||
)}" fill="#f9fafb" stroke="#111827" stroke-width="1.6"/>
|
||||
${hiddenHoleMarkup}
|
||||
<line x1="${roundDimension(objectX)}" y1="${roundDimension(
|
||||
objectY + objectHeight + 22
|
||||
)}" x2="${roundDimension(objectX + objectWidth)}"
|
||||
y2="${roundDimension(objectY + objectHeight + 22)}" stroke="#111827"
|
||||
marker-start="url(#side-arrow-${row.itemNumber})"
|
||||
marker-end="url(#side-arrow-${row.itemNumber})"/>
|
||||
<line x1="${roundDimension(objectX)}" y1="${roundDimension(
|
||||
objectY + objectHeight
|
||||
)}" x2="${roundDimension(objectX)}" y2="${roundDimension(
|
||||
objectY + objectHeight + 30
|
||||
)}" stroke="#6b7280"/>
|
||||
<line x1="${roundDimension(objectX + objectWidth)}" y1="${roundDimension(
|
||||
objectY + objectHeight
|
||||
)}" x2="${roundDimension(objectX + objectWidth)}" y2="${roundDimension(
|
||||
objectY + objectHeight + 30
|
||||
)}" stroke="#6b7280"/>
|
||||
<text x="${roundDimension(objectX + objectWidth / 2)}"
|
||||
y="${roundDimension(objectY + objectHeight + 39)}"
|
||||
text-anchor="middle">${formatDimension(horizontal)}</text>
|
||||
<line x1="${roundDimension(objectX - 22)}" y1="${roundDimension(objectY)}"
|
||||
x2="${roundDimension(objectX - 22)}" y2="${roundDimension(
|
||||
objectY + objectHeight
|
||||
)}" stroke="#111827" marker-start="url(#side-arrow-${row.itemNumber})"
|
||||
marker-end="url(#side-arrow-${row.itemNumber})"/>
|
||||
<line x1="${roundDimension(objectX - 30)}" y1="${roundDimension(objectY)}"
|
||||
x2="${roundDimension(objectX)}" y2="${roundDimension(objectY)}"
|
||||
stroke="#6b7280"/>
|
||||
<line x1="${roundDimension(objectX - 30)}" y1="${roundDimension(
|
||||
objectY + objectHeight
|
||||
)}" x2="${roundDimension(objectX)}" y2="${roundDimension(
|
||||
objectY + objectHeight
|
||||
)}" stroke="#6b7280"/>
|
||||
<text x="${roundDimension(objectX - 37)}"
|
||||
y="${roundDimension(objectY + objectHeight / 2)}" text-anchor="middle"
|
||||
transform="rotate(-90 ${roundDimension(
|
||||
objectX - 37
|
||||
)} ${roundDimension(objectY + objectHeight / 2)})">${formatDimension(
|
||||
vertical
|
||||
)}</text>
|
||||
</svg>`
|
||||
}
|
||||
|
||||
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) {
|
||||
<span>数量 ${row.quantity} · 厚度 ${formatDimension(depth)}</span>
|
||||
</div>
|
||||
<div class="part-detail-layout">
|
||||
<svg viewBox="0 0 390 245" role="img" aria-label="${escapeHtml(
|
||||
<svg class="part-front-view" viewBox="0 0 390 245" role="img" aria-label="${escapeHtml(
|
||||
row.name
|
||||
)} 加工尺寸图">
|
||||
<defs>
|
||||
@ -988,6 +1093,7 @@ function machiningDetailSvg(row: BomRow) {
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#111827"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="195" y="18" text-anchor="middle" font-weight="700">正面图</text>
|
||||
<rect x="${roundDimension(objectX)}" y="${roundDimension(objectY)}"
|
||||
width="${roundDimension(objectWidth)}" height="${roundDimension(
|
||||
objectHeight
|
||||
@ -1033,6 +1139,7 @@ function machiningDetailSvg(row: BomRow) {
|
||||
vertical
|
||||
)}</text>
|
||||
</svg>
|
||||
${sideDimensionSvg(row)}
|
||||
<div class="hole-schedule">
|
||||
<strong>孔位与加工基准</strong>
|
||||
<ul>${holeNotes}</ul>
|
||||
@ -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; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user