fix: make HiCAD engineering analysis resilient

This commit is contained in:
134 2026-07-27 21:06:08 +08:00
parent c028d2ad00
commit 194762835f
2 changed files with 167 additions and 29 deletions

View File

@ -76,6 +76,18 @@ test.describe('HiCAD manufacturing extension', () => {
)
expect(launcher).toContain("lazy(() =>")
expect(launcher).toContain('ManufacturingBoundary')
const panel = fs.readFileSync(
path.join(
projectRoot,
'vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template'
),
'utf8'
)
expect(panel).toContain("type: 'scene_get_entity_ids'")
expect(panel).toContain("filter: ['solid3d']")
expect(panel).toContain("readBoundingBox([])")
expect(panel).toContain('engineFailureMessage(response)')
})
test('creates non-zero reversible offsets for every independent body', () => {

View File

@ -86,11 +86,37 @@ function resolveBodyName(
function modelingResponseData(response: unknown): ModelingResponse {
const normalized = Array.isArray(response) ? response[0] : response
if (!isModelingResponse(normalized)) {
throw new Error('HiCAD 引擎未返回有效结果')
throw new Error(engineFailureMessage(response))
}
return normalized.resp.data.modeling_response as ModelingResponse
}
function engineFailureMessage(response: unknown) {
const responses = Array.isArray(response) ? response : [response]
const messages = responses.flatMap((item) => {
if (item instanceof Error) return [item.message]
if (typeof item !== 'object' || item === null || !('errors' in item)) {
return []
}
const errors = (item as { errors?: unknown }).errors
if (!Array.isArray(errors)) return []
return errors.flatMap((error) => {
if (typeof error === 'string') return [error]
if (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof error.message === 'string'
) {
return [error.message]
}
return []
})
})
return messages.find((message) => message.trim()) ?? 'HiCAD 引擎未返回有效结果'
}
function numberValue(data: unknown, key: string) {
if (typeof data !== 'object' || data === null) return undefined
const value = (data as Record<string, unknown>)[key]
@ -175,7 +201,13 @@ export function HicadManufacturingPanel({
return modelingResponseData(normalized)
}
if (response !== null && response !== undefined) {
return modelingResponseData(response)
const message = engineFailureMessage(response)
console.error('[HiCAD manufacturing] Engine command failed', {
command: cmd.type,
message,
response,
})
throw new Error(message)
}
await new Promise((resolve) => setTimeout(resolve, 200))
@ -184,6 +216,50 @@ export function HicadManufacturingPanel({
throw new Error('HiCAD 引擎连接尚未就绪,请稍后重新分析')
}
const getSceneSolidIds = async () => {
const response = await sendModelingCommand({
type: 'scene_get_entity_ids',
filter: ['solid3d'],
skip: 0,
take: 1000,
})
if (
response.type !== 'scene_get_entity_ids' ||
typeof response.data !== 'object' ||
response.data === null ||
!('entity_ids' in response.data) ||
!Array.isArray(response.data.entity_ids)
) {
return []
}
return [
...new Set(
response.data.entity_ids.flatMap((ids) =>
Array.isArray(ids)
? ids.filter((id): id is string => typeof id === 'string')
: []
)
),
]
}
const readBoundingBox = async (entityIds: string[]) => {
const response = await sendModelingCommand({
type: 'bounding_box',
entity_ids: entityIds,
output_unit: 'mm',
})
if (
response.type !== 'bounding_box' ||
typeof response.data !== 'object' ||
response.data === null
) {
throw new Error('HiCAD 引擎没有返回尺寸数据')
}
return response.data as BoundingBox
}
const captureSnapshot = async () => {
const response = await sendModelingCommand({
type: 'take_snapshot',
@ -239,8 +315,26 @@ export function HicadManufacturingPanel({
await resetExplodeTransforms()
}
const bodyEntries = [...getBodiesFromArtifactGraph(kclManager.artifactGraph)]
if (bodyEntries.length === 0) {
const artifactBodies = [
...getBodiesFromArtifactGraph(
kclManager.execStateSignal.value.artifactGraph
),
]
let sceneBodyIds: string[] = []
try {
sceneBodyIds = await getSceneSolidIds()
} catch (error) {
console.warn(
'[HiCAD manufacturing] Unable to enumerate scene solids; using artifact graph',
error
)
}
const bodyIds =
sceneBodyIds.length > 0
? sceneBodyIds
: artifactBodies.map(([bodyId]) => bodyId)
if (bodyIds.length === 0) {
throw new Error('当前模型还没有可识别的实体')
}
@ -248,22 +342,9 @@ export function HicadManufacturingPanel({
string,
unknown
>
const measured = await Promise.all(
bodyEntries.map(async ([bodyId], index): Promise<BodyMeasurement> => {
const boundingResponse = await sendModelingCommand({
type: 'bounding_box',
entity_ids: [bodyId],
output_unit: 'mm',
})
if (
boundingResponse.type !== 'bounding_box' ||
typeof boundingResponse.data !== 'object' ||
boundingResponse.data === null
) {
throw new Error(`无法读取零件 ${index + 1} 的尺寸`)
}
const boundingBox = boundingResponse.data as BoundingBox
const measurementResults = await Promise.allSettled(
bodyIds.map(async (bodyId, index): Promise<BodyMeasurement> => {
const boundingBox = await readBoundingBox([bodyId])
const [volumeResponse, areaResponse] = await Promise.allSettled([
sendModelingCommand({
type: 'volume',
@ -293,18 +374,63 @@ export function HicadManufacturingPanel({
}
})
)
const measured = measurementResults.flatMap((result) =>
result.status === 'fulfilled' ? [result.value] : []
)
const measurementErrors = measurementResults.flatMap((result) =>
result.status === 'rejected'
? [
result.reason instanceof Error
? result.reason.message
: String(result.reason),
]
: []
)
if (measured.length === 0) {
try {
const overallBoundingBox = await readBoundingBox([])
measured.push({
id: 'hicad-scene-aggregate',
name: '模型整体',
center: overallBoundingBox.center,
dimensions: overallBoundingBox.dimensions,
})
toast(
'当前引擎只返回了整模尺寸,已生成整体工程记录;独立零件 BOM 需模型包含可分离实体。'
)
} catch (error) {
const details =
measurementErrors[0] ||
(error instanceof Error ? error.message : String(error))
throw new Error(`无法读取模型尺寸:${details}`)
}
} else if (measurementErrors.length > 0) {
toast(
`已识别 ${measured.length} 个实体,另有 ${measurementErrors.length} 个实体暂时无法测量。`
)
}
setBodies(measured)
await engineViewIsometric({
engineCommandManager: kclManager.engineCommandManager,
padding: 0.22,
})
await engineStreamZoomToFit({
engineCommandManager: kclManager.engineCommandManager,
padding: 0.24,
})
await Promise.allSettled([
engineViewIsometric({
engineCommandManager: kclManager.engineCommandManager,
padding: 0.22,
}),
engineStreamZoomToFit({
engineCommandManager: kclManager.engineCommandManager,
padding: 0.24,
}),
])
await new Promise((resolve) => setTimeout(resolve, 180))
await captureSnapshot()
try {
await captureSnapshot()
} catch (error) {
console.warn(
'[HiCAD manufacturing] Snapshot failed after successful analysis',
error
)
}
return measured
} catch (error) {
const message =