diff --git a/tests/routes/hicad-manufacturing-extension.test.ts b/tests/routes/hicad-manufacturing-extension.test.ts index d4f850a28..5e0ec8271 100644 --- a/tests/routes/hicad-manufacturing-extension.test.ts +++ b/tests/routes/hicad-manufacturing-extension.test.ts @@ -5,6 +5,8 @@ import { buildExplodeOffsets, createBomCsv, createEngineeringSheetHtml, + createSnapshotDataUrl, + filterManufacturingBodies, groupBodiesIntoBom, type BodyMeasurement, } from '../../vendor/zoo-modeling-app/manufacturing' @@ -117,6 +119,48 @@ test.describe('HiCAD manufacturing extension', () => { expect(csv).toContain('"生产工艺"') }) + test('filters helper solids and groups directional leg names', () => { + const stoolBodies: BodyMeasurement[] = [ + { + id: 'template', + name: 'unitBox', + center: { x: 0, y: 0, z: 0 }, + dimensions: { x: 1, y: 1, z: 1 }, + }, + { + id: 'seat', + name: 'seatBody', + center: { x: 0, y: 0, z: 310 }, + dimensions: { x: 300, y: 220, z: 20 }, + }, + ...['frontLeftLeg', 'frontRightLeg', 'backLeftLeg', 'backRightLeg'].map( + (name, index): BodyMeasurement => ({ + id: `leg-${index}`, + name, + center: { x: index % 2 ? 100 : -100, y: index < 2 ? -70 : 70, z: 150 }, + dimensions: { x: 30, y: 30, z: 300 }, + }) + ), + ] + + const filtered = filterManufacturingBodies(stoolBodies) + const rows = groupBodiesIntoBom(filtered) + + expect(filtered).toHaveLength(5) + expect(rows).toHaveLength(2) + expect(rows.find((row) => row.name === 'Leg')?.quantity).toBe(4) + expect(rows.some((row) => row.name === 'unitBox')).toBe(false) + }) + + test('normalizes URL-safe engine snapshots for browser image rendering', () => { + expect(createSnapshotDataUrl('iVBORw0KGgo_8A')).toBe( + 'data:image/png;base64,iVBORw0KGgo/8A==' + ) + expect( + createSnapshotDataUrl('data:image/png;base64,iVBORw0KGgo_8A') + ).toBe('data:image/png;base64,iVBORw0KGgo/8A==') + }) + test('builds a printable dimensioned engineering sheet safely', () => { const rows = groupBodiesIntoBom(bodies) const html = createEngineeringSheetHtml({ diff --git a/vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template b/vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template index c334ac39f..bc64270f4 100644 --- a/vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template +++ b/vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template @@ -8,8 +8,10 @@ import { useEffect, useMemo, useRef, useState } from 'react' import toast from 'react-hot-toast' import { buildExplodeOffsets, + createSnapshotDataUrl, createBomCsv, createEngineeringSheetHtml, + filterManufacturingBodies, getOverallBounds, groupBodiesIntoBom, type BodyMeasurement, @@ -128,12 +130,6 @@ function formatNumber(value: number | undefined) { return Math.round(value * 100) / 100 } -function dataUrlFromSnapshot(contents: string) { - return contents.startsWith('data:') - ? contents - : `data:image/png;base64,${contents}` -} - export function HicadManufacturingPanel({ hideTrigger = false, initialOpen = false, @@ -274,7 +270,7 @@ export function HicadManufacturingPanel({ ) { return undefined } - const dataUrl = dataUrlFromSnapshot(response.data.contents) + const dataUrl = createSnapshotDataUrl(response.data.contents) setSnapshot(dataUrl) return dataUrl } @@ -411,7 +407,13 @@ export function HicadManufacturingPanel({ ) } - setBodies(measured) + const manufacturingBodies = filterManufacturingBodies(measured) + if (manufacturingBodies.length < measured.length) { + toast( + `已忽略 ${measured.length - manufacturingBodies.length} 个建模模板实体。` + ) + } + setBodies(manufacturingBodies) await Promise.allSettled([ engineViewIsometric({ engineCommandManager: kclManager.engineCommandManager, @@ -431,7 +433,7 @@ export function HicadManufacturingPanel({ error ) } - return measured + return manufacturingBodies } catch (error) { const message = error instanceof Error ? error.message : '工程数据分析失败' diff --git a/vendor/zoo-modeling-app/manufacturing.ts b/vendor/zoo-modeling-app/manufacturing.ts index e15c3d27d..7f20c6404 100644 --- a/vendor/zoo-modeling-app/manufacturing.ts +++ b/vendor/zoo-modeling-app/manufacturing.ts @@ -48,6 +48,61 @@ function roundDimension(value: number) { return Math.round(value * 100) / 100 } +function maximumDimension(body: BodyMeasurement) { + return Math.max( + Math.abs(body.dimensions.x), + Math.abs(body.dimensions.y), + Math.abs(body.dimensions.z) + ) +} + +function compactPartName(name: string) { + return name.trim().replaceAll(/[\s_-]+/g, '').toLowerCase() +} + +function isKnownTemplateBody(body: BodyMeasurement) { + return /^unit(?:box|cube|cylinder[xyz]?|righttriangle[xyz]?)$/.test( + compactPartName(body.name) + ) +} + +export function filterManufacturingBodies(bodies: BodyMeasurement[]) { + if (bodies.length <= 1) return bodies + + const hasProductionScaleBody = bodies.some( + (body) => maximumDimension(body) >= 20 + ) + const filtered = bodies.filter((body) => { + if (isKnownTemplateBody(body)) return false + return !(hasProductionScaleBody && maximumDimension(body) <= 2) + }) + + return filtered.length > 0 ? filtered : bodies +} + +function normalizeBase64Payload(value: string) { + const normalized = value + .trim() + .replaceAll(/\s+/g, '') + .replaceAll('-', '+') + .replaceAll('_', '/') + return normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=') +} + +export function createSnapshotDataUrl(contents: string) { + const trimmed = contents.trim() + if (trimmed.startsWith('data:')) { + const commaIndex = trimmed.indexOf(',') + if (commaIndex < 0 || !trimmed.slice(0, commaIndex).includes(';base64')) { + return trimmed + } + return `${trimmed.slice(0, commaIndex + 1)}${normalizeBase64Payload( + trimmed.slice(commaIndex + 1) + )}` + } + return `data:image/png;base64,${normalizeBase64Payload(trimmed)}` +} + function vectorLength(point: Point3d) { return Math.hypot(point.x, point.y, point.z) } @@ -146,7 +201,12 @@ export function buildExplodeOffsets( function normalizePartName(name: string) { const cleaned = name.trim() || '未命名零件' - return cleaned.replace(/(?:[_-]?(?:copy|part))?[_-]?\d+$/i, '').trim() || cleaned + const withoutCopySuffix = + cleaned.replace(/(?:[_-]?(?:copy|part))?[_-]?\d+$/i, '').trim() || cleaned + const locationLegMatch = withoutCopySuffix.match( + /^(?:front|back|rear)(?:left|right)(leg|foot)$/i + ) + return locationLegMatch?.[1] ?? withoutCopySuffix } function dimensionsKey(dimensions: Point3d) {