fix: correct HiCAD exploded previews and BOM templates

This commit is contained in:
134 2026-07-27 21:36:46 +08:00 committed by root
parent 46f1d97593
commit dc7f58d940
3 changed files with 116 additions and 10 deletions

View File

@ -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({

View File

@ -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 : '工程数据分析失败'

View File

@ -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) {