fix: make HiCAD engineering analysis resilient
This commit is contained in:
parent
c028d2ad00
commit
194762835f
@ -76,6 +76,18 @@ test.describe('HiCAD manufacturing extension', () => {
|
|||||||
)
|
)
|
||||||
expect(launcher).toContain("lazy(() =>")
|
expect(launcher).toContain("lazy(() =>")
|
||||||
expect(launcher).toContain('ManufacturingBoundary')
|
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', () => {
|
test('creates non-zero reversible offsets for every independent body', () => {
|
||||||
|
|||||||
@ -86,11 +86,37 @@ function resolveBodyName(
|
|||||||
function modelingResponseData(response: unknown): ModelingResponse {
|
function modelingResponseData(response: unknown): ModelingResponse {
|
||||||
const normalized = Array.isArray(response) ? response[0] : response
|
const normalized = Array.isArray(response) ? response[0] : response
|
||||||
if (!isModelingResponse(normalized)) {
|
if (!isModelingResponse(normalized)) {
|
||||||
throw new Error('HiCAD 引擎未返回有效结果')
|
throw new Error(engineFailureMessage(response))
|
||||||
}
|
}
|
||||||
return normalized.resp.data.modeling_response as ModelingResponse
|
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) {
|
function numberValue(data: unknown, key: string) {
|
||||||
if (typeof data !== 'object' || data === null) return undefined
|
if (typeof data !== 'object' || data === null) return undefined
|
||||||
const value = (data as Record<string, unknown>)[key]
|
const value = (data as Record<string, unknown>)[key]
|
||||||
@ -175,7 +201,13 @@ export function HicadManufacturingPanel({
|
|||||||
return modelingResponseData(normalized)
|
return modelingResponseData(normalized)
|
||||||
}
|
}
|
||||||
if (response !== null && response !== undefined) {
|
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))
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
@ -184,6 +216,50 @@ export function HicadManufacturingPanel({
|
|||||||
throw new Error('HiCAD 引擎连接尚未就绪,请稍后重新分析')
|
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 captureSnapshot = async () => {
|
||||||
const response = await sendModelingCommand({
|
const response = await sendModelingCommand({
|
||||||
type: 'take_snapshot',
|
type: 'take_snapshot',
|
||||||
@ -239,8 +315,26 @@ export function HicadManufacturingPanel({
|
|||||||
await resetExplodeTransforms()
|
await resetExplodeTransforms()
|
||||||
}
|
}
|
||||||
|
|
||||||
const bodyEntries = [...getBodiesFromArtifactGraph(kclManager.artifactGraph)]
|
const artifactBodies = [
|
||||||
if (bodyEntries.length === 0) {
|
...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('当前模型还没有可识别的实体')
|
throw new Error('当前模型还没有可识别的实体')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,22 +342,9 @@ export function HicadManufacturingPanel({
|
|||||||
string,
|
string,
|
||||||
unknown
|
unknown
|
||||||
>
|
>
|
||||||
const measured = await Promise.all(
|
const measurementResults = await Promise.allSettled(
|
||||||
bodyEntries.map(async ([bodyId], index): Promise<BodyMeasurement> => {
|
bodyIds.map(async (bodyId, index): Promise<BodyMeasurement> => {
|
||||||
const boundingResponse = await sendModelingCommand({
|
const boundingBox = await readBoundingBox([bodyId])
|
||||||
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 [volumeResponse, areaResponse] = await Promise.allSettled([
|
const [volumeResponse, areaResponse] = await Promise.allSettled([
|
||||||
sendModelingCommand({
|
sendModelingCommand({
|
||||||
type: 'volume',
|
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)
|
setBodies(measured)
|
||||||
await engineViewIsometric({
|
await Promise.allSettled([
|
||||||
engineCommandManager: kclManager.engineCommandManager,
|
engineViewIsometric({
|
||||||
padding: 0.22,
|
engineCommandManager: kclManager.engineCommandManager,
|
||||||
})
|
padding: 0.22,
|
||||||
await engineStreamZoomToFit({
|
}),
|
||||||
engineCommandManager: kclManager.engineCommandManager,
|
engineStreamZoomToFit({
|
||||||
padding: 0.24,
|
engineCommandManager: kclManager.engineCommandManager,
|
||||||
})
|
padding: 0.24,
|
||||||
|
}),
|
||||||
|
])
|
||||||
await new Promise((resolve) => setTimeout(resolve, 180))
|
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
|
return measured
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user